DEV Community

Cover image for React State 5 Ways
Nader Dabit
Nader Dabit

Posted on

React State 5 Ways

Cover image by Bryan Goff

To see the code for these examples, click here

There are seemingly endless ways of dealing with state management in React. Trying to understand the options, the tradeoffs between them, and how they work can be overwhelming.

When I'm trying to learn something, seeing a side by side comparison implementing some common real-world functionality helps me understand the differences between various options as well as form a mental model around how I may use them in my own applications.

In this post I'm going to walk through how to implement global state management in a React application using the same pattern across 5 of the most popular libraries and APIs using the most modern and up-to-date versions of the libraries.

  1. Recoil
  2. MobX
  3. XState
  4. Redux (with hooks)
  5. Context

I'll also try to explain the differences between them as well as my thoughts about and a general overview of each approach.

To demonstrate the APIs we'll be implementing a notes app using each library / approach that shows how to do create and list an array of notes.

Getting started

If you'd like to follow along, create a new React app that we'll be using for testing these approaches:

npx create-react-app react-state-examples

cd react-state-examples
Enter fullscreen mode Exit fullscreen mode

To run the app at any time, run the start command:

npm start
Enter fullscreen mode Exit fullscreen mode

Recoil

Recoil Docs

Lines of code: 30

One of the things I really liked about Recoil was the hooks-based API and how intuitive it was go get going with.

When compared to some of the other options, I would say that the setup and API with recoil is easier than most.

Recoil in action

To get started with Recoil, install the library as a dependency:

npm install recoil
Enter fullscreen mode Exit fullscreen mode

Next, add the RecoilRoot to the root / entry-point of the app:

import App from './App'
import { RecoilRoot } from 'recoil'

