Can You Replace Redux with React Hooks?

Chris Achard - Aug 26 '19 - - Dev Community

This was originally published on chrisachard.com

Hooks are gaining popularity as a way to add state and effects to function components - but can they go further?

Many people find Redux confusing or verbose - so maybe hooks could serve as simple replacement... so let's find out - Can you replace Redux with hooks?

TL;DR: Hooks do a lot, but only get 3/5 stars from me for replacing Redux: ⭐️⭐️⭐️
But! It really depends on how you use Redux

Why use Redux?

Before we figure out if we can replace Redux, we first have to understand the problem it solves.

1. Shared State

The #1 reason I reach for Redux in a project is to share state between components that are far apart in the component tree. Here's a picture of what I mean:

Component Tree

Here, the Navbar component holds a piece of state called username.

With a regular state/props strategy, if we wanted to get that username into the Messages component - then we'd need to go up through App, down through Body, and into Messages. That kind of prop drilling gets really cumbersome and verbose in large applications - so we need a way to share certain state across multiple components.

Redux fixes that by keeping a global, shared state, and allows us to access that state from any component by connecting to it.

2. Consolidate Business Logic

Another major aspect of Redux is that it allows you to centralize and (potentially) reuse your business logic. You can do that in a few different ways:

  • Reducers let you move your state update logic into a single place
  • Actions with the help of redux-thunk, allow for async data fetching and complex logic, before sending that data to reducers
  • Middleware allows you to inject custom functions in to the middle of the action/update cycle, and centralize your logic
  • Redux Sagas let you handle long running async actions in a smooth, centralized way

3. Enhanced Debugging

There are two powerful tools that Redux can give you that help with debugging:

Redux DevTools

As actions run through a Redux application, the changes they make to the data can be traced. That trace is available in the Redux DevTools (and Redux DevTools Extension), which lets you see all of the actions performed in your app, and how it affected the state in Redux.

Redux DevTools Extension

That let's you track everything that happens in your app - and if something isn't happening the way you think it should, you can see exactly why. Neat!

Time-Travel Debugging

When you take that a step further, then you realize that you can rewind your actions just as easily as playing them forward - and you get time travel!

Going back and forward in "time" with your actions can really help for catching sticky bugs - or for catching bugs that require a lot of setup time to capture.

What do hooks give us?

Hooks were added to React in 16.8 and in particular, there are three hooks that we might be able to combine to give us Redux functionality:

useContext

Context existed before the useContext hook did, but now we have a straightforward, easy way to access context from function components.

Context allows us to lift and share state up to a higher component in the tree - which then allows us to share it with other components.

So if we define a shared context:

const AppContext = React.createContext({});
Enter fullscreen mode Exit fullscreen mode

and provide it to our app by wrapping the entire app with it:

<AppContext.Provider value={{ username: 'superawesome' }}>
  <div className="App">
    <Navbar />
    <Messages />
  </div>
</AppContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Then we can consume that context in the child components:

const Navbar = () => {
  const { username } = useContext(AppContext)

  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

And that works! It let's us share state across our entire application (if we want) - and use that state in any of our components.

useReducer

When we get down to it, this is the component that had people excited about hooks possibly replacing Redux... after all - it has reducer right in the name! But let's check out what it actually does first.

To use useReducer, first we define a reducer function - that can look exactly like one from Redux:

const myReducer = (state, action) => {
  switch(action.type) {
    case('countUp'):
      return {
        ...state,
        count: state.count + 1
      }
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

Then in our component, we use the useReducer hook, passing in that reducer function and a default state. That returns the current state, and a dispatch function (again - just like Redux!)

const [state, dispatch] = useReducer(myReducer, { count: 0 })
Enter fullscreen mode Exit fullscreen mode

And finally, we can use that state to show the values inside, and we can use dispatch to change them:

<div className="App">
  <button onClick={() => dispatch({ type: 'countUp' })}>
    +1
  </button>
  <p>Count: {state.count}</p>
</div>
Enter fullscreen mode Exit fullscreen mode

And here's a demo of it all working:

useEffect

OK - the last thing we need then is reusable logic inside of our actions. To accomplish that, we'll take a look at useEffect, and how we can write custom hooks.

useEffect allows us to run asynchronous actions (like http requests) inside of a function component, and it lets us re-run those actions whenever certain data changes.

Let's take a look at an example:

useEffect(() => {
  // Async Action
}, [dependencies])

Enter fullscreen mode Exit fullscreen mode

This is just like a Redux action with redux-thunk installed. We can run an async action, and then do whatever we'd like with the result. For example - here we are loading from an http request, and setting that to some local state:

const [person, setPerson] = useState({})

useEffect(() => {
  fetch(`https://swapi.co/api/people/${personId}/`)
    .then(response => response.json())
    .then(data => setPerson(data))
}, [personId])
Enter fullscreen mode Exit fullscreen mode

And here's a demo of that working:

And so we've re-created actions now too!

So!

...we've made a mini Redux!... right?

By combining useContext which allows us to share state across multiple components, with useReducer which allows us to write and share reducers just like redux, and with useEffect which allows us to write asynchronous actions and then dispatch to those reducers... that sounds a lot like Redux!

But: let's take a look at how we've done when we consider what people actually use Redux for:

1. Shared State

In terms of shared state, we've done pretty well. We can use context to share a global state (that we keep in a reducer) with multiple components.

However, we should be careful to think that Context is the answer to all of our shared state problems. Here's a tweet from Dan Abromov (the creator of Redux) describing one of the possible downsides:

Dan Abromov Context

https://twitter.com/dan_abramov/status/1163051479000866816

So, while Redux is meant to keep your entire state (or most of it) in a globally accessible, single store - context is really designed to only share state that is really needed to be shared across multiple components across the component tree.

Shared State Score

Since it's possible (though perhaps shouldn't be your first choice) to share state with useContext - I'll give hooks a 4/5 stars for sharing state.

Score: ⭐️⭐️⭐️⭐️

2. Consolidate Business Logic

The main methods for consolidating business logic in Redux are in the reducers and in actions - which we can achieve with useReducer and useEffect ... hooray!

But we can't forget about Redux middleware, which some people use heavily, and other solutions like Redux Sagas, which can provide advanced asynchronous work flow options.

Business Logic Score

Since we're missing parts of Redux that some people use a lot, I have to give this a lower score: 3/5 stars.

If you are someone who really likes middleware or sagas though - then your score would be more like 1/5 stars here.

Score: ⭐️⭐️⭐️

3. Enhanced Debugging

The one thing that hooks don't give us at all is any kind of enhanced debugging like Redux DevTools or time travel debugging.

It's true, there is the useDebugValue hook, so you can get a little bit of debugging for custom hooks - but in general, Redux is far ahead here.

Debugging Score

We're missing almost everything here - so this score has to be low: 1/5 stars.

Score: ⭐️

So, can we replace Redux with Hooks?

If you use Redux only to share state across components

Then yes! ... probably. However, you may want to consider other options as well. There is the famous Dan Abramov post that You might not need Redux - so you may want to consider all of your options before you jump to trying to use useContext to replace all of Redux.

If you use middleware or sagas heavily

Then no, unless you rework how to handle your application logic. Hooks just don't have the same control options that Redux does, unless you custom build it.

If you really like Redux DevTools and time travel debugging

Then definitely not, no. Hooks don't have that ability (yet?!) so you're better off sticking with Redux.

I should mention

Redux hasn't been sitting by and just watching hooks! Check out these docs for hooks in Redux and you can join the hook party, even if you use Redux!

Also, for a more complete answer comparing Redux to other options, there's a post that explains that Redux is not dead yet

Overall score

For how I use Redux, I give hooks a 3/5 stars for replacing Redux

3/5 Stars: ⭐️⭐️⭐️

At least - I'm going to try hooks first on my next project, before just jumping into Redux. However, for complex projects, with multiple developers - I wouldn't rule out Redux just yet.

 

Like this post?
You can find more by:

Thanks for reading!

. . . . . . . . . . . . . . . . . . . . . . . .