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:
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.
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({});
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>
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>
)
}
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
}
}
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 })
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>
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])
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])
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:
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:
- Following me on twitter: @chrisachard
- Joining the newsletter: chrisachard.com
Thanks for reading!