Academind Logo

Context API + Hooks

React Hooks seem to be a real game changer. Combine that with the Context API for more awesomeness?

Created by Maximilian Schwarzmüller
#

The React Context API

This tutorial can be seen as part 2 of my Redux vs React Context tutorial. There, I dive into what the Context API is all about and how it compares to Redux - the predominant global state management solution for React apps.

In this part, we'll dive into how React Hooks can be used in conjunction with the Context API.

#

React Hooks

First things first - what are React Hooks? Good that you asked!

I updated my complete React course to reflect this important new feature that was added to React with version 16.8 and I also created a free, introductory tutorial on React Hooks that you might want to check out!

For the rest of this article, I'll assume that you know what the core idea behind hooks is.

#

Context API & React Hooks

So how do the Context API and React Hooks play together then?

Remember that you could consume your Context like this (in both functional and class-based components):

import React from 'react'
import ThemeContext from './theme-context'
const Person = props => {
return (
<ThemeContext.Consumer>
{context => <p className={context.isLight ? 'light' : 'dark'}>...</p>}
</ThemeContext.Consumer>
)
}

This way of consuming Context has a couple of drawbacks though. Besides being quite verbose, you can only access Context in your render method/ JSX code. You can't use it in componentDidMount (in class-based components) or outside of your JSX code.

This was solved with the introduction of static contextType for class-based components.

But what about functional components?

With React Hooks, you got a great way of tapping into your context: The useContext() hook:

import React, { useContext } from 'react' // highlight-line
import ThemeContext from './theme-context'
const Person = props => {
const context = useContext(ThemeContext) // highlight-line
return <p className={context.isLight ? 'light' : 'dark'}>...</p>
}

This is way more straight-forward and definitely easier to read. AND you can use context in your entire function body now, not just in your JSX code!

#

Does the Context API now win over Redux?

If you read my first part, then you learned that the Context API is a great choice for low-frequency updates (theme changes, user authentication) but not so great at high-frequency ones (keyboard input, fast-changing state).

The useContext() hook doesn't change that of course - underneath the hood, it's the same Context API and the way we tap into it doesn't have an impact on how React internally updates components and checks for Context updates.

Therefore, I DO encourage you to dive into useContext() but you should not use it for every global state you might need to manage. Redux is not dead (yet?).

#

Bonus: useReducer()

We're not done yet! We can't talk about the Context API and React Hooks without diving into useReducer().

useReducer is a Hook built-into React and it helps you with state management.

It's important to understand that useReducer() can simplify tasks you could achieve with useState() as well. So it's kind of a utility Hook, it's not adding a crucial core functionality like useState() is.

Here's a quick example:

const initialState = { cart: [] } // could also be just an array or a string etc
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_TO_CART':
return { cart: state.cart.concat(action.item) }
case 'REMOVE_FROM_CART':
return { cart: state.cart.filter(item => item.id !== action.id) }
default:
return state
}
}
const Shop = props => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<button
onClick={() =>
dispatch({ type: 'ADD_TO_CART', item: { id: 'p1', name: 'A Book' } })
}
>
Add to Cart
</button>
<button onClick={() => dispatch({ type: 'REMOVE_FROM_CART', id: 'p1' })}>
Remove from Cart
</button>
</div>
)
}

If you know Redux, this looks familiar.

useReducer() takes two arguments:

  • A reducer function (which in turn receives the state and an action as arguments)

  • An initial state

The reducer function works like a reducer function you know from Redux.

It receives the current state as an input and should return the updated state. To do that, it also receives an action that describes the desired state update.

useReducer() then returns an array with exactly two elements: The state and a dispatch function that allows you to "send a new action to the reducer".

An action then can be anything you want. Could be just a string identifying the action or an object to also hold additional data (besides the type) like the item that should be added to the cart.

The above example could've been built with useState(), too:

const reducer = (state, action) => {
switch (action.type) {
case 'ADD_TO_CART':
return { cart: state.cart.concat(action.item) }
case 'REMOVE_FROM_CART':
return { cart: state.cart.filter(item => item.id !== action.id) }
default:
return state
}
}
const Shop = props => {
const [cart, setCart] = useState({ cart: [] })
return (
<div>
<button
onClick={() =>
setCart({ cart: cart.concat({ id: 'p1', name: 'A Book' }) })
}
>
Add to Cart
</button>
<button onClick={() => setCart({cart: cart.filter(item => item.id !== 'p1')})>
Remove from Cart
</button>
</div>
)
}

This is even a bit shorter, isn't it?

Well, it is for this example. But the more cases you handle for state updating (and the more complex the logic for each case becomes - here it's just concat or filter), the more attractive useReducer() gets.

Your code is easier to read, your logic easier to reason about and you can make changes to your code in one place only (the reducer function) instead of multiple places.

So definitely consider using useReducer() for more complex state updates. Also, if you're not using the Context API, because useReducer() is not tied to that at all! It works totally independent!

Recommended Courses