Here are some notes from the Epic React workshop Advanced Hooks.
Function Dependencies in useEffect
We use the useEffect
hook to run side effects in React. The hook has a “dependency array” which tells it when to fire.
That works fine if the trigger is a variable. Like this:
const [count, setCount] = useState(0)
React.useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`
}, [count]) // <-- that's the dependency list
What happens if we use a function as a trigger?
const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
updateLocalStorage()
}, []) // <-- what goes in that dependency list?
Kent explains that if we put count
into the dependency array (the variable from the updateLocalStorage
function), we have a disconnect between the update function (updateLocalStorage
) and useEffect
. If we change updateLocalStorage
(the trigger), we have to remember to update the dependency list, too. That’s not ideal.
It also doesn’t make sense to put the updateLocalStorage
function as a dependency into the array:
const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
updateLocalStorage()
}, [updateLocalStorage]) // <-- function as a dependency
The function is re-initialized with every render. Now useEffect
runs on every render.
And that’s why we need the useCallback
hook:
function asyncReducer(state, action) {
switch (action.type) {
case 'pending': {
return { status: 'pending', data: null, error: null }
}
case 'resolved': {
return { status: 'resolved', data: action.data, error: null }
}
case 'rejected': {
return { status: 'rejected', data: null, error: action.error }
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function useAsync(asyncCallback, initialState) {
const [state, dispatch] = React.useReducer(asyncReducer, {
status: 'idle',
data: null,
error: null,
...initialState,
})
React.useEffect(() => {
const promise = asyncCallback()
if (!promise) {
return
}
dispatch({ type: 'pending' })
promise.then(
(data) => {
dispatch({ type: 'resolved', data })
},
(error) => {
dispatch({ type: 'rejected', error })
}
)
}, [asyncCallback])
return state
}
In your component, you can use the hook like this:
function PokemonInfo({ pokemonName }) {
const asyncCallback = React.useCallback(() => {
if (!pokemonName) {
return
}
return fetchPokemon(pokemonName)
}, [pokemonName])
const state = useAsync(asyncCallback, {
status: pokemonName ? 'pending' : 'idle',
})
const { data: pokemon, status, error } = state
if (status === 'idle') {
return 'Submit a pokemon'
} else if (status === 'pending') {
return <PokemonInfoFallback name={pokemonName} />
} else if (status === 'rejected') {
throw error
} else if (status === 'resolved') {
return <PokemonDataView pokemon={pokemon} />
}
throw new Error('This should be impossible')
}
Create a Custom Consumer Hook for React Context
You can create a custom consumer hook for your context:
const CountContext = React.createContext()
function CountProvider(props) {
const [count, setCount] = React.useState(0)
const value = [count, setCount]
return <CountContext.Provider value={value} {...props} />
}
// custom consumer hook!
function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function CountDisplay() {
const [count] = useCount()
return <div>{`The current count is ${count}`}</div>
}
function Counter() {
const [, setCount] = useCount()
const increment = () => setCount((c) => c + 1)
return <button onClick={increment}>Increment count</button>
}
function App() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}
Scroll Behavior With useImperativeHandle
You can use useImperativeHandle
for a “scrollToTop” function:
const MessagesDisplay = React.forwardRef(function MessagesDisplay(
{messages},
ref,
) {
const containerRef = React.useRef()
React.useLayoutEffect(() => {
scrollToBottom()
})
function scrollToTop() {
containerRef.current.scrollTop = 0
}
function scrollToBottom() {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
React.useImperativeHandle(ref, () => ({
scrollToTop,
scrollToBottom,
}))
return (
<div ref={containerRef} role="log">
// jsx
</div>
)
Complete code is on GitHub.
useDebugValue
The useDebugValue
hook which you can use in custom hooks for debugging. You can use it with the React DevTools.
Thoughts
I like how the workshop delves deeper into intermediate patterns and good practices for using hooks. I was familiar with most hooks, but my code wasn’t as re-usable and decoupled as what I learned in the workshop.
Further Reading
- When to useMemo and useCallback by Kent C. Dodds
- Epic React
- Using the Effect Hook
- How to use React Context effectively by Kent C. Dodds
- Advanced React Hooks GitHub repository by Kent C. Dodds
- A Complete Guide to useEffect by Dan Abramov