React Hooks cheat sheet
Cause 2: states updating each other
Local state: useState Reducers to manage state with useReducer
useEffect(() => {
Declare the state setSecond(first * 3) Initialize a local state:
}, [first])
const [name, setName] = useState('initial value') const initialState = {
useEffect(() => { value: 0,
setFirst(second / 2) }
Update the state }, [second])
setName('new value') // Triggers an infinite update loop :(
Create the reducer:
// or <button onClick={() => setFirst(5)}/>
setName((value) => 'new ' + value) const reducer = (state, action) => {
switch (action.type) {
Solution 2: store in a state which value was updated by the user: case 'increment':
Lazy-initialize the state
// Must create a new state, not modify the current one!
const [updating, setUpdating] = useState(null) return { ...state, value: state.value + 1 }
const [value, setValue] = useState(
case 'set_to':
// Evaluated only at first rendering useEffect(() => { return { ...state, value: action.value }
() => computeValue() if (isUpdating === 'first') setSecond(first * 3) default:
) }, [first, isUpdating]) throw new Error('Unhandled action')
}
useEffect(() => { }
Side effects: useEffect if (isUpdating === 'second') setFirst(second / 2)
}, [second, isUpdating])
Create a local state using useReducer :
Trigger side effects when the component is mounted
// No infinite update loop :)
<button onClick={() => { const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
setUpdating('first')
// HTTP request, setTimeout, etc. <span>{state.value}</span>
setFirst(5)
doSomething()
} />
}, [])
Dispatch actions to update the state:
Cause 3: using an object state
Trigger side effects each time a prop or state is updated <button onClick={() => {
const [object, setObject] = useState({ value: 'aaa', changes: 0 }) dispatch({ type: 'increment' })
useEffect(() => { }} />
doSomethingWith(value) <button onClick={() => {
useEffect(() => {
}, [value]) dispatch({ type: 'set_to', value: 42 })
setObject({ ...object, changes: object.changes + 1 })
}, [object]) }} />
Clean something when the component is unmounted:
<button onClick={() => {
useEffect(() => { // Will trigger an infinit loop :( Examples of hooks to access the browser API
let timeout = setTimeout(doSomething, 5000) setObject({ ...object, value: 'bbb' })
return () => clearTimeout(timeout) }} /> Persist a state in the local storage
}, [])
Solution 3: watch only some of the object’s attributes const usePersistedState = (key, initialValue) => {
const [value, setValue] = useState(initialValue)
Rules when using hooks const [object, setObject] = useState({ value: 'aaa', changes: 0 })
useEffect(() => {
Must only be used in function components useEffect(() => { const existingValue = localStorage.getItem(key)
setObject({ ...object, changes: object.changes + 1 }) if (existingValue !== null) {
Only at component top-level (not in if ) }, [object.value]) // watch only the `value` attribute setValue(existingValue)
}
No return before any hook <button onClick={() => { }, [key])
// No infinit loop :)
setObject({ ...object, value: 'bbb' }) const setAndPersistValue = (newValue) => {
Custom hooks }} /> setValue(newValue)
localStorage.setItem(key, newValue)
Must start with use }
Used to extract common behavior of components, like async requests: Memoize a value with useMemo
return [value, setAndPersistValue]
}
const useApiResult = (param) => { const value = useMemo(() => {
const [result, setResult] = useState(null) // Will be evalutated only when param1 or param2 change // Usage
return expensiveOperation(param1, param2) const [name, setName] = usePersistedState('name', 'John Doe')
useEffect(() => { }, [param1, param2])
fetch('http://your.api?param=' + param)
Get an element’s size
.then((res) => res.json())
.then((result) => setResult(result))
}, [])
Memoize a callback with useCallback const useElementSize = (elementRef) => {
const [width, setWidth] = useState(undefined)
return { result } // Will return a new function only when param1 or param2 change const [height, setHeight] = useState(undefined)
} const handleClick = useCallback(() => {
// To use it in a component: doSomethingWith(param1, param2) useEffect(() => {
const { result } = useApiResult('some-param') }, [param1, param2]) const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
if (entry.contentRect) {
Memoize callback for a dynamic list of elemments: setWidth(entry.contentRect.width)
Getting the current state in async code setHeight(entry.contentRect.height)
// The same function for all the buttons created dynamically }
Problem: in a useEffect , you want to get the value of the current state to const handleClick = useCallback((event) => { }
const button = event.target })
update it (e.g. increment a number)
const value = button.getAttribute('data-value') resizeObserver.observe(elementRef.current)
doSomethingWith(value)
const [val, setVal] = useState(0)
}, []) return () => {
useEffect(() => {
setInterval(() => { resizeObserver.disconnect()
<ul> }
setVal(val + 1)
{objects.map((obj) => ( }, [elementRef])
}, 1000)
<li key={obj.id}>
}, [])
<button data-value={obj.value} onClick={handleClick}> return [width, height]
console.log(val) // always logs 0 :(
{obj.value} }
</button>
Solution 1: use the other syntax of setValue : </li> // Usage
))} const div = useRef()
const [val, setVal] = useState(0) </ul> const [width, height] = useElementSize(div)
useEffect(() => { <div style={{ resize: 'both' }} ref={div} />
setInterval(() => {
// val always contain the current value Contexts & provider/consumer with useContext Get the user’s geolocation
setVal((val) => val + 1)
}, 1000)
}, [])
Create the context: const useGeolocation = () => {
console.log(val) // logs 0, 1, 2, 3... :) const [status, setStatus] = useState('pending')
const themeContext = createContext() const [latitude, setLatitude] = useState(undefined)
const [longitude, setLongitude] = useState(undefined)
Solution 2: use a ref containing the current value:
Create a specific provider for the context:
useEffect(() => {
const [val, setVal] = useState(0) navigator.geolocation.getCurrentPosition(
const ThemeProvider = ({ children, initialTheme = 'light' }) => {
(res) => {
const valRef = useRef(val) const [theme, setTheme] = useState(initialTheme)
setStatus('success')
useEffect(() => (valRef.current = val), [val]) return (
setLatitude(res.coords.latitude)
<themeContext.Provider value={[theme, setTheme]}>
setLongitude(res.coords.longitude)
useEffect(() => { {children}
},
setInterval(() => { </themeContext.Provider>
(err) => {
// valRef.current contains the current value )
console.log(err)
setVal(valRef.current + 1) }
setStatus('error')
}, 1000) }
}, []) // Usage
)
console.log(val) // logs 0, 1, 2, 3... :) ;<ThemeProvider initialTheme="dark">
}, [])
<Label>Hello</Label>
</ThemeProvider>
return { status, latitude, longitude }
Solving infinite loops with useEffect }
Create a custom hook to consume the context:
Cause 1: no dependencies array: // Usage
const useTheme = () => { const { status, latitude, longitude } = useGeolocation()
const [theme, setTheme] = useContext(themeContext)
useEffect(() => {
// Add here additional logic if necessary...
asyncFunction().then((res) => setValue(res))
return [theme, setTheme]
})
} Struggling with hooks, or want to be more comfortable with them?
Solution: always pass an array as second parameter to useEffect : // Usage
Learn how to use them and solve the problems they cause:
const [theme, setTheme] = useTheme()
useEffect(() => {
// ...
}, [])
Created by@scastiel