The contents of this article/summary are based off of the excelent course Advanced React Patterns by Kent C. Dodds.
Author: Daniel Einars
Date Published: 26.08.2020
Date Edited: 26.08.2020
This chapter serves as a small warm-up
When updating state in a component, which references its past state, always use an updater function. React sometimes puts state changes in batches, and without the updater function you cannot guarantee what the state currently is.
state = {on: falase}
this.setState((currentState) => {!currentState.on})
Additionally, provide a changeHandler
function to when updating the state, like so:
this.setState((currentState) => {!currentState.on}, () => {
console.log("I have changed my state to: ", !currentState.on)
})
The changeHandler
function can be passed in by the parent component and notify it of state changes. It's basically a funciton which runs every time the state has been updated.
Compound components have two characteristics:
Compound components are similar to html select elements:
<select name="cars" id="cars">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
The select
element can be used on its own, but it really only becomes useful when used in conjunction with the option
elements.
Compound components can be defined in a similar fashion. The static
instances can be iterated over using React.Children.map
or React.Children.forEach
. In this instance we use the map
function because we want to return elements (as it is being done in the render()
function) using the React.cloneElement
function.
The React.cloneElement
function creates a copy of the passed in component (in this case static On
, static Off
or static Button
) and passing additional props to them like so:
[...]
return React.cloneElement(childElement, {
on: this.state.on,
toggle: this.toggle
})
[...]
Here is the complete example.
import React from "react";
class Toggle extends React.Component {
static On = (props) => props.on ? props.children : null;
static Off = (props) => props.on ? null: props.children;
static Button = ({on, toggle}) => <Switch on={on} onClick={toggle}/>
state = {on: false}
toggle = () =>
this.setState(
({on}) => ({on: !on}),
() => this.props.onToggle(this.state.on),
)
render() {
return React.Children.map(this.props.children, childElement => {
return React.cloneElement(childElement, {
on: this.state.on,
toggle: this.toggle
})
})
}
}
React.Children.map
or React.Children.forEach
React.cloneElement
and pass in new props (by passing them in).this.props.renderMessage ? this.props.renderMessage : undefined
)The drawback of the above implementation is that it can not handle undefined elements, such as wrapping one of the Compounds in a div
tag.
function Usage({
onToggle = (...args) => console.log('onToggle', ...args),
}) {
return (
<Toggle onToggle={onToggle}>
<Toggle.On>The button is on</Toggle.On>
<Toggle.Off>The button is off</Toggle.Off>
<div>
<Toggle.Button />
</div>
</Toggle>
)
}
To deal with this, we provide use the React.Context
api to only give props to those children which require them. First we need to create a context.
const ToggleContext = React.createContext({
on: false,
toggle: () => {},
})
The object passed into the React.createContext
function is the default state. This state is consumed by the children. In order for them to be able to consume it, we need to provide it it the render()
function of the Toggle
class.
[...]
render() {
return (
<ToggleContext.Provider value={this.state}>
{this.props.children}
</ToggleContext.Provider>
)
}
The value
here is mapped to the component state.
[...]
toggle = () =>
this.setState(
({on}) => ({on: !on}),
() => this.props.onToggle(this.state.on),
)
state = {on: false, toggle: this.toggle}
We use the state because we want to hinder unnecessary rendering (react only renders components if their internal state has changed). It seems a bit odd at first, but we need to pass the toggle
function into the state, so the children of the component can "consume" this function. We destructure the provided on
in the consumer function, (we could also provide contextValue
and check for contextValue.on
) and either render the children, or don't.
[...]
static On = ({children}) => (
<ToggleContext.Consumer>
{({on}) => (on ? children : null)}
</ToggleContext.Consumer>
)
The Button
compound doesn't use the components toggle function, but consumes it from the provided context.
[...]
static Button = (props) => (
<ToggleContext.Consumer>
{({on, toggle}) => (
<Switch on={on} onClick={toggle} {...props} />
)}
</ToggleContext.Consumer>
)
React.Context
in small components as well as in large applications. It remains isolated to this component.This chapter deals with render props and answers which problem they solve and how to use them.
The idea behind render props is to give the user implementing the responsibillity and freedom to configure how the component renders. This way the user has the possibillity to add additional functionallity to the component without having to re-implement anything.
class Toggle extends React.Component {
state = {on: false}
toggle = () =>
this.setState(
({on}) => ({on: !on}),
() => {
this.props.onToggle(this.state.on)
},
)
render() {
const {on} = this.state
return this.props.children({on: on, toggle: this.toggle})
}
Using the toggle component has requirements. Here we access the Toggle Components on
and toggle
function to define how the component is wired up.
function Usage({
onToggle = (...args) => console.log('onToggle', ...args),
}) {
return (
<Toggle onToggle={onToggle}>
{({on, toggle}) => (
<div>
{on ? 'The button is on' : 'The button is off'}
<Switch on={on} onClick={toggle} />
<hr />
<button aria-label="custom-button" onClick={toggle}>
{on ? 'on' : 'off'}
</button>
</div>
)}
</Toggle>
)
}
When the responsibility of rendering shifts from the library to the user, the use is in charge of applying the correct props to the render prop components. In order to do this, it is helpful to provide prop collection functions
. These functions retrieve the props, which are necessary for the components using render props to work. In the Toggle
class we now provide a togglerProps
object, which returns all the props reqired to use the Toggle
class functionallity.
class Toggle extends React.Component {
state = {on: false}
toggle = () =>
this.setState(
({on}) => ({on: !on}),
() => this.props.onToggle(this.state.on),
)
getStateAndHelpers() {
return {
on: this.state.on,
toggle: this.toggle,
togglerProps: {
'aria-pressed': this.state.on,
onClick: this.toggle,
},
}
}
render() {
return this.props.children(this.getStateAndHelpers())
}
}
These togglerProps
are passed into the props.children
method, which means that they are accessible to any component implementing the Toggle
class. We can then use the pass the props to our components as needed.
function Usage({
onToggle = (...args) => console.log('onToggle', ...args),
}) {
return (
<Toggle onToggle={onToggle}>
{({on, togglerProps}) => (
<div>
<Switch on={on} {...togglerProps} />
<hr />
<button aria-label="custom-button" {...togglerProps}>
{on ? 'on' : 'off'}
</button>
</div>
)}
</Toggle>
)
}
The problem with prop collections on their own is that you are not allowed to overwrite any props which are in the collection. In the above example the togglerProps
object defines an onClick
function, which is applied to the button via ...togglerProps
. If I want to define a custom onClick function (for example if I wanted to track clicks), I would have to define this onClick
function explicity on the button. However, this overwrites the onClick
function provided by the togglerProps
object as shown behlow.
<button
aria-label="custom-button"
onClick={() => console.log("I've bene clicked!")}
{...togglerProps}>
{on ? 'on' : 'off'}
</button>
A possible fix for ths could be passing the togglerProps
arguments to the new onClick
function.
<button
aria-label="custom-button"
{...togglerProps}>
onClick={(...args) => {
togglerProps.onClick(...args) // fowrading all arguments, whatever they are
console.log("I've bene clicked!")}
}
{on ? 'on' : 'off'}
</button>
However, becomes cumbersome to implement at scale. In order to solve this we provide an getTogglerProps
function, which we can pass our custom arguments to and let the render prop component handle what to do with them. For this to work we define the getTogglerProps
function on the Toggle
class. It accepts an object which will be the users custom attributes (for instance, the custom onClick function). The attributes which the render component cares about are extrated out (in this case, just the onClick
function). The remaining attributes are spread out over the button.
class Toggle extends React.Component {
state = {on: false}
toggle = () =>
this.setState(
({on}) => ({on: !on}),
() => this.props.onToggle(this.state.on),
)
getStateAndHelpers() {
return {
on: this.state.on,
toggle: this.toggle,
getTogglerProps: ({onClick, ...customProps}) => {
return {
onClick: (...args) => {
onClick && onClick(...args) //If `onClick` is passed as an argument, run it as well as our own toggle function.
this.toggle()
},
'aria-pressed': this.state.on,
...customProps, //spread the rest of the custom attributes out over the button.
}
},
}
}
render() {
return this.props.children(this.getStateAndHelpers())
}
}
If you don't want to have to call each function individually, Kent provides a small utility function called callAll
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
It accepts any number of functions, which returns a function which accepts any number of arguments and then loops over each function and if it exists, calls it with the arguments. So, instead of this
[...]
({onClick, ...customProps}) => {
return {
onClick: (...args) => {
onClick && onClick(...args) //If `onClick` is passed as an argument, run it as well as our own toggle function.
this.toggle()
},
'aria-pressed': this.state.on,
...customProps, //spread the rest of the custom attributes out over the button.
}
},
[...]
you can have this (order does not matter):
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
[...]
({onClick, ...customProps}) => {
return {
onClick: callAll(this.toggle, onClick),
'aria-pressed': this.state.on,
...customProps, //spread the rest of the custom attributes out over the button.
}
},
[...]
This chapter deals with controlling state.
When running applications you want to be able to control the initial state as well as be able to reset the state to the initial state. This exersize asks the user to allow for a passed initial state, adds a default initial state and adds a reset function, which resets the state to the passed initial state or the default state.
initialState
object, you won't have to worry about changing the initial state in a buch of other places.If a render prop enables users to control how things are rendered, state reducers enable users to control how the logic works. (Kent C. Dodds)
This part deals with allowing the user to pass his own state management mechanism into the component. Keeping in fashion with the last examples, the added state reducer, which is passed into the Toggle
class, looks like this. It allows the user to toggle the button four times. After that it only accepts changes to the state except on
.
[...]
toggleStateReducer = (state, changes) => {
if (this.state.timesClicked >= 4) {
return {...changes, on: false}
}
return changes
}
[...]
In order to allow the the Toggle
class to accept a state management prop, we define our own internalSetState
function like this and replace all existing this.setState
calls with this.internalSetState
(still passing in either the new state object, or the function which changes the state)
[...]
internalSetState = (changes, callBack) => {
this.setState(currentState => {
return [changes]
.map(c => typeof c === 'function' ? c(currentState) : c)
.map(c => this.props.stateReducer(currentState, c) || {})
.map(c => Object.keys(c).length ? c : null)[0]
}, callBack)
}
[...]
There are a couple of things going on here, so let me go into some detail. Like the this.setState
function, the internalSetState
function also accepts a changes object or function and a callback (to be executed after the state changes have propagated).
this.setState(currentState => {
... : Because we still want to manage state, we call the this.setState
function but request our current state with it.return [changes]
: we wrap the new changes in an array so we can use map
to iterate over the changes.map(c => typeof c === 'function' ? c(currentState) : c)
: We check if the passed changes are a function or an object (as `this.setState accepts both, we also need to accept both) and retrieve the state by either returning it directly or running the passed changes function..map(c => this.props.stateReducer(currentState, c) || {})
: We call the stateReducer provided by the user implementing our Toggle
class.map(c => Object.keys(c).length ? c : null)[0]
: In th event that this.props.stateReducer(currentState, c)
returned an empty object we return null in order to prevent unnecessary rerenders. Lastly we grab the first item in the array with [0]
, which is our state object.}, callBack)
: We run the callback.(example replaced this.setState
function)
[...]
this.internalSetState(
({on}) => ({on: !on}),
() => this.props.onToggle(this.state.on),
)
[...]
Object.keys(object).length ? ...
)internalSetState
function, which accept external state reducer functions. Writing state based components in this way makes them more reusable, especially when combining this with the render prop pattern.In this section we add a type
to the toggle and reset button (default
, reset
forced
) to allow the user to specify more detailed behavior. The usage is updated by including a force update
button.
[...]
<button onClick={() => toggle({type: 'forced'})}>
Force Toggle
</button>
[...]
To handle this type of behavior the toggleReducer
needs to be updated as well to include the forced
action.
[...]
toggleStateReducer = (state, changes) => {
if (changes.type === 'forced') {
return changes
}
if (this.state.timesClicked >= 4) {
return {...changes, on: false}
}
return changes
}
[...]
This section elaborates on how to facilitate state management through props. This allows to determine a component's behavior through props and/or through state.
If you want to be able to do both, you have to check if a state update is coming in through props or if it's part of the state. This can easily be done by checking if the prop in question is undefined
.
isControlled = (prop) => this.props[prop] !== undefined
If a state update comes in through the props, it stands to reason that it exists in the props (otherwise it must come from the component itself either through User Input or some other action).
Additionally you then need to merge the external and internal state. Such a function could look like this:
getState = (state) => {
return Object.entries(state).reduce(
(mergedState, [key, value]) => {
if (this.isControlled(key)) {
mergedState[key] = this.props[key]
} else {
mergedState[key] = value
}
return mergedState
},
{},
)
}
In the example by Kent, he worked the code required to use an external reducer as well. In essence it filters and returns changes, which are not controlled (external input), but still calls the onStateChange
prop with all changes.
Object
This brief chapter will focus on solving prop drilling through usage of the Context API
and Higher Order Components.
This brief example shows how to use the context API. it is largly similar to compund components, with the exception that a pure context API implementation breaks render props. In order to do that we have to check if we're passing standard react "children" or actual functions.
render() {
const ui = typeof this.props.children === 'function' ? this.props.children(this.state) : this.props.children
return (
<ToggleContext.Provider value={this.state}>
{ui}
</ToggleContext.Provider>
)
}
this.props.children
and alter the child behavior with this.
Higher Order Components allow its user to share code. They accept a Component
, add features to the component and return it. Typical implementations of these are found in react-redux, react-router, etc.
The withToggle
function accepts a React.Component
, applies the Toggle.Consumer
logic from previous examples and returns the wrapped component. We have to ensure that props from the Compnent are passed to the wrapped component (done via spreading) as well as forwarding any React.ref
s which might have been applied. Lastly, we ensure that any static propertis of the passed component are hoisted onto the wrapped component (using a library), otherwise these would be lost in the wrapped component.
function withToggle(Component) {
const Wrapper = (props, ref) => {
return (
<Toggle.Consumer>
{toggleUtils => <Component toggle={toggleUtils} ref={ref} {...props}/>}
</Toggle.Consumer>
)
}
Wrapper.displayName = `withToggle(${Component.displayName || Component.name})`
return hoistNonReactStatics(React.forwardRef(Wrapper), Component)
}
We can now easily create components which use the Toggle.Consumr
without having to explicitly type it out in every component.