Advanced React Patterns
Context Module Functions
Context module functions are basically helper functions you build to provide a more clean API for users who consume your custom contexts.
For example, suppose you have a CounterProvider
where a common use case is to increment
and decrement
the count
inside of that context.
You could write context module functions like this...
However, there are few limitations with the above approach:
You can't place
increment
anddecrement
into dependency lists foruseEffect
oruseCallback
unless you memoize them by wrapping them inuseCallback
. Otherwise, the function reference will change on every re-render, leading to unintended side effects.Because the context module functions live inside of
CounterProvider
(or in auseCounter
hook you could create), you can't tree shake, code split, or lazily load them.
To solve this problem, your context module functions can accept dispatch
as as an argument, so it can live outside of your React components.
In this pattern, you continue to give the user direct access to dispatch
in context. However, to make their lives easier, you provide them with common operations in the form of context module functions.
These functions accept dispatch
as an argument, so they can interact with context without being directly involved with it (like defined inside of a provider or custom hook).
In doing so, these helper functions don't have to worry about memoization, and they can be easily tree shaken, code split, and lazily loaded as well!
Compound Components
Compound components are components that work together to form a complete UI. In HTML, think of the combination of select
and option
:
select
and option
can't be used separately, but together they form a dropdown input.
More specifically, select
handles the management of state of the UI, while option
handles configuration for how the select should operate (its options and their values).
A naive React implementation
In React world, if we wanted to create a select
and option
, it's common to create a CustomSelect
with an options
prop:
The trouble comes when we want to extend our CustomSelect
. Maybe we need to add additional attributes to the option
s rendered. Or maybe we need the display
to change style based on whether it's selected.
We could add to the API surface area by introducing more props, but that just means more to code and more for users to learn! (This could blow up and get very messy in a real-world application.)
Compound components as a solution
Suppose we want to create a Toggle
that contains a toggle button and that shows different content when the toggle is on vs. when it's off.
In a compound components approach, it would look something like this:
Toggle
should manage the state (just like select
does). But how do we work with that state inside of ToggleOn
, ToggleOff
, and ToggleButton
? From the perspective of the user, we don't see any of the state sharing.
Answer: You can implicitly pass props to the children of Toggle
using a combination of React.Children.map
and React.cloneElement
.
There's a few things going on in the code above:
Toggle
manages theon
state and creates a helper functiontoggle
for switching that state.We map over
children
usingReact.Children.map
. (This is required when mapping over React components. A simpleArray.map
wouldn't do.)If the child is a DOM component like
<div />
or<span />
, we just return it.Otherwise, we clone the child with
React.cloneElement
and pass along the internal state ofToggle
to the child in the form of props.Finally, the child components just access the props
on
andtoggle
that we passed like normal.
Pro tip: One concern with this approach is that a user could create a CustomToggleButton
that accepts on
and toggle
props, and Toggle
will pass those props along. If you want to stop this, you can just create a allowedTypes
array to filter out components that you don't want to share internal state with.
Flexible Compound Components
What happens if you want to wrap your compound components inside your own custom parent components (like for style reasons)? In other words, how do you share state with compound components that are grandchildren?
Answer: Use context!
Now you can nest your Toggle
child components as deep as you want, and they should still work.
Prop Collections and Getters
Prop collections
Prop collections are basically just objects of props that you maintain for components that have a lot of props you need to keep track of. Common components that would benefit from prop collections are complex interactive elements like toggles or accordions. These components usually require props like onClick
, onKeyDown
, onFocus
,aria-pressed
, aria-expanded
, etc.
The basic idea with prop collections is that you pass them to the user, so they can spread the props over UI components. The benefit of this approach is that the user doesn't have to wire it all up themselves. Returning to the Toggle
case:
Prop getters
Just like prop collections, prop getters help you maintain a list of common props for your UI components. However, they are functions that return props, so you can allow your user to extend, customize, and combine props.
For example, suppose the user needs to trigger analytics when the toggle button is pressed.
The above code overrides the onClick
that provides the toggle functionality, so the toggle stops working. We need a way of combining the built-in onClick
functionality with the custom onClick
analytics functionality. This is where prop getters comes in:
State Reducers and Inversion of Control
Suppose you have a complex input component like a Autocomplete
where you need to handle all kinds of interactions. State reducers are a way of managing all the state of these kinds of components using the reducer pattern via useReducer
.
Suppose someone else now consumes your Autocomplete
component. Sometimes their requirements can get very complex and layered, so how do we build an API that covers all these use cases?
Answer: You don't. It's near impossible to account for every possible use case. You can code for the most common use cases, but there are always going to be edge cases.
Solution: Instead, you implement inversion of control. This just means that you give the consumer back some control of the internals, so they can decide for themselves how to implement their unique requirements.
In practice, this means that you can give the user the option to pass their own reducer to useAutocomplete
, so they can code their own use cases:
There are a few things to note that are clever in this code snippet:
We default to
autocompleteReducer
if the user doesn't provide a custom reducer touseAutocomplete
because this helps cover the majority of times where the user doesn't need to make any customizations to our default logic.We expose the default
autocompleteReducer
to the user, so they can use it as the fallback in theircustomAutocompleteReducer
. The advantage of this is that the user doesn't have to build the reducer from the ground up. They can customize what they want and fall back to default logic for the rest.
Control Props
Last updated