How Do You Do Your Todos?

State management options in React 16.3 with Typescript

Daniel Cook
ITNEXT

--

“Modern geometric neon bridge architecture at night on walkway, High Trestle Trail Bridge” by Tony Webster on Unsplash

As Javascript applications moved beyond small nuggets of script or plain DOM manipulation into fully-fledged Single Page Applications several problems of scale emerged. One, the problem of code organization, went through various iterations (gulp, grunt, bower etc) until Webpack and NPM came to rule the roost as bundler and dependency manager of choice. Eventually some of the brightest minds in the community turned their attention to the scalability of data and state management became a hot topic. The Flux architecture, with its core of immutability and unidirectional data flow, which originated in Facebook proved the stimulus for various state management solutions.

Some frameworks (e.g. Vue with Vuex) have an official state management package. React has redux which is undeniably the most popular solution, however there are alternatives and, even in redux, there are problems to be solved around how to model side-effects (think remote data access as a common side effect).

With the arrival of React 16.3 the React Context API went from an undocumented and unstable part of the architecture to another potential alternative for smaller applications. I decided to produce a canonical Todo application using 6 different approaches to get a flavour of how they differ from each other. The 6 state management options are:

  1. Local state — setState and passing state around using props
  2. Redux w/ redux-thunk
  3. Redux w/ redux-observable
  4. Redux w/ redux-saga
  5. Mobx
  6. React Context — setState and using the context to pass data to child components

The code is here. The master branch implements the baseline solution using local state and each other state management option is a branch from that, as explained in the readme. Each solution also uses typescript and the antd component library.

There are already a whole host of examples available online, the reason I thought it was worth another is that I found some of them used older versions of the state management libraries or of typescript and wouldn’t work as written. I hope to keep the code up to date as new versions are released. Hopefully this will be useful for people taking a look at these options.

Local State

React components, non-functional ones at least, come with their own state management API, setState. State is local to the component and initialized with a class level property:

interface AppState {
filter: string;
todosLoading: boolean;
todos: Todo[];
}
....state: AppState = {
filter: 'ALL',
todosLoading: false,
todos: []
};

Updates to the state object however are not made by directly mutating this class level property, instead you should call setState passing in either a set of state changes which will be merged with the current state or a function which takes the current state and props of the component as parameters and again returns an object of changes to be merged into the component state.

addTodo = async (title: string) => {
const response = await api.add(title);
this.setState({
todos: [ ...this.state.todos, response.data ]
});
}

As state is local to a single component, it is shared with sub-components via props. Props can be data from the state or functions that will be called to propagate state updates via setState.

This is already a sophisticated state management option, based on sound principles of immutability and unidirectional data flow. Indeed, as the creator of Redux himself has stated, you may not need it. However, as your application grows you may find the props passing becomes tedious and error-prone, as different parts of your app attempt to share the same data the state is pushed higher and higher up the hierarchy. At that point you may decide you need Redux.

Redux w/ redux-thunk

Redux is a “predictable state container for JavaScript app”. It is a way of taking the local state and putting it into a container (known as a store), this can then be connected to components in your application who can receive data from the store as props and dispatch updates to the store as actions. The terminology around redux (reducers, action creators) can seem arcane for developers coming from an object-oriented background however once it clicks they become relatively straightforward, in fact the core functionality of redux itself can be implemented in ~20 lines of code!

A reducer is a function which takes the current state and an action object and returns the new state.

export default (state: AppState, action: TodoAction): AppState => {
switch (action.type) {
case ActionTypes.ADD_TODO_SUCCESS:
return {
...state,
todos: [ ...state.todos, (action as AddTodoAction).todo]
};
}
}

An action is a plain javascript object with a type property (string) and a payload.

{
type: 'ADD_TODO_SUCCESS',
todo: {
text: 'Walk the dog',
completed: false
}
}

