Using React Hooks & Context to Avoid Adding Redux Too Early

Article summary

Many people think of React and Redux as inseparable, despite the Redux author’s own advice to avoid using it “until you have problems with vanilla React.”

The bar for “problems with vanilla React” has traditionally been pretty low, though. As soon as developers find themselves passing props to a component that doesn’t need them just so they can pass those props further down to a component that does, they will be tempted to reach for Redux. This situation often occurs in the first week of a project.

Redux is kind of complicated, though. There’s a philosophy to learn, a lot of concepts, and a lot of pieces. Actions, reducers, container components, presentation components, state props, dispatch props, thunks, etc. You might have to touch a bunch of files to make changes that seem like they should be relatively minor. All of this overhead may be worth it in some situations, but for a small project, it can feel like a lot.

Since version 16.8, vanilla React includes two new features—hooks and context. They can be used together to alleviate some of the early pain, and they should raise the threshold for deciding that Redux is the best choice significantly. Using these two features gives you a way to remove some of the noise and indirection that results from nesting components with vanilla React, without introducing Redux and its own set of pain.

Context

The context API addresses a common criticism of React, which is that properties have to be passed down explicitly through every level of a component tree to whatever component needs them, even if the ones in between don’t need them at all. This makes it possible for a component deep in the tree to subscribe to updates from a “context” that’s defined elsewhere.

By itself, the context API is perhaps a little messier than ideal. Take a look at this example, and notice that creating a context involves two higher-order components, a provider and a consumer. The provider must be wrapped around a parent node of all of the children that will subscribe to it, and the consumer needs to be wrapped around any component that wants to access it. Combining the context API with the corresponding hook makes it possible to eliminate this.

Hooks

React Hooks is the name for the set of functions the React team has provided to “hook” into underlying React functionality. The three basic hooks are useState, useContext, and useEffect.

useState

The useState hook addresses another common criticism of React, which is that you can’t use state with functional components. The context API makes it possible to have functional components with state. Previously, if you wanted your components to have state, you had to use class components.

Converting a functional component to a class component involves quite a bit of boilerplate, and it always felt a little unnecessary to me. The following example contrasts a component written in the class style with the same component written as a function using the useState hook:

As a class:


class Counter extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    };

    this.incrementCounter = this.incrementCounter.bind(this);
    this.decrementCounter = this.decrementCounter.bind(this);
  }

  incrementCounter() {
    this.setState({
      counter: this.state.counter + 1
    });
  }

  decrementCounter() {
    this.setState({
      counter: this.state.counter - 1
    });
  }

  render() {
    return (
      <div>
        <div>{ this.state.counter }</div>
        <button onClick={ this.incrementCounter }>+</button>
        <button onClick={ this.decrementCounter }>-</button>
      </div>
    )
  }
}

As a functional component with useState:


const Counter = () => {
  const [counter, setCounter] = useState(0);

  const incrementCounter = () => setCounter(counter + 1)
  const decrementCounter = () => setCounter(counter - 1)

  return (
    <div>
      <div>{ counter }</div>
      <button onClick={ incrementCounter }>+</button>
      <button onClick={ decrementCounter }>-</button>
    </div>
  )
}

useContext

The example above doesn’t need to use context because the value of the counter is local to the component, but what if we wanted the counter to be global to the application? The answer before context and hooks would have been to put the counter and its manipulation functions in the top level, and then pass it down through props to the components that want to manipulate or display it. With the useContext hook, we can just create a context and then subscribe to it by calling a function, instead of wrapping everything with that Consumer HOC.

counter-context.js:


const CounterContext = React.createContext({
  counter: 0
});

const counterContextProvider = (props) => {
  const [counter, setCounter] = React.useState(0);

  return (
    <CounterContext.Provider value={{
      counter,
      setCounter
    }}>
      { props.children }
    </CounterContext.Provider>
  )
}

export { counterContextProvider }

index.js:



import { CounterContextProvider } from './counter-context';

ReactDOM.render(
  <CounterContextProvider>
    <App/>
  </CounterContextProvider>
)

counter.js:


import { CounterContext } from './counter-context'

const Counter = () => {
  const counterContext = React.useContext(CounterContext);

  const incrementCounter = () => counterContext.setCounter(counterContext.counter + 1)
  const decrementCounter = () => counterContext.setCounter(counterContext.counter - 1)

  return (
    <div>
      <div>{ counterContext.counter }</div>
      <button onClick={ incrementCounter }>+</button>
      <button onClick={ decrementCounter }>-</button>
    </div>
  );
}

Calling the useContext function subscribes the component to context updates, so now, the component itself is declaring its intention to use a context, rather than hiding it in a wrapper HOC in a parent component.

useEffect

The third basic hook, useEffect, allows you to execute code for side effects. Previously, the only way to do this was to create a class component and put your code into one of its lifecycle methods like componentDidMount or componentDidUpdate. A common pattern to fetch data without Redux involves making the API call for some data in a lifecycle method. This sets the state on the component, causing the component to re-render with the data. With Redux, you might use a library such as redux-thunk to create actions that fetch from remote APIs.

With the useEffect hook, you don’t have to convert your functional components to class components, and you don’t have to put your code in a lifecycle method. In the example below, we use the useEffect hook to fetch the initial state of the counter from some remote source and then set the value.


const Counter = () => {
  const [counter, setCounter] = useState(0);
  const incrementCounter = () => setCounter(counter + 1)
  const decrementCounter = () => setCounter(counter - 1)

  useEffect(() => {
    fetchCounter().then((value) => {
      setCounter(value)
    })
  })

  return (
    <div>
      <div>{ counter }</div>
      <button onClick={ incrementCounter }>+</button>
      <button onClick={ decrementCounter }>-</button>
    </div>
  )
}

I hope these examples convince you to look further into hooks and context before jumping straight into Redux on your next project. A good place to start learning more is in the official documentation for hooks and context.