Comparison of state management solutions for react

September 18, 2018

Update 2020/06

React added with 16.08 Supoort for React Hooks. Especially the useContext-hook became one which replaced all my need for other statemanagement-solutions.

Nowadays I would create a Todo Context as following

import React, { useState, useContext } from "react"
import * as helper from "../helper"
import { TodoList } from "../components/TodoList"
import { CreateTodoInput } from "../components/CreateTodoInput"

export const todoContext = React.createContext({
  todos: [],
  actions: {
    createTodoItem: () => undefined,
    updateTodoItem: () => undefined,
    removeTodoItem: () => undefined,
  },
})

export const TodoProvider = ({ children, initialTodos }) => {
  const [todos, setTodos] = useState(initialTodos)
  console.log(initialTodos)
  const removeTodoItem = (todoId: number) => {
    setTodos(helper.remove(todos, todoId))
  }

  const createTodoItem = (todoTitle: string) => {
    setTodos(helper.create(todos, new Todo(todoTitle)))
  }

  const updateTodoItem = (todoId: number, todo: ITodo) => {
    setTodos(helper.update(todos, todoId, todo))
  }

  return (
    <todoContext.Provider
      value={{
        todos,
        actions: {
          createTodoItem,
          removeTodoItem,
          updateTodoItem,
        },
      }}
    >
      {children}
    </todoContext.Provider>
  )
}

Somewhere in the application I would define a component which connects to the context with useContext.

const ContextHookTodoList = () => {
  const { actions, todos } = useContext(todoContext)
  return (
    <React.Fragment>
      <h1>Context Hook Todo</h1>
      <CreateTodoInput createTodoItem={actions.createTodoItem} />
      <TodoList
        todos={todos}
        removeTodoItem={actions.removeTodoItem}
        updateTodoItem={actions.updateTodoItem}
      />
    </React.Fragment>
  )
}

To provide the Context to the Application the Context Provider has to contain all Components which use the context value.

export const ContextHookTodoApp = ({ initialState: { todos } }) => {
  return (
    <TodoProvider initialTodos={todos}>
      <ContextHookTodoList />
    </TodoProvider>
  )
}

Intro

The component-based approach of React and other frontend frameworks like Vue and Angular has changed the way our web looks like today. One massive part of their success story is the way components communicate and share state with each other. This empowers the developer to create maintainable software by separating different parts of logic and state into dedicated components that pushes our future web.

In this article I will present multiple state-of-the-art state management libraries and line out in which scenario each of them shines and where they do not.

Why do we need state at all

In an application, state is the interface between your data from any kind of backend or local change and the representation of this data with UI-elements in the frontend. State is able to keep the data of different components in sync because each state update will rerender all relevant components. State can be a medium to communicate between different components aswell.

Because state plays an essential role in applications and there are so many different paradigms and libraries to keep your state managed I implemented a simple todo application which is driven by each of the described libraries to differentiate how they are used.

The Setting

To inject the different libraries seamlessly I have built a few helper functions to abstract the usage of those libraries. The mountHelper class wraps the todo list component with all those providers and settings you would ideally place at the root-level of your application. I also wanted to provide an initial state to each state management library so that i have got the possibility to write integration tests against each library with the todo application.

import * as React from "react"
import { StateTodoList } from "./provider/StateTodoList"
import { ContextTodoList } from "./provider/ContextTodoList"
import {
  UnstatedTodoList,
  TodoListContainer,
  mountUnstated,
} from "./provider/UnstatedTodoList"
import { Provider, Subscribe, Container } from "unstated"
import {
  ApolloLinkStateTodoList,
  mountWithApollo,
} from "./provider/ApolloLinkStateTodoList"
import {
  ConnectedReduxTodoList,
  mountWithRedux,
} from "./provider/ReduxTodoList"

import {
  ConnectedReduxThunkTodoList,
  mountWithReduxThunk,
} from "./provider/ReduxThunkTodoList"

export const mountWithInitialProps = (initialState, C) => (
  <C initialState={initialState ? { ...initialState } : null} />
)

