Rolling your own Redux with React Hooks and Context
For managing shared state in complex JavaScript applications, Redux is undisputedly the most popular choice. At the time of writing, it has trumped the alternatives in the last three The State of JavaScript surveys, has the most GitHub stars of any JavaScript state container library, and is installed from npm over 2 million times a week.
I’ve recently been experimenting with alternative approaches to consuming and updating global state in React applications, namely with RxJS (an approach on which I’ll be giving a talk at FrontCon 2019). In order to connect my presentational components to an observable stream, I wrote a higher-order component that subscribes to said stream when it mounts, and unsubscribes when it unmounts; I used this as an opportunity to give Hooks a try, and sifting through the built-in hooks unearthed this beauty:
As I have also been looking for an opportunity to use the latest iteration of the Context API, I pondered combining it with useReducer
to replicate Redux and the official React Redux bindings. Following the former’s concepts of actions, reducers, and a single source of truth for state, as well as mostly maintaining the same API contracts, I developed a simple app:
Before I discuss my code, allow me to briefly introduce Hooks and the Context API. I should also add that one should have prior experience with Redux before tackling this post, so if the nomenclature I mentioned in the previous paragraph (actions and reducers) resulted in head-scratching, pop on over to the official documentation first.
What are React Hooks?
Hooks are a React feature which allow one to add stateful or side effect-orientated logic to functional — otherwise stateless — components. Rather than get wordy on y’all, allow me to illustrate this with an example, using the built-in useState
hook:
Compare this with the instance-backed, class-driven approach:
To me, the Hook-based implementation is preferable for two key reasons:
- Reduced verbosity: everything is happening within a single function, rather than being described across a constructor and an instance method
- Independent of function invocation context: although static analysis (did you notice my use of TypeScript in the first two gists? 😉) can pinpoint potential issues in this area, Hooks eliminate the need to consider the value of
this
within functions that are responsible for mutating local state
Two additional advantages provided by the React team are:
- allowing the reuse of stateful logic, such as subscriptions to data sources, across components
- avoiding the cognitive juggling of combining lifecycle methods, favouring the composition of complex, stateful logic by combining multiple Hooks
How About Context?
From the React team themselves:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
How so? Let’s say we want to pass the signed-in user’s details down multiple levels of our app’s tree. We can use createContext
to produce Provider
and Consumer
components which control access to our user:
Here, the selling point of Context is that any components rendering UserContext
's Consumer
will be re-rendered whenever the Provider
’s value
prop is updated; this, in turn, will be triggered by our root App
component re-rendering, perhaps because the user
prop has changed.
We can absolutely achieve this with props, but the need to reintegrate them across the entire depth of the tree, and the resultant verbosity, are unwieldy:
While the React docs openly warn that the use of Context can impede component reuse, and should be used sparingly, it’s nonetheless a reasonable approach for sharing common data at multiple levels of one’s tree; this is precisely why the official React Redux bindings make use of it.
Note: Interestingly, React provides a useContext
hook which serves the same role as the Consumer
component. For my proof-of-concept, however, I leaned towards the latter.
Back to the App
Let’s tie our newfound, or at least reinforced, knowledge in with the app I wrote.
I will embed gists to highlight the most important areas of the codebase, but you can also peruse and clone the entire repository on GitHub.
As a recap, here it is in “action” (pun somewhat intended):
Here’s the component tree that drives it:
Does this look familiar? Wait until I show you how the MessageForm
component is connected to our shared state:
Our app’s sole reducer conforms to the function signature expected by useReducer
, as well as Redux’s createStore
function:
Bonus aside: the functions which determine if an action is of a certain type, such as isAddMessage
, serve as type guards, so we can determine the type of the payload
property at compile time and inherently rely upon this safety at run time.
Our action creators should also be recognisable to you:
The Ties That Bind
We have an API surface that closely mirrors the React Redux bindings. How does this work under the hood? Foremost, we need to create a single Context:
We can consequently render StateContext.Provider
within our own Provider
component; this will utilise the useReducer
hook, taking in our app’s reducer and default state, and returning the current state and a dispatch function, which we can forward to mapDispatchToProps
. Whenever one of these properties invokes dispatch
, the hook will pass the action into our reducer, compute our new state, and enqueue a re-render of our own Provider
:
Our connect
higher-order component can now be written as so:
Like the React Redux namesake, connect
allows one to map both shared state and calls to dispatch
to props, which are passed to the inner component. We use the StateContext
's Consumer
to access the current state and the dispatch
function and pass them to mapStateToProps
and mapDispatchToProps
respectively.
In case you’re wonder what withDefault
does: it’s a higher-order function that takes a prop computation function — i.e. mapStateToProps
or mapDispatchToProps
- and defaults to an empty object if said computation function is not provided.
I should note that I’ve taken a few liberties:
- The bindings are built around our own
State
type. Ideally, one would be able to pass this in via a type parameter Provider
doesn't accept a store prop, instead taking a reducer function directly. Given the hook maintains the state, I saw no need to implement a store abstractionconnect
'smergeProps
andoptions
parameters are unavailable- If
mapStateToProps
is omitted, then the inner component will still be rendered when the state changes - The object shorthand form of
mapDispatchToProps
cannot be used in place of a function
Thunky Monks
In revisiting the example app’s functionality (don’t worry, I won’t drop that GIF for the third time), you may have noticed the Add Ron Swanson Quote button. Who doesn’t appreciate a bit of wisdom from everybody’s favourite patriotic, free market-adoring libertarian?
As this requires a HTTP request to a JSON API, the asynchronous,Promise
-orientated Fetch API is an optimal fit for grabbing this data. From my experience, the most popular approach to describing asynchronous action creators is with Redux Thunk; this allows multiple dispatches within one action creator by supporting the return of a function in lieu of a plain action:
While Redux supports middleware, enabling the likes of Thunk, Redux Saga and Redux Observable, I ultimately concluded that it would be overkill for my POC, and leant towards a higher-order function that takes the dispatch
and state
provided by useReducer
, and returns a new dispatcher that:
- assumes inputs of type
Function
to be thunks, which are called with the underlyingdispatch
and current state - treats other inputs as regular actions, and thus dispatches them directly
This augmented dispatch is passed to the provider, always receiving the latest state whenever the root tree is subsequently updated:
Consumers of connect
who specify a mapDispatchToProps
parameter will now receive this dispatch with superpowers.
Next Steps
I did want to implement combineReducers
, but I had no need for this function given the size of my app. If you’d like to know how this works, then feel free to take a peek at the source.
Steady on! Aren’t Hooks Experimental?!
Nope! Hooks are an approved, production-ready feature as of React 16.8! 🍾 Even React’s official test renderer supports them, so they should land in Enzyme imminently if they aren’t already enabled.
Thanks for reading! I hope you found this post useful. Please add a response if you have any questions or feedback; otherwise, I’d really appreciate some claps!
Written by James Wright — Software developer at YLD.