export default function Main() {
  return (
    <RecoilRoot>
      <App />
    </RecoilRoot>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, to create some state we will use an atom from Recoil and set a key as well as some initial state:

import { atom } from 'recoil'

const notesState = atom({
  key: 'notesState', // unique ID (with respect to other atoms/selectors)
  default: [], // default value (aka initial state)
});
Enter fullscreen mode Exit fullscreen mode

Now you can use useRecoilState from Recoil to access this value anywhere in your app. Here is the notes app implemented using Recoil:

import React, { useState } from 'react';
import { RecoilRoot, atom, useRecoilState } from 'recoil';

const notesState = atom({
  key: 'notesState', // unique ID (with respect to other atoms/selectors)
  default: [], // default value (aka initial state)
});

export default function Main() {
  return (
    <RecoilRoot>
      <App />
    </RecoilRoot>
  );
}

function App() {
  const [notes, setNotes] = useRecoilState(notesState);
  const [input, setInput] = useState('')
  function createNote() {
    const notesArray = [...notes, input]
    setNotes(notesArray)
    setInput('')
  }
  return (
    <div>
      <h1>My notes app</h1>
      <button onClick={createNote}>Create Note</button>
      <input value={input} onChange={e => setInput(e.target.value)} />
      { notes.map(note => <p key={note}>Note: {note}</p>) }
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Recoil selectors

From the docs:

Selectors are used to calculate derived data that is based on state. This lets us avoid redundant state, usually obviating the need for reducers to keep state in sync and valid. Instead, a minimal set of state is stored in atoms, while

Using Recoil selectors you can compute derived data based on your state, for instance maybe a filtered array of todos that are completed (in a todo app), or an array of orders that have been shipped (in an e-commerce app):

import { selector, useRecoilValue } from 'recoil'

const completedTodosState = selector({
  key: 'todosState',
  get: ({get}) => {
    const todos = get(todosState)
    return todos.filter(todo => todo.completed)
  }
})

const completedTodos = useRecoilValue(completedTodosState)
Enter fullscreen mode Exit fullscreen mode

Verdict

The recoil docs say that "Recoil is an experimental set of utilities for state management with React.". When I hear the word "experimental" it doesn't make me extremely comfortable when I'm making the decision to use a library in production, so I'm unsure how I feel about doing so now with Recoil, at least at the moment.

Recoil is awesome and I'd jump on it for my next app but am worried about the experimental label so I will be keeping an eye on it but not using it for anything in production right now.

MobX

MobX React Lite Docs

Lines of code: 30

MobX React has always been one of my favorite ways to manage React state, mainly because it was the next thing I tried after using Redux. The stark difference for me between the two cemented it for me as my go-to option over the years.

MobX React now has a light version (MobX React Lite) that is made especially for functional components and is slightly faster and smaller.

MobX has the idea of observables and observers, but the observable API has changed a bit and you do not have to specify each item that you'd like to be observable, instead you can use makeAutoObservable which will handle everything for you.

If you want your data to be reactive and subscribed to changes in the store, then you wrap the component using it in an observer.

MobX in action

To get started with MobX, install the library as a dependency:

npm install mobx mobx-react-lite
Enter fullscreen mode Exit fullscreen mode

The state for the app is created and managed in Stores.

The store for our app looks like this:

import { makeAutoObservable } from 'mobx'

class NoteStore {
  notes = []
  createNote(note) {
    this.notes = [...this.notes, note]
  }
  constructor() {
    /* makes all data in store observable, replaces @observable */
    makeAutoObservable(this)
  }
}

const Notes = new NoteStore()
Enter fullscreen mode Exit fullscreen mode

We can then import the Notes and use them anywhere in our app. To make a component observe changes, you wrap it in an observer:

import { observer } from 'mobx-react-lite'
import { notes } from './NoteStore'

const App = observer(() => <h1>{notes[0]|| "No notes"}</h1>)
Enter fullscreen mode Exit fullscreen mode

Let's see how all of it works together:

import React, { useState } from 'react'
import { observer } from "mobx-react-lite"
import { makeAutoObservable } from 'mobx'

class NoteStore {
  notes = []
  createNote(note) {
    this.notes = [...this.notes, note]
  }
  constructor() {
    makeAutoObservable(this)
  }
}

const Notes = new NoteStore()

const App = observer(() => {
  const [input, setInput] = useState('')
  const { notes } = Notes
  function onCreateNote() {
    Notes.createNote(input)
    setInput('')
  }
  return (
    <div>
      <h1>My notes app</h1>
      <button onClick={onCreateNote}>Create Note</button>
      <input value={input} onChange={e => setInput(e.target.value)} />
      { notes.map(note => <p key={note}>Note: {note}</p>) }
    </div>
  )
})

export default App
Enter fullscreen mode Exit fullscreen mode

Verdict

MobX has been around for a while and is tried and true. I've used it in massive production applications at enterprise companies as have many others.

After using it again recently I feel like the documentation was slightly lacking compared to some of the other options. I'd try it out for yourself to see what you think before making a bet on it.

XState

XState Docs

Lines of code: 44

XState is trying to solve the problem of modern UI complexity and relies on the idea – and an opinionated implementation of – finite state machines.

XState was created by David Khourshid, who I have seen talking alot about it since it was released so I have been eager to give it a shot for a while. This is the only library here that I was unfamiliar with before writing this post.

After trying it out, I can say for sure that it is a much different approach than any of the others. The complexity here is more than any of the others, but the mental model of how state works is really cool and empowering, and made me feel smart after getting it working and building a few example apps with it 🧠.

To learn more about the problems that XState is trying to solve, check out this video from David Khourshid or this post which I also found interesting.

XState does not translate especially well here as it really shines with more complex state, but this light introduction will at least hopefully give you an introduction to help you wrap your mind around how it all works.

XState in action

To get started with XState, install the libraries:

npm install xstate @xstate/react
Enter fullscreen mode Exit fullscreen mode

To create a state machine you use the Machine utility from xstate. Here is the machine we will be using for the Notes app:

import { Machine } from 'xstate'

const notesMachine = Machine({
  id: 'notes',
  initial: 'ready',
  context: {
    notes: [],
    note: ''
  },
  states: {
    ready: {},
  },
  on: {
    "CHANGE": {
      actions: [
        assign({
          note: (_, event) => event.value
        })
      ]
    },
    "CREATE_NOTE": {
      actions: [
        assign({
          note: "",
          notes: context => [...context.notes, context.note]
        })
      ]
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

The data we will be working with is stored in the context object. Here, we have the array of notes as well as a note that will be controlled by a text input. There are two actions, one for creating a note (CREATE_NOTE) and one for setting the text input (CHANGE).

Putting it all together:

import React from 'react'
import { useService } from '@xstate/react'
import { Machine, assign, interpret } from 'xstate'

const notesMachine = Machine({
  id: 'notes',
  initial: 'ready',
  context: {
    notes: [],
    note: ''
  },
  states: {
    ready: {},
  },
  on: {
    "CHANGE": {
      actions: [
        assign({
          note: (_, event) => event.value
        })
      ]
    },
    "CREATE_NOTE": {
      actions: [
        assign({
          note: "",
          notes: context => [...context.notes, context.note]
        })
      ]
    }
  }
})

const service = interpret(notesMachine).start()

export default function App() {
  const [state, send] = useService(service)
  const { context: { note, notes} } = state

  return (
    <div>
      <h1>My notes app</h1>
      <button onClick={() => send({ type: 'CREATE_NOTE' })}>Create Note</button>
      <input value={note} onChange={e => send({ type: 'CHANGE', value: e.target.value})} />
      { notes.map(note => <p key={note}>Note: {note}</p>) }
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

To subscribe to state changes across the app, we use the useService hook from xstate-react.

Verdict

XState is like the Rolls Royce or Swiss Army Knife of state management. There is a ton you can do, but all of the power comes with additional complexity.

I look forward to learning and understanding it better in the future so I can apply it to problems and reference architectures here at AWS, but for small projects I think it may be overkill.

Redux

React Redux docs

Lines of code: 33

Redux is one of the earliest and most successful state management libraries in the entire React ecosystem. I've used Redux in countless projects and it still is going strong today.

The new Redux hooks API makes redux boilerplate somewhat less of an issue and a lot easier to work with.

Redux Toolkit has also improved the DX as well as lowered the learning curve a lot from what it was in the past.

Redux in action

To get started with Redux, install the necessary libraries:

npm install @reduxjs-toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

To work with Redux, you need to create and configure the following:

  1. A store
  2. Reducers
  3. A provider

To help explain how all of this works, I've made comments in the code that implements the Notes app in redux:

import React, { useState } from 'react'
import { Provider, useDispatch, useSelector } from 'react-redux'
import { configureStore, createReducer, combineReducers } from '@reduxjs/toolkit'

function App() {  
  const [input, setInput] = useState('')

  /* useSelector allows you to retrieve the state that you'd like to work with, in our case the notes array */
  const notes = useSelector(state => state.notes)

  /* dispatch allows us to send updates to the store */
  const dispatch = useDispatch()

  function onCreateNote() {
    dispatch({ type: 'CREATE_NOTE', note: input })
    setInput('')
  }
  return (
    <div>
      <h1>My notes app</h1>
      <button onClick={onCreateNote}>Create Note</button>
      <input value={input} onChange={e => setInput(e.target.value)} />
      { notes.map(note => <p key={note}>Note: {note}</p>) }
    </div>
  );
}

/* Here we create a reducer that will update the notes array when the `CREATE_NOTE` action is dispatched */
const notesReducer = createReducer([], {
  'CREATE_NOTE': (state, action) => [...state, action.note]
})

/* Here we create the store using the reducers in the app */
const reducers = combineReducers({ notes: notesReducer })
const store = configureStore({ reducer: reducers })

function Main() {
  return (
    /* Here we configure the Provider with the store */
    <Provider store={store}>
      <App />
    </Provider>
  )
}

export default Main
Enter fullscreen mode Exit fullscreen mode

Verdict

Redux is a really solid choice if you're looking something with a massive community and a large amount of documentation and answers. Because it has been around for so long, you can pretty much Google any question and at least get a somewhat relevant answer.

When working with async operations like data fetching you typically need to add additional middleware which adds additional boilerplate and complexity.

To me, Redux was hard to learn at first. Once I became familiar with the framework it was really easy to work with and understand. In the past it was sometimes overwhelming for new developers, but with the recent improvements made with Redux hooks and Redux Toolkit, the learning curve is much easier and I still highly recommend Redux as a first-class option.

Context

Context docs

Lines of code: 31

The great thing about context is that there's no libraries to install and keep up to date, it's just part of React. There are a ton of examples of how to use it, and it's documented right there along with the rest of the React documentation.

Working with context is pretty straightforward, the problem often arises in a larger or more complex application when you're trying to manage a large number of different context values so you will often have to build your own abstractions to manage these situations yourself.

Context in action

To create and use context, import the hooks directly from React. Here is how it works:

/* 1. Import the context hooks */
import React, { useState, createContext, useContext } from 'react';

/* 2. Create a piece of context */
const NotesContext = createContext();

/* 3. Set the context using a provider */
<NotesContext.Provider value={{ notes: ['note1', 'note2'] }}>
  <App />
</NotesContext.Provider>

/* 4. Use the context */
const { notes } = useContext(NotesContext);
Enter fullscreen mode Exit fullscreen mode

Putting it all together:

import React, { useState, createContext, useContext } from 'react';

const NotesContext = createContext();

export default function Main() {
  const [notes, setNotes] = useState([])
  function createNote(note) {
    const notesArray = [...notes, note]
    setNotes(notesArray)
  }
  return (
    <NotesContext.Provider value={{ notes, createNote }}>
      <App />
    </NotesContext.Provider>
  );
}

function App() {
  const { notes, createNote } = useContext(NotesContext);
  const [input, setInput] = useState('')
  function onCreateNote() {
    createNote(input)
    setInput('')
  }

  return (
    <div>
      <h1>My notes app</h1>
      <button onClick={onCreateNote}>Create Note</button>
      <input value={input} onChange={e => setInput(e.target.value)} />
      { notes.map(note => <p key={note}>Note: {note}</p>) }
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Verdict

Context is a really solid and straightforward way to manage state in your app. The API may not be as nice as some of the other options, but if you understand how to use it and can create the right abstraction with it in your app, you can't really go wrong with choosing context to manage global state in your app.

Top comments (14)

Collapse
 
markerikson profile image
Mark Erikson

Note that we now specifically teach using our official Redux Toolkit package as the default syntax for writing Redux logic.

We have a new "Redux Essentials" core docs tutorial that builds a real-world app while teaching RTK as the standard way to use Redux, and I'm currently working on a new "Redux Fundamentals" docs tutorial that will teach the lower-level principles involved in using Redux, then finish by showing how and why to use RTK instead of writing Redux code by hand.

In this particular example, using RTK would keep the number of lines of code basically the same. Instead of this:

function notesReducer(state = [], action) {
  switch (action.type) {
    case 'CREATE_NOTE':
      return [...state, action.note]
    default:
      return state
  }
}

/* Here we create the store using the reducers in the app */
const reducers = combineReducers({ notes: notesReducer })
const store = createStore(reducers)
Enter fullscreen mode Exit fullscreen mode

You'd have:

const notesReducer = createReducer([], {
  createNote: (state, action) =>state.concat(action.payload)
})

const store = configureStore({reducer: {notes: notesReducer}})
Enter fullscreen mode Exit fullscreen mode

But, the difference in LOC between hand-written Redux and RTK becomes dramatically different as the app grows in size - RTK requires way fewer lines of code.

The updated tutorials should also make it a lot easier for people to learn Redux as well.

Collapse
 
dabit3 profile image
Nader Dabit

Hey Mark, thanks for the helpful update. I'll probably adjust my tutorial to use the most up to date implementation using Redux Toolkit. Nice work!!

Collapse
 
markerikson profile image
Mark Erikson • Edited

Thanks for the quick article update! One bugfix: the package installation example is wrong - the package name is @reduxjs/toolkit:

redux.js.org/introduction/installa...

Thread Thread
 
dabit3 profile image
Nader Dabit

Saved me, thank you!!

Collapse
 
spierala profile image
Florian Spier • Edited

What do you think of RxJS based state management in React world? E.g. Akita or MiniRx Store?
I am working on MiniRx. It is like Redux but implemented with RxJS. State is exposed as an RxJS Observable. Observables integrate nicely with Angular. But not sure about React and Observables.

Collapse
 
eecolor profile image
EECOLOR

Thank you for writing the article!

I noticed two common mistakes with the context example, see my reaction to another article here: medium.com/@ewestra/great-article-...

Collapse
 
rrackiewicz profile image
rrackiewicz • Edited

Could you respond to the claim that Recoil is a context replacement, not necessarily a Redux replacement. I haven't used Recoil but have watched their videos. I am a Redux user.

Also, wouldn't it be fair to add GraphQL cache to this list as well?

Thanks.

Collapse
 
vasco3 profile image
JC

Recoil is great but it's annoying that it doesn't work well with hot-reloading in development (have to manually refresh)

Collapse
 
aquibyatoo profile image
mohammad aQib

Awesome, such a nice to see you started with recoil :thumb

Collapse
 
drubb profile image
drubb

Great collection! Another nice solution would be Overmind by Codesandbox: overmindjs.org

Collapse
 
efleurine profile image
Emmanuel

Every time I see Redux those days. I am crying.

swr.vercel.app/
This library is not a state management per see. But I found sometimes I can get a long way with it.

Collapse
 
dbtek profile image
İsmail Demirbilek • Edited

I'd suggest looking in to unstated-next. It's based on context and React's state, handles async. It's dead simple and lightweight.

Collapse
 
juanmamenendez15 profile image
Juan Manuel Menendez Hernandez

You miss the great Apollo Client, which is better than the others you mentioned (in general)