export const providers = {
  StateTodoList: {
    component: StateTodoList,
    mounter: mountWithInitialProps,
  },
  ContextTodoList: {
    component: ContextTodoList,
    mounter: mountWithInitialProps,
  },
  UnstatedTodoList: {
    component: UnstatedTodoList,
    mounter: mountUnstated(TodoListContainer),
  },
  ApolloLinkStateTodoList: {
    component: ApolloLinkStateTodoList,
    mounter: mountWithApollo,
  },
  ReduxTodoList: {
    component: ConnectedReduxTodoList,
    mounter: mountWithRedux,
  },
  ReduxThunkTodoList: {
    component: ConnectedReduxThunkTodoList,
    mounter: mountWithReduxThunk,
  },
}

export const renderWithProvider = ({ component, mounter }, initialState) =>
  mounter(initialState, component)

Another helper class I created is responsible for calculating the updated state object after an interaction with the state, because this business logic is equal for all libraries.

import { IEntity } from "./types"

export const update = (
  entityArray: IEntity[],
  entityId: number,
  data: IEntity & Object
) => {
  return entityArray.reduce((prev, cur) => {
    const { id, ...rest } = data
    if (cur.id === entityId) {
      prev.push({ id: entityId, ...rest })
    } else {
      prev.push(cur)
    }
    return prev
  }, [])
}
export const remove = (entityArray: IEntity[], itemId: number) =>
  entityArray.filter(cur => cur.id !== itemId)

export const create = (entityArray: IEntity[], item: IEntity & Object) => [
  ...entityArray,
  item,
]

Component state

React includes several ways of managing state in an application. The most straight forward way is to define a state inside a component. The state of a component is like the props which are passed to a component, a plain JavaScript object containing information that influences the way a component is rendered. In comparison, to the props, the state can be changed by the component itself by calling setState which will trigger a re-render of the component. The state API of React is really simple at all and doesn’t add too much complexity to your application. Besides all other state management solutions, the component state is preferred to not be replaced at all because you should always keep your state as close to where it is needed to avoid unnecessary complexity. Because managing the state of an single input in a global state isn’t what you are aiming for.

import * as React from "react"
import { render } from "react-dom"

import { ITodo, Todo } from "../types"
import { TodoList } from "../components/TodoList"
import { CreateTodoInput } from "../components/CreateTodoInput"

import * as helper from "../helper"

export class StateTodoList extends React.Component {
  /* TODO: find out why it may be useful to insert
   * your state Functions inside the state
   * (e.g.: https://reactjs.org/docs/context.html#updating-context-from-a-nested-component)
   */

  constructor(props) {
    super(props)

    const defaultState = { todos: [] }
    this.state = props.initialState ? props.initialState : defaultState
  }

  removeTodoItem = (todoId: number) => {
    this.setState(state => {
      return {
        todos: helper.remove(state.todos, todoId),
      }
    })
  }

  createTodoItem = (todoTitle: string) => {
    this.setState(state => {
      return {
        todos: helper.create(state.todos, new Todo(todoTitle)),
      }
    })
  }

  updateTodoItem = (todoId: number, todo: ITodo) => {
    this.setState(state => {
      return {
        todos: helper.update(state.todos, todoId, todo),
      }
    })
  }

  render() {
    const { todos } = this.state
    return (
      <React.Fragment>
        <h1>SetState Todo</h1>
        <CreateTodoInput createTodoItem={this.createTodoItem} />
        <TodoList
          todos={todos}
          removeTodoItem={this.removeTodoItem}
          updateTodoItem={this.updateTodoItem}
        />
      </React.Fragment>
    )
  }
}

As you can see, it is totally possible to write your app just with component state, but as your component dependencies and the size and complexity of your app grows you will find yourself by pushing the state up in your component -tree to inject the relevant state in several components. The distance between your states location and your components which need a certain part of the state will increase. This leads to prop drilling, meaning passing props through components which don’t need the props but their children do. You want to avoid this because it increases the complexity, for example during a refactoring.

