A simple guide to proper state management in React

A common problem developers are faced when working on a React application is figuring out the best way to manage state between all their components. I am not saying that it is difficult to manage state in React, I mean to say that it isn’t always easy to figure out which way is the best way to manage state in your application. I will go over three common ways to manage state in your app, and help guide your design decisions around each one.

1) React Component Props

This is the most basic way to manage state for your components, you simply pass the state via props. Of course things can get pretty complicated as you add more and more components that rely on the same shared state. For most use cases, this will probably be the best solution – it’s clean, simple, and keeps your components reusable. If your component is a “dumb” UI component, then it ideally should take in state as props and render it.

function ButtonWithText(buttonText) {
	return {buttonText}
}
// Usage

Probably the most common issue I have seen with managing state this way is prop drilling (Kent C. Dobbs has a great post on prop drilling). In my experience, prop drilling is not an issue that surfaces immediately, but happens over time as components are gradually refactored and split into multiple components. For example, imagine a not-too-uncommon scenario where you have a single component, but then later on you realize that part of the component would be useful elsewhere in your application, so then you do the reasonable thing and split out the reusable stuff into a separate component. One or two of these refactors and we end up with textbook prop drilling. This leads to cluttered components that are aware of state that they don’t actually care about, causing hits to both the maintainability and readability of your code.

(The following code example is from Kent C. Dobbs aforementioned article, it was more concise than anything I could come up with)

function Toggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(o => !o)
  return <Switch on={on} onToggle={toggle} />
}

function Switch({on, onToggle}) {
  return (
    <div>
      <SwitchMessage on={on} />
      <SwitchButton onToggle={onToggle} />
    </div>
  )
}

function SwitchMessage({on}) {
  return <div>The button is {on ? 'on' : 'off'}</div>
}

function SwitchButton({onToggle}) {
  return <button onClick={onToggle}>Toggle</button>
}

In these code examples, we are using function components and React Hooks. If you have not yet tried out React Hooks, I would highly recommend it (it’s surprisingly easy to convert class components to instead use Hooks).

Prop drilling only really becomes an issue once your app grows and your component hierarchy becomes cumbersome. In some cases you can prevent deep prop-drilling by re-combining components that didn’t really need to be split in the first place. In other cases, you might want to consider trying one of the next two options.

2) React Context

React Context was added to React to help solve the problem of sharing state between multiple components, especially between ones that are not close in the component hierarchy. React Context is a great option because it is very straight forward to use and has native support, as it is part of React itself.

function ParentComponent() {
    [text, setText] = useState("");
    // Create a Context
    const TextContext = React.createContext(null);
    return (
        <TextContext.Provider
            value = {{
                displayText: text,
                updateText: setText,
            }}
        >
          <ChildComponent />  
          <AnotherChildComponent />
        </TextContext.Provider>
	)
}

function ChildComponent() {
    // With React Hooks, 'useContext' will cause your 
    // component to re-render everytime the context value 
    // changes
    const textContext = useContext(TextContext);
    return (
        <FancyButton onClick={() => textContext.updateText("clicked!")} />
    );
}

function AnotherChildComponent() {
    const textContext = useContext(TextContext);
    return (
    	<p>{textContext.text}</p>
    );
}

In a more fleshed out real world example, you could really see how this could simplify things by allowing you to just cut out all the extraneous prop passing. However, the major downside to using React Context is that it reduces the reusability of the components that use it. A simple component takes state in via props, this allows it to be reused in any location that is able to provide it the state it needs to properly render – including an entirely different application. When your component now depends React Contexts, your component may still be reusable in parts of your app (depending on the use case), it will be much less likely to be reusable outside of your specific use case.

3) Redux state management

Redux is a separate library that allows you to maintain a centralized store for your app’s shared state while also providing a unidirectional data flow for the state managed by your Redux store. There other libraries that achieve similar results (MobX, Relay + GraphQL, Jumpsuit), but Redux is the most popular one.

(If you haven’t yet worked with Redux before, I would highly recommend going through Redux’s Getting Started guide, I found it to be a good starting point when I was first learning Redux.)

Rather than diving into the more technical details of how to implement Redux into your application, I want to outline the various pros and cons of it, so you can have a better idea if it is the right tool for your app.

The Cons:

  • Decent amount of boilerplate code — when switching to Redux, you will need to create quite a few more classes to achieve arguably the same functionality that you had before. In the long run it will be worth it, but I found it frustrating at first.
  • Non-trivial learning curve — Redux has a steep enough learning curve where it is not uncommon for some developers to make mistakes that can counteract the benefits of using the library in the first place. Mistakes like using Redux unnecessarily (when props could suffice) is not uncommon, especially with more junior developers.

The Pros:

  • Structured solution to a complex problem — before utilizing Redux (and React), our application tried to maintain a form of consistent shared state in a truly complex mess that became unmanageable due to mutations to that state being difficult to properly track and sync across all components. Redux greatly simplified this for us with its well-defined and structured approach.
  • Easy to test and debug — with Redux, state in your application becomes predictable because of changes to the store are limited to the reducer functions. There is great tooling for Redux that allow you to go back in time and see how your app looked with “old” state (redux-devtools).

Redux is a fantastic tool for helping manage state in larger React applications, however, it can be a bit heavy-handed for simpler use cases.

Closing Thoughts

My general process is that I default to using simple props for my state management until I find a compelling reason not to. Of course there are cases where the component I am building will clearly need to use state that is already managed in either an existing Context or in the Redux store. I would encourage you to spend some time thinking about how state will be managed for the next component you create, even before you start writing any code!

If you want updates from me on my future blog posts or on my future projects, please sign up for my email list below!

Processing…
Success! You're on the list.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: