Jul
11

Advanced Redux in Xamarin Part 1: Action Creators

posted on 11 July 2017 in programming with 0 Comments

Redux is an implementation of the Flux architecture that manages UI changes through a single global State object that can only be mutated by dispatching Actions. It provides a way to easily reason about state changes and to share that state across multiple separated areas of the UI.

Originally developed for React in JavaScript, it’s now finding its way into other languages, there are a number of implementations of Redux in .NET (e.g. Redux.NET, YAXL.Redux and Reducto). Rather than rehashing the basics of how to use Redux in Xamarin (there are plenty of good articles), in this post series we’ll look at some more advanced concepts, starting with Action Creators.

In Redux the only way to manipulate state is by dispatching an Action, an Action is a simple POCO that carries enough information for the Reducers to update the state. Action Creators are simply methods that create Actions, e.g

public class TodoActionCreators
{
    public static IAction AddTodo(Guid id, string text)
    {
        return new AddTodoAction() {
            Id = id,
            Text = text
        };
    }
}

Action Creators can be thought of as an SDK for your Actions, they provide a clean interface for your code to create Actions and can encapsulate any external or impure interactions, like calling an API.

Redux Thunks

A Redux Thunk provides a way to return a function from an Action Creator rather than returning an Action directly. This allows you to do conditional or asynchronous dispatching. There is a Redux.NET Thunk middleware available, but I prefer the store extensions approach found in the Redux.NET examples. We’ve adapted it slightly to allow it to return asynchronous or synchronous actions.

namespace Helpers
{
    public delegate Task AsyncActionsCreator<TState>(Dispatcher dispatcher, Func<TState> getState);
    public delegate void ActionsCreator<TState>(Dispatcher dispatcher, Func<TState> getState);

    public static class StoreExtensions
    {
        /// <summary>
        /// Extension on IStore to dispatch multiple actions via a thunk. 
        /// Can be used like https://github.com/gaearon/redux-thunk without the need of middleware.
        /// </summary>
        public static Task DispatchAsync<TState>(this IStore<TState> store, AsyncActionsCreator<TState> actionsCreator)
        {
            return actionsCreator(store.Dispatch, store.GetState);
        }

        public static void Dispatch<TState>(this IStore<TState> store, ActionsCreator<TState> actionsCreator)
        {
            actionsCreator(store.Dispatch, store.GetState);
        }
    }
}

And it’s used like this

public class TodoActionCreators
{
    public static AsyncActionsCreator<ApplicationState> AddTodo(Guid id, string text)
    {
        return async (dispatch, getState) =>
        {
            dispatch(new AddTodoRequestAction() { Id = id, Text = text });
            var resp = await ApiService.AddTodo(text);
            if (resp.IsSuccess)
            {
                dispatch(new AddTodoSuccessAction() { Id = id, Text = text });
            }
            else
            {
                dispatch(new AddTodoFailureAction() { Id = id, Error = resp.Error });
            }
        };
    }
}

By emitting a Request action and matching Success or Failure actions, you can optimistically update the application state / display, then rollback the change only if it fails. An optimistic Reducer would look like this

public class Reducers
{
    public static ApplicationState ReduceApplication(ApplicationState previousState, IAction action)
    {
        return new ApplicationState { Todos = TodosReducer(previousState.Todos, action) };
    }

    private static ImmutableArray<TodoItem> TodosReducer(ImmutableArray<TodoItem> previousState, IAction action)
    {
        if (action is AddTodoRequestAction)
        {
            return AddTodoRequestReducer(previousState, (AddTodoRequestAction)action);
        }
        if (action is AddTodoSuccessAction)
        {
            return AddTodoSuccessReducer(previousState, (AddTodoSuccessAction)action);
        }
        if (action is AddTodoFailureAction)
        {
            return AddTodoFailureReducer(previousState, (AddTodoFailureAction)action);
        }
        return previousState;
    }

    private static ImmutableArray<TodoItem> AddTodoRequestReducer(ImmutableArray<TodoItem> previousState, AddTodoRequestAction action)
    {
        return previousState
            .Insert(0, new TodoItem()
                    {
                    Id = action.Id,
                    Text = action.Text,
                    Pending = true
                    });
    }

    private static ImmutableArray<TodoItem> AddTodoSuccessReducer(ImmutableArray<TodoItem> previousState, AddTodoRequestAction action)
    {
        return previousState
            .Select(x =>
                    {
                    if (x.Id == action.Id)
                    {
                    return new TodoItem()
                    {
                    Id = action.Id,
                    Text = action.Text,
                    Pending = false
                    };
                    }
                    return x;
                    })
        .ToImmutableArray();
    }

    private static ImmutableArray<TodoItem> AddTodoFailureReducer(ImmutableArray<TodoItem> previousState, AddTodoFailureAction action)
    {
        return previousState
            .Where(x => x.Id != action.Id)
            .ToImmutableArray();
    }
}

Alternatively, you could set an IsLoading property of the state and show a spinner while you wait for the Success / Failure.

Project Structure

There are a few best practices for structuring Redux projects, the most typical is to collect all Actions, Reducers, Action Creators and Middleware into separate folders (see the Redux sample projects), we put ours all within a Redux folder in our PCL.

Redux Project Structure in a Xamarin app

 

In part 2 of this series we’ll look at how to persist and restore Actions on app restart using Redux Middleware.

Part 2