To refresh your knowledge about the state API I would recommend the official React docs which contain a basic faq for state and an example where state is shared between different components.

Context API

The Context API was added to React in version 16.3.0 earlier this year. The Context API React provides an internal solution for passing state to where it is needed and avoids the possibility of prop drilling.

This is achieved by providers what provide a certain component state to all consumers that are located somewhere in the React component tree below the provider.

<Provider value={/* some value */}>

Beneath the state, a provider can also provide functions to manipulate the state within the provided values.

<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

The consumer accepts a render function and is able to access the value prop from the provider.

import * as React from "react"
import { render } from "react-dom"

import { ITodo, Todo } from "../types"
import { TodoList } from "../components/TodoList"
import { CreateTodoInput } from "../components/CreateTodoInput"

import * as helper from "../helper"

const Context = React.createContext()

export class ContextTodoList extends React.Component {
  static Consumer = Context.Consumer
  static Provider = Context.Provider

  constructor(props) {
    super(props)

    const defaultState = { todos: [] }
    this.state = props.initialState ? props.initialState : defaultState
  }

  /* TODO: find out why it may be useful to insert
   * your state Functions inside the state
   * (e.g.: https://reactjs.org/docs/context.html#updating-context-from-a-nested-component)
   */

  removeTodoItem = (todoId: number) => {
    this.setState(state => {
      return {
        todos: helper.remove(state.todos, todoId),
      }
    })
  }

  createTodoItem = (todoTitle: string) => {
    this.setState(state => {
      return {
        todos: helper.create(state.todos, new Todo(todoTitle)),
      }
    })
  }

  updateTodoItem = (todoId: number, todo: ITodo) => {
    this.setState(state => {
      return {
        todos: helper.update(state.todos, todoId, todo),
      }
    })
  }

  render() {
    return (
      <ContextTodoList.Provider
        value={{
          state: {
            ...this.state,
          },
          actions: {
            createTodoItem: this.createTodoItem,
            updateTodoItem: this.updateTodoItem,
            removeTodoItem: this.removeTodoItem,
          },
        }}
      >
        <React.Fragment>
          <h1>Context Todo</h1>
          {/* the two consumer seem useless here, but imagine them somewhere nested in our UI */}
          <ContextTodoList.Consumer>
            {({ actions }) => (
              <CreateTodoInput createTodoItem={actions.createTodoItem} />
            )}
          </ContextTodoList.Consumer>
          <ContextTodoList.Consumer>
            {({ state, actions }) => (
              <TodoList
                todos={state.todos}
                removeTodoItem={actions.removeTodoItem}
                updateTodoItem={actions.updateTodoItem}
              />
            )}
          </ContextTodoList.Consumer>
        </React.Fragment>
      </ContextTodoList.Provider>
    )
  }
}

/*render(
  <ContextTodoList
    todos={[
      new Todo("Statemanagement with SetState", true),
      new Todo("Statemanagement with React.Context"),
      new Todo("Statemanagement with Unstated"),
      new Todo("Statemanagement with MobX"),
      new Todo("Statemanagement with Redux"),
      new Todo("Statemanagement with Redux Thunk"),
      new Todo("Statemanagement with Apollo Link State")
    ]}
  />,
  document.getElementById("root")
);*/

Unstated

Unstated by Jamie Kyle is a state management library that uses the Context API internally.

Unstated is designed to build on top of the patterns already set out by React components and context.

The state is managed in a container that also includes methods to work with a state object. A container looks and feels like any React component without the UI-part. Also, the setState function mimics React’s setState the only difference is, that Unstated’s setState returns a Promise you can await.

import { Container } from 'unstated';

type CounterState = {
  count: number
};

class CounterContainer extends Container<CounterState> {
  state = {
    count: 0
  };

  increment() {
    await this.setState({ count: this.state.count + 1 });
    console.log("count",this.state.count) // this works with Unstated
  }

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}

If you desire a specified state from a container you can subscribe to it with a subscriber. A subscriber needs a render function as a child, like a consumer if you are using context.

function Counter() {
  return (
    <Subscribe to={[CounterContainer]}>
      {counter => (
        <div>
          <button onClick={() => counter.decrement()}>-</button>
          <span>{counter.state.count}</span>
          <button onClick={() => counter.increment()}>+</button>
        </div>
      )}
    </Subscribe>
  )
}
import * as React from "react"
import { render } from "react-dom"

import { ITodo, Todo } from "../types"
import { TodoList } from "../components/TodoList"
import { CreateTodoInput } from "../components/CreateTodoInput"
import { Provider, Subscribe, Container } from "unstated"

import * as helper from "../helper"

export const mountUnstated = Container => (
  initialState,
  Component: component
) => {
  const container = new Container({
    initialState: initialState ? initialState : null,
  })
  return (
    <Provider inject={[container]}>
      <Component />
    </Provider>
  )
}

export class TodoListContainer extends Container {
  constructor(props) {
    super(props)

    const defaultState = { todos: [] }
    this.state = props.initialState ? props.initialState : defaultState
  }

  removeTodoItem = (todoId: number) => {
    this.setState(state => {
      return {
        todos: helper.remove(state.todos, todoId),
      }
    })
  }

  createTodoItem = (todoTitle: string) => {
    this.setState(state => {
      return {
        todos: helper.create(state.todos, new Todo(todoTitle)),
      }
    })
  }

  updateTodoItem = (todoId: number, todo: ITodo) => {
    this.setState(state => {
      return {
        todos: helper.update(state.todos, todoId, todo),
      }
    })
  }
}

export const UnstatedTodoList = () => (
  <Subscribe to={[TodoListContainer]}>
    {list => (
      <React.Fragment>
        <h1>Unstated Todo</h1>
        {/* the two consumer seem useless here, but imagine them somewhere nested in our UI */}
        <CreateTodoInput createTodoItem={list.createTodoItem} />
        <TodoList
          todos={list.state.todos}
          removeTodoItem={list.removeTodoItem}
          updateTodoItem={list.updateTodoItem}
        />
      </React.Fragment>
    )}
  </Subscribe>
)

With Unstated you are able to split your UI logic from your state logic in contrast to the Context API.

Redux

The preceding libraries and API’s work very well but if you want to have control over what is happening and especially why something is happening in your application it can be hard to debug certain state updates. In this case, Redux may help you by forcing you to work in a certain form.

Redux is a predictable state container for JavaScript apps.

Which means, that you are able to reproduce each state update that has happened if you reapply the same actions to the Redux store.

The Redux store is defined by reducers. A reducer is a pure function that takes the previous state and an action and returns the next state. An action is an object that contains a type and additional properties. You are modifying the Redux state by dispatching an action with a certain type. Afterwards, each reducer checks if it accepts the type. If the reducer accepts the type the reducer reduces the action to a new reducer state.

If you want to provide the reducer state to a component you have to connect your component with the connect function from react-redux. The function accepts two functions as parameter mapStateToProps that maps the Redux store to a property and mapDispatchToProps which maps functions/action creators as properties which are allowed to dispatch actions to the Redux store. The connect function is a higher order component that injects the desired props into the wrapped component. In the example below, you can see a Redux setup for a counter application.

import React from "react"
import ReactDOM from "react-dom"
import { Provider, connect } from "react-redux"
import { createStore } from "redux"

const reducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1
    case "DECREMENT":
      return state - 1
    default:
      return state
  }
}

const store = createStore(reducer)
const rootEl = document.getElementById("root")

const Counter = ({ onIncrement, onDecrement, value }) => (
  <React.Fragment>
    <button onClick={onIncrement}>+</button>
    {value}
    <button onClick={onDecrement}>-</button>
  </React.Fragment>
)

const mapStateToProps = state => {
  return {
    value: state,
  }
}

const mapDispatchToProps = dispatch => ({
  onIncrement: () => dispatch({ type: "INCREMENT" }),
  onDecrement: () => dispatch({ type: "DECREMENT" }),
})

