Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Created April 2, 2019 16:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kentcdodds/0281594d9aa5e57cc53e7e5f88de59d8 to your computer and use it in GitHub Desktop.
Save kentcdodds/0281594d9aa5e57cc53e7e5f88de59d8 to your computer and use it in GitHub Desktop.
An implementation of control props with hooks
// control props
import React from 'react'
import _ from 'lodash'
import {Switch} from '../switch'
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
const noop = () => {}
function toggleReducer(state, {type, initialState}) {
switch (type) {
case useToggle.types.toggle: {
return {on: !state.on}
}
case useToggle.types.reset: {
return initialState
}
case useToggle.types.toggleOn: {
return {on: true}
}
case useToggle.types.toggleOff: {
return {on: false}
}
default:
throw new Error(`Unsupported type: ${type}`)
}
}
function useControlledReducer(reducer, initialState, lazyInitializer, options) {
if (typeof lazyInitializer === 'object') {
options = lazyInitializer
lazyInitializer = undefined
}
const controlledState = _.omitBy(options.controlledState, _.isUndefined)
const [internalState, dispatch] = React.useReducer(
(state, action) => {
const changes = reducer({...state, ...controlledState}, action)
const controlledChanges = {...changes, ...controlledState}
return _.isEqual(state, controlledChanges) ? state : controlledChanges
},
initialState,
lazyInitializer,
)
return [
{...internalState, ...controlledState},
action => {
dispatch(action)
options.onChange(
reducer({...internalState, ...controlledState}, action),
action,
)
},
]
}
function useToggle({
initialOn = false,
reducer = toggleReducer,
onChange = noop,
state: controlledState = {},
} = {}) {
const {current: initialState} = React.useRef({on: initialOn})
const [{on}, dispatch] = useControlledReducer(reducer, initialState, {
controlledState,
onChange,
})
function toggle() {
dispatch({type: useToggle.types.toggle})
}
function reset() {
dispatch({type: useToggle.types.reset, initialState})
}
function getTogglerProps({onClick, ...props} = {}) {
return {
'aria-pressed': on,
onClick: callAll(onClick, toggle),
...props,
}
}
return {
on,
reset,
toggle,
getTogglerProps,
}
}
useToggle.reducer = toggleReducer
useToggle.types = {
toggle: 'toggle',
reset: 'reset',
}
function Toggle({on: controlledOn, onChange}) {
const {on, getTogglerProps} = useToggle({state: {on: controlledOn}, onChange})
const props = getTogglerProps({on})
return <Switch {...props} />
}
function Usage() {
const [bothOn, setBothOn] = React.useState(false)
const [timesClicked, setTimesClicked] = React.useState(0)
function handleToggleChange(state, action) {
if (action.type === useToggle.types.toggle && timesClicked >= 4) {
return
}
setBothOn(state.on)
setTimesClicked(c => c + 1)
}
function handleResetClick(params) {
setBothOn(false)
setTimesClicked(0)
}
return (
<div>
<div>
<Toggle on={bothOn} onChange={handleToggleChange} />
<Toggle on={bothOn} onChange={handleToggleChange} />
</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 />
<div>
<div>Uncontrolled Toggle:</div>
<Toggle
onChange={(...args) =>
console.info('Uncontrolled Toggle onChange', ...args)
}
/>
</div>
</div>
)
}
Usage.title = 'Control Props'
export default Usage
export {Toggle}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment