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
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
thiswithin 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
Consumer components which control access to our user:
Here, the selling point of Context is that any components rendering
Consumer will be re-rendered whenever the
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
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
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
Consumer to access the current state and the
dispatch function and pass them to
In case you’re wonder what
withDefault does: it’s a higher-order function that takes a prop computation function — i.e.
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
Statetype. Ideally, one would be able to pass this in via a type parameter
Providerdoesn'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
optionsparameters are unavailable
mapStateToPropsis omitted, then the inner component will still be rendered when the state changes
- The object shorthand form of
mapDispatchToPropscannot 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
state provided by
useReducer, and returns a new dispatcher that:
- assumes inputs of type
Functionto be thunks, which are called with the underlying
dispatchand 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:
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.