const ConnectedCounter = connect(mapStateToProps, mapDispatchToProps)(Counter)

ReactDOM.render(
  <Provider store={store}>
    <ConnectedCounter />
  </Provider>,
  rootEl
)

The todo list implementation can be seen below.

import * as React from "react"
import { render } from "react-dom"

import { createStore } from "redux"
import { connect, Provider } from "react-redux"
import { mainReducer as reducer } from "./reducers"
import * as TodoActions from "./actions/Todo"

import { TodoList } from "../../components/TodoList"
import { CreateTodoInput } from "../../components/CreateTodoInput"

import { devToolsEnhancer } from "redux-devtools-extension"

export const mountWithRedux = (initialState, Component) => {
  const store = createStore(
    reducer,
    initialState ? initialState : {},
    devToolsEnhancer()
  )
  return (
    <Provider store={store}>
      <Component />
    </Provider>
  )
}

export class ReduxTodoList extends React.Component {
  render() {
    const {
      todos,
      createTodoItem,
      updateTodoItem,
      removeTodoItem,
      title,
    } = this.props
    return (
      <React.Fragment>
        <h1>{title ? title : "Redux List"}</h1>
        <CreateTodoInput createTodoItem={createTodoItem} />
        <TodoList
          todos={todos}
          removeTodoItem={removeTodoItem}
          updateTodoItem={updateTodoItem}
        />
      </React.Fragment>
    )
  }
}

const mapStateToProps = (state, ownProps) => ({
  todos: state.todos,
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  createTodoItem: title => {
    dispatch(TodoActions.createTodoItem(title))
  },
  updateTodoItem: (todoId, todo) => {
    dispatch(TodoActions.updateTodoItem(todoId, todo))
  },
  removeTodoItem: todoId => {
    dispatch(TodoActions.removeTodoItem(todoId))
  },
})

export const ConnectedReduxTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(ReduxTodoList)

Redux Thunk

When using Redux you are able to add middleware to your store. With middleware you are able to extend the behavior of React. For example, you can add a logger which logs all dispatched actions to the console or a lot more, just have a look at the ecosystem.

Redux Thunk is a middleware, besides Redux Saga and Redux Observable, that adds support for side-effects to Redux. This is especially relevant if you ‘re communicating with a server.

export const actions = {
  CREATE_TODO_START: "CREATE_TODO_START",
  CREATE_TODO_SUCCESS: "CREATE_TODO_SUCCESS",
  CREATE_TODO_ERROR: "CREATE_TODO_ERROR",
  UPDATE_TODO_START: "UPDATE_TODO_START",
  UPDATE_TODO_SUCCESS: "UPDATE_TODO_SUCCESS",
  UPDATE_TODO_ERROR: "UPDATE_TODO_ERROR",
  REMOVE_TODO_START: "REMOVE_TODO_START",
  REMOVE_TODO_SUCCESS: "REMOVE_TODO_SUCCESS",
  REMOVE_TODO_ERROR: "REMOVE_TODO_ERROR",
}

export const createTodoItem = (title: string) => async (dispatch, getState) => {
  dispatch({
    type: actions.CREATE_TODO_START,
  })
  // Mocking Server Communication here
  await new Promise(resolve => setTimeout(resolve, 300))
  if (Math.random() < 0.3) {
    dispatch({
      type: actions.CREATE_TODO_ERROR,
      error: {
        msg: "Unexpected Error",
      },
    })
  } else {
    dispatch({
      type: actions.CREATE_TODO_SUCCESS,
      payload: {
        title,
      },
    })
  }
}

export const updateTodoItem = (todoId, todo) => async (dispatch, getState) => {
  dispatch({
    type: actions.UPDATE_TODO_START,
  })
  // Mocking Server Communication here
  await new Promise(resolve => setTimeout(resolve, 300))
  if (Math.random() < 0.3) {
    dispatch({
      type: actions.UPDATE_TODO_ERROR,
      error: {
        msg: "Unexpected Error",
      },
    })
  } else {
    dispatch({
      type: actions.UPDATE_TODO_SUCCESS,
      payload: {
        todo,
        todoId,
      },
    })
  }
}

