Rolling your own Redux with React Hooks and Context
by James Wright • January 7th, 2019 • 5min
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 abstraction
- connect's mergeProps and options 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
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 underlying dispatch 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.
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!
Interested in React? Read more about it:
Written by James Wright • January 1st, 2019
- Redux Thunk
Share this article