Redux is often associated with React as it works so well with it, however it can be used in other JavaScript frameworks. To link our React components to the store we build “Containers”, these use the react-redux “connect” higher-order function to link component props to state properties and actions. Our components can then be easy to test functional components with no knowledge of the state management system being used.

Redux is extensible through the use of middleware, one such is redux-thunk which solves the problem of asynchronous actions by allowing action creators to return a function rather than the action object. This allows side-effects such as calls to an external API to be rolled into our store actions.

export const addTodo = (title: string) => {
return async (dispatch: Dispatch) => {
const response = await api.add(title);
dispatch(addTodoSuccess(response.data));
};
};

Redux w/ redux-observable

Redux-observable is another middleware for redux asynchronous actions, it approaches the problem from an alternative direction, using RxJS observables. The core concept is the “Epic”, a function which receives a stream of actions and returns a stream of actions.

const fetchTodoEpic = (action$: ActionsObservable<FetchTodosAction>) =>
action$
.ofType(ActionTypes.FETCH_TODOS)
.mergeMap((action: FetchTodosAction) =>
Observable.ajax({
url: baseUrl,
withCredentials: false,
crossDomain: true
})
.map(req => fetchTodosSuccess(req.response)));

RxJS comes with a large amount of “operators”, functions for manipulating the stream such as mergeMap in the example above. For instance, the takeUntil operator allows an action to be dispatched which cancels the async operation, a big selling point of redux-observable.

Redux w/ redux-saga

Redux-saga is yet another middleware for redux that is aimed at making side-effects easier. It uses ES6 generators which means that the async flows become incredibly easy to read (assuming you understand the generator syntax).

export function* fetchTodos() {
yield put(updateLoading(true));
const response = yield call(api.fetch);
yield put(loadTodosSuccess(response.data));
yield put(updateLoading(false));
}

Out of the 3 redux middlewares this was the one I enjoyed using the most, I think the separation between sagas and actions makes the end-user code very readable and simple to understand and test.

Mobx

Mobx takes a very different approach to state management to Redux, one that will be very familiar to developers who have come from Vue and Vuex. A Mobx store is “reactive”, you choose the shape of the data structure that aligns with your state and then use annotations to mark it as observable. Your components then use an observer annotation to enable them to react to changes in the store state.

@observable todosLoading = false;
@observable todos: Todo[] = [];
async refreshTodos() {
this.todosLoading = true;

const response = await api.fetch();
this.todos = response.data;
this.todosLoading = false;
}

Coming from Redux and setState this is a bit of a culture shock, where is the immutability? It is certainly very different however it allows some nice abstractions, for instance the computed annotation allows computed property logic to be placed in the store.

@computed get filteredTodos() {
switch (this.filter) {
case 'ACTIVE':
return this.todos.filter(todo => !todo.completed);
case 'COMPLETED':
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}

React Context

React’s context API has long been marked as experimental and subject to change, however its consumer/provider model was the basis for Redux’s providers. With 16.3 the API has stabilised and is now another option for state management.

Similarly to the local state option state and the functions that operate on it sit in a components towards the top of the hierarchy. However instead of passing the state into sub-components via props it is placed in a context using the createContext function.

export interface AppState {
filter: string;
loading: boolean;
todos: Todo[];
updateFilter: (filter: string) => void;
addTodo: (title: string) => void;
toggleTodo: (todo: Todo) => void;
deleteTodo: (todo: Todo) => void;
}
export const TodoContext = React.createContext<AppState>({
filter: 'ALL',
loading: false,
todos: [],
updateFilter: (filter: string) => {},
addTodo: (title: string) => {},
toggleTodo: (todo: Todo) => {},
deleteTodo: (todo: Todo) => {}
});

TodoContext in the code above is an object with 2 properties, Provider and Consumer, these can then be used in our component JSX to hook components up to the context. The Consumer is notable as it uses another pattern, the “render prop” to allow the context to be accessed when rendering a child component.

I hope that the brief descriptions above and, more importantly, the code give a straightforward introduction to each method of state management.

--

--