export const removeTodoItem = todoId => async (dispatch, getState) => {
  dispatch({
    type: actions.REMOVE_TODO_START,
  })
  // Mocking Server Communication here
  await new Promise(resolve => setTimeout(resolve, 300))
  if (Math.random() < 0.3) {
    dispatch({
      type: actions.REMOVE_TODO_ERROR,
      error: {
        msg: "Unexpected Error",
      },
    })
  } else {
    dispatch({
      type: actions.REMOVE_TODO_SUCCESS,
      payload: {
        todoId,
      },
    })
  }
}

By dispatching a single action creator from your component you are able to dispatch even more actions inside the action itself. This enables you to trigger a request inside an action creator which dispatches a success action with the payload from the server if the request was successful or an error action with the error message if the request failed. Since each reducer listens to all dispatched actions you are able to have different reducers that listen on the same actions like the loading or error reducer in the example above.

import * as React from "react"
import { render } from "react-dom"

import { createStore, compose, applyMiddleware } from "redux"
import { connect, Provider } from "react-redux"
import { mainReducer as reducer } from "./reducers"
import * as TodoActions from "./actions/Todo"
import { composeWithDevTools } from "redux-devtools-extension"

import thunk from "redux-thunk"

// The Redux Component behave exactly like in simple Redux
import { ReduxTodoList } from "../ReduxTodoList"

const LoadingReduxTodoList = props => {
  return (
    <React.Fragment>
      {props.loading !== 0 && "Loading"}
      {props.error !== null && props.error}
      <ReduxTodoList {...props} title="Redux Thunk Todo List" />;
    </React.Fragment>
  )
}

export const mountWithReduxThunk = (initialState, Component) => {
  const store = createStore(
    reducer,
    initialState ? initialState : { todos: [], loading: false, error: null },
    composeWithDevTools(applyMiddleware(thunk))
  )
  return (
    <Provider store={store}>
      <Component />
    </Provider>
  )
}

const mapStateToProps = (state, ownProps) => ({
  todos: state.todos,
  loading: state.loading,
  error: state.error,
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  createTodoItem: title => {
    dispatch(TodoActions.createTodoItem(title))
  },
  updateTodoItem: (todoId, todo) => {
    dispatch(TodoActions.updateTodoItem(todoId, todo))
  },
  removeTodoItem: todoId => {
    dispatch(TodoActions.removeTodoItem(todoId))
  },
})

export const ConnectedReduxThunkTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(LoadingReduxTodoList)

Besides that Redux is very good if you want to keep up with what happens in your state but it comes with the bitter taste of repetition and writing a lot of code for action creators and reducers. Sometimes this is tedious, but if its done you easily keep track. One step on top of Redux Thunk would be the idiomatic Redux concept by Dan Abramov which is described in one of his egghead courses.

Peggy Rayzis from Apollo implemented a library for managing local state as well to avoid the necessity for using a state management library from above when managing the state from the GraphQL Server in your app. You do not even need to consume a GraphQL API to use the expressive query language. You can simply query and mutate your local application state.

You define the state of your application by creating an ApolloClientwith LocalLink .

export const mountWithApollo = (initialState, Component) => {
  const cache = new InMemoryCache()
  // define the initial Store

  const defaultState = { todos: [] }
  const stateLink = withClientState({
    cache,
    defaults: initialState ? initialState : defaultState,
  })
  const client = new ApolloClient({
    cache,
    link: stateLink,
  })

  return (
    <ApolloProvider client={client}>
      <Component />
    </ApolloProvider>
  )
}

From this point on you are able read from your local state by writing GraphQL queries where each top level field includes the @client directive. By appending this directive Apollo knows, that you want to fetch from your local state.

const TODO_QUERY = gql`
  {
    todos [@client](http://twitter.com/client) {
      id
      title
      finished
    }
  }
`

