Context Module Functions
Example usage:
import * as React from 'react'
import {CounterProvider, useCounter, increment, decrement} from './counter'
function App() {
const [state, dispatch] = useCounter()
return (
<CounterProvider>
<div>Current Count: {state.count}</div>
<button onClick={() => decrement(dispatch)}>-</button>
<button onClick={() => increment(dispatch)}>+</button>
</CounterProvider>
)
}
// ./counter.js
import * as React from 'react'
const CounterContext = React.createContext()
function CounterProvider({step = 1, initialCount = 0, ...props}) {
const [state, dispatch] = React.useReducer(
(state, action) => {
const change = action.step ?? step
switch (action.type) {
case 'increment': {
return {...state, count: state.count + change}
}
case 'decrement': {
return {...state, count: state.count - change}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
},
{count: initialCount},
)
const value = [state, dispatch]
return <CounterContext.Provider value={value} {...props} />
}
function useCounter() {
const context = React.useContext(CounterContext)
if (context === undefined) {
throw new Error(`useCounter must be used within a CounterProvider`)
}
return context
}
const increment = dispatch => dispatch({type: 'increment'})
const decrement = dispatch => dispatch({type: 'decrement'})
export {CounterProvider, useCounter, increment, decrement}
Compound Components
Compound components are components that work together to form a
complete UI. The classic example of this is
<select> and <option> in HTML:
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
The <select> is the element responsible for
managing the state of the UI, and the
<option> elements are essentially more
configuration for how the select should operate (specifically, which
options are available and their values).
This Patterns creates an API that make the state shared between the components implicit, meaning that the developer using your component cannot actually see or interact with the state (on) or the mechanisms for updating that state (toggle) that are being shared between the components.
Example usage:
import * as React from 'react'
import {Toggle, ToggleOn, ToggleOff, ToggleButton} from './toggle'
function App() {
return (
<div>
<Toggle>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<span>Hello</span>
<ToggleButton />
</Toggle>
</div>
)
A more flexible variant of this pattern, which accept the state (on) and mechanisms for updating that state (toggle) regardless of where they’re rendered within the Toggle component’s “posterity” is the Flexible Compound Components .
from advanced-react-patterns workshop - exercise #2 (advanced-react-patterns/src/final/2.extra-1.js)
See also: Write Compound Components | egghead.io
// ./toggle.js
import * as React from 'react'
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return React.Children.map(children, child => {
return typeof child.type === 'string' // so DOM components as childs work as well
? child
: React.cloneElement(child, {on, toggle})
})
}
function ToggleOn({on, children}) {
return on ? children : null
}
function ToggleOff({on, children}) {
return on ? null : children
}
function ToggleButton({on, toggle, ...props}) {
return <Switch on={on} onClick={toggle} {...props} />
}
export {Toggle, ToggleOn, ToggleOff, ToggleButton}
Flexible Compound Components
The Compound Component pattern only clones and passes props to immediate children. With this more flexible pattern it's possible to render the compound components anywhere in the render tree.
Example usage:
import * as React from 'react'
import {Toggle, ToggleOn, ToggleOff, ToggleButton} from './toggle'
function App() {
return (
<div>
<Toggle>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<div>
<ToggleButton />
</div>
</Toggle>
</div>
)
}
from advanced-react-patterns workshop - exercise #3 (advanced-react-patterns/src/final/03.extra-1.js)
See also Make Compound React Components Flexible | egghead.io (Outdated: usage of legacy context API)
// ./toggle.js
import * as React from 'react'
const ToggleContext = React.createContext()
ToggleContext.displayName = 'ToggleContext'
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return (
<ToggleContext.Provider value={{on, toggle}}>
{children}
</ToggleContext.Provider>
)
}
function useToggle() {
const context = React.useContext(ToggleContext)
if (context === undefined) {
throw new Error('useToggle must be used within a <Toggle />')
}
return context
}
function ToggleOn({children}) {
const {on} = useToggle()
return on ? children : null
}
function ToggleOff({children}) {
const {on} = useToggle()
return on ? null : children
}
function ToggleButton({...props}) {
const {on, toggle} = useToggle()
return <Switch on={on} onClick={toggle} {...props} />
}
export {Toggle, ToggleOn, ToggleOff, ToggleButton}
Prop Collections
Lots of the reusable/flexible components and hooks have some common use-cases. This pattern makes it easier to use components and/or hooks the right way without requiring people to wire things up for common use cases by creating and returning object(s) for the common use cases, so people can simply spread it across the UI they render in conjunction with the hook and/or component.
Example usage:
import * as React from 'react'
import {Switch} from '../switch'
import useToggle from './use-toggle'
function App() {
const {on, togglerProps} = useToggle()
return (
<div>
<Switch on={on} {...togglerProps} />
<hr />
<button aria-label="custom-button" {...togglerProps}>
{on ? 'on' : 'off'}
</button>
</div>
)
}
from advanced-react-patterns workshop - exercise #4 (advanced-react-patterns/src/final/04.js)
See also Use Prop Collections with Render Props | egghead.io
// ./use-toggle.js
import * as React from 'react'
function useToggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return {
on,
toggle,
togglerProps: {
'aria-pressed': on,
onClick: toggle,
},
}
}
export default useToggle
Prop Getters
Similiar to the Prop Collections pattern, this pattern creates and returns functions, instead of objects, that take additional props from the user which should be applied and composes the props together.
Example usage:
import * as React from 'react'
import {Switch} from '../switch'
import useToggle from './use-toggle'
function App() {
const {on, getTogglerProps} = useToggle()
return (
<div>
<Switch {...getTogglerProps({on})} />
<hr />
<button
{...getTogglerProps({
'aria-label': 'custom-button',
onClick: () => console.info('onButtonClick'),
id: 'custom-button-id',
})}
>
{on ? 'on' : 'off'}
</button>
</div>
)
}
from advanced-react-patterns workshop - exercise #4.extra-1 (advanced-react-patterns/src/final/04.extra-1.js)
// ./use-toggle.js
import * as React from 'react'
const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args))
function useToggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
function getTogglerProps({onClick, ...props} = {}) {
return {
'aria-pressed': on,
onClick: callAll(onClick, toggle),
...props,
}
}
return {
on,
toggle,
getTogglerProps,
}
}
export default useToggle
Control Props
Sometimes, the end-developer wants to manage the internal state of the component himself. The State Reducer pattern allows them to manage what state changes are made when a state change happened, but sometimes people may want to make state changes themselves. The Control Props allows this.
The biggest limitation of a state reducer is that it’s impossible to set state of the component from outside it’s normal setState calls (I couldn't implement the first example using a state reducer).
The concept is basically the same as controlled form elements in React itself.
function MyCapitalizedInput() {
const [capitalizedValue, setCapitalizedValue] = React.useState('')
return (
<input
value={capitalizedValue}
onChange={e => setCapitalizedValue(e.target.value.toUpperCase())}
/>
)
}
Example usage:
import * as React from 'react'
import {Switch} from '../switch'
import useToggle from './use-toggle'
function App() {
const [bothOn, setBothOn] = React.useState(false)
const [timesClicked, setTimesClicked] = React.useState(0)
const {toggle: toggleBoth} = useToggle({on: bothOn, onChange: handleToggleChange})
const {toggle: toggleUncontrolled} = useToggle({onChange: {(...args) =>
console.info('Uncontrolled Toggle onChange', ...args)
}})
function handleToggleChange(state, action) {
if (action.type === actionTypes.toggle && timesClicked > 4) {
return
}
setBothOn(state.on)
setTimesClicked(c => c + 1)
}
function handleResetClick() {
setBothOn(false)
setTimesClicked(0)
}
return (
<div>
<div>
<Switch on={bothOn} onClick={toggle} />
<Switch on={bothOn} onClick={toggle} />
</div>
{timesClicked > 4 ? (
<div data-testid="notice">
Whoa, you clicked too much!
<br />
</div>
) : (
<div data-testid="click-count">Click count: {timesClicked}</div>
)}
<button onClick={handleResetClick}>Reset</button>
<hr />
<Switch
onClick={toggleUncontrolled}
/>
</div>
)
}
from advanced-react-patterns workshop - exercise #6 (advanced-react-patterns/src/final/06.js)
See also: Make Controlled React Components with Control Props | egghead.io
// ./use-toggle.js
import * as React from 'react'
const actionTypes = {
toggle: 'toggle',
reset: 'reset',
}
function toggleReducer(state, action) {
switch (action.type) {
case actionTypes.toggle: {
return {on: !state.on}
}
case actionTypes.on: {
return {on: true}
}
case actionTypes.off: {
return {on: false}
}
default: {
throw new Error(`Unhandled type: ${action.type}`)
}
}
}
function useToggle({
initialOn = false,
reducer = toggleReducer,
onChange,
on: controlledOn,
} = {}) {
const {current: initialState} = React.useRef({on: initialOn})
const [state, dispatch] = React.useReducer(reducer, initialState)
const onIsControlled = controlledOn != null
const on = onIsControlled ? controlledOn : state.on
function dispatchWithOnChange(action) {
if (!onIsControlled) {
dispatch(action)
}
onChange?.(reducer({...state, on}, action), action)
}
const toggle = () => dispatchWithOnChange({type: actionTypes.toggle})
const reset = () =>
dispatchWithOnChange({type: actionTypes.reset, initialState})
// ...
return {
on,
reset,
toggle,
}
}
export {actionTypes, toggleReducer, useToggle}
State Reducer
As a [API] user, it'd be cool if I could hook into every state update before it actually happens and modify it [...]
I'm really excited by this new pattern that I see sits in the sweet spot between an uncontrolled and an controlled component.
During the life of a reusable component which is used in many different contexts, feature requests are made over and over again to handle different cases and cater to different scenarios.
The State Reducer pattern gives control over the reducer to the end-developer, so he can update its state his way.
Example usage:
import * as React from 'react'
import {Switch} from '../switch'
import {useToggle, toggleReducer, actionTypes} from './use-toggle'
function Toggle() {
const [clicksSinceReset, setClicksSinceReset] = React.useState(0)
const tooManyClicks = clicksSinceReset >= 4
const {on, toggle, setOn, setOff} = useToggle({
reducer(currentState, action) {
const changes = toggleReducer(currentState, action)
if (tooManyClicks && action.type === actionTypes.toggle) {
// other changes are fine, but on needs to be unchanged
return {...changes, on: currentState.on}
} else {
// the changes are fine
return changes
}
},
})
return (
<div>
<button onClick={setOff}>Switch Off</button>
<button onClick={setOn}>Switch On</button>
<Switch
onClick={() => {
toggle()
setClicksSinceReset(count => count + 1)
}}
on={on}
/>
{tooManyClicks ? (
<button onClick={() => setClicksSinceReset(0)}>Reset</button>
) : null}
</div>
)
}
from advanced-react-patterns workshop - exercise #5 (advanced-react-patterns/src/final/05.extra-2.js)
// ./use-toggle.js
import * as React from 'react'
const actionTypes = {
toggle: 'toggle',
reset: 'reset',
}
function toggleReducer(state, action) {
switch (action.type) {
case actionTypes.toggle: {
return {on: !state.on}
}
case actionTypes.on: {
return {on: true}
}
case actionTypes.off: {
return {on: false}
}
default: {
throw new Error(`Unhandled type: ${action.type}`)
}
}
}
function useToggle({reducer = toggleReducer} = {}) {
const [{on}, dispatch] = React.useReducer(reducer, {on: false})
const toggle = () => dispatch({type: actionTypes.toggle})
const setOn = () => dispatch({type: actionTypes.on})
const setOff = () => dispatch({type: actionTypes.off})
return {on, toggle, setOn, setOff}
}
export {actionTypes, toggleReducer, useToggle}