Created
April 2, 2019 16:05
-
-
Save kentcdodds/0281594d9aa5e57cc53e7e5f88de59d8 to your computer and use it in GitHub Desktop.
An implementation of control props with hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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