If using Apollo with a GraphQL or REST server you can request those sources as well in one single request. When using apollo-link-rest you can wrap an ordinary REST-resource to be queried and mutated by GraphQL queries/mutations.

The result of the query can be accessed with the Query-component of Apollo, that accepts a render-method like the Subscriber in Unstated or the Consumer from the Context API.

<Query query={TODO_QUERY}>
  {({ client, data }) => <Component data={data} />}
</Query>

Mutating your local state is possible by writing GraphQL-mutations that get applied with cache.writeQuery or by mutating the state directly with cache.writeData.

A possible mutation with writeData looks like:

updateTodoItem = (cache, queryData) => (todoId: number, todo: ITodo) => {
  const currentTodos = queryData.todos
  cache.writeData({
    data: {
      todos: helper.update(currentTodos, todoId, todo),
    },
  })
}

The working Apollo Link State example can be seen below.

import * as React from "react"
import { render } from "react-dom"
import { ApolloClient } from "apollo-client"
import { HttpLink } from "apollo-link-http"
import { InMemoryCache } from "apollo-cache-inmemory"
import { ApolloProvider, Query } from "react-apollo"
import { withClientState } from "apollo-link-state"
import { ApolloLink } from "apollo-link"
import gql from "graphql-tag"

import { TodoList } from "../components/TodoList"
import { CreateTodoInput } from "../components/CreateTodoInput"

import { Todo, ITodo } from "../types"
import * as helper from "../helper"

export const mountWithApollo = (initialState, Component) => {
  const cache = new InMemoryCache()
  // define the initial Store

  const defaultState = { todos: [] }
  const stateLink = withClientState({
    cache,
    defaults: initialState ? initialState : defaultState,
  })
  const client = new ApolloClient({
    cache,
    link: stateLink,
  })

  return (
    <ApolloProvider client={client}>
      <Component />
    </ApolloProvider>
  )
}

// We are querying our local State with GraphQL
// @client defines that this element is called from cache
const TODO_QUERY = gql`
  {
    todos @client {
      id
      title
      finished
    }
  }
`

export class ApolloLinkStateTodoList extends React.Component {
  removeTodoItem = (cache, queryData) => (todoId: number) => {
    const currentTodos = queryData.todos
    cache.writeData({
      data: {
        todos: helper.remove(currentTodos, todoId),
      },
    })
  }

  createTodoItem = (cache, queryData) => (todoTitle: string) => {
    const currentTodos = queryData.todos
    cache.writeData({
      data: {
        todos: helper.create(currentTodos, new Todo(todoTitle)),
      },
    })
  }

  updateTodoItem = (cache, queryData) => (todoId: number, todo: ITodo) => {
    const currentTodos = queryData.todos
    cache.writeData({
      data: {
        todos: helper.update(currentTodos, todoId, todo),
      },
    })
  }

  render() {
    return (
      <Query query={TODO_QUERY}>
        {({ client, data }) => (
          <React.Fragment>
            <h1>Apollo Link State Todo</h1>
            <CreateTodoInput
              createTodoItem={this.createTodoItem(client, data)}
            />
            <TodoList
              todos={data.todos}
              removeTodoItem={this.removeTodoItem(client, data)}
              updateTodoItem={this.updateTodoItem(client, data)}
            />
          </React.Fragment>
        )}
      </Query>
    )
  }
}

Apollo Link State as state management library shines especially when having a GraphQL API as backend because it abstracts the source of data you are using to a single level which will simplifies state management with ease.

Conclusion

To wrap things up I have made the following observations. The more complex your app becomes the more complex your state management will become. This is the case because there are a lot more components that need to be managed and the amount will increase over time. However, there is a complementary relationship between your app complexity and your state management complexity because the more you care about state management the more complex your application will become.

I would recommend you not to take the most complex state management solution you can think of. Keep it simple and change when you really have the need for more control over your state.

Edit statemanagement-comparison

© 2023 Daniel Schulz       Datenschutz