React's "missing" useOnMount & useOnUpdate hooks
I have recently been slowly converting some old class-based React components to hooks-based
functional components as part of some broader changes, and added some custom hooks to help migrate
some legacy patterns that depended on class-component lifecyle methods like componentDidMount and
componentDidUpdate. IMHO the kinds of patterns that arise in class-based React components &
functional components are often different, but invasive re-writes of large codebases all at once are
pretty much never a good idea. Introducing hooks that mimic old patterns makes each component’s
rewrite fairly mechanical, which can improve confidence of correctness with less testing or manual
QA and makes reviewing the changes easier, enabling incremental improvements rather than getting
bogged down in larger scale refactors.
useOnMount as a replacment for componentDidMount
The implementation of this is so trivial I probably wouldn’t even add it if I were writing a React app from scratch, but the semantic name is nice for code readability, particularly in an older codebase where people are used to seeing class-based lifecycle methods.
/* Run `setup` when component mounts. `setup` can return a `cleanup`
* function that will act as an "on unmount" handler.
*/
export const useOnMount = setup => useEffect(setup, [])
useOnUpdate as a replacement for componentDidUpdate
This is also pretty trivial and in many cases could just as easily be a direct usage of useEffect.
Again, the semantic name can be nice for readability. The real value I see in having this
extracted is for cases where complex componentDidUpdate logic would compare previous/current
values
A pattern I would generally try to avoid in new code but when modernizing a
messy old codebase you gotta pick your battles., since this hook encapsulates
tracking the previous values & passing them to the function
This implementation does
not support returning a “cleanup” handler, though it’s easy to add if you need that. Since the main
use case is re-calling the function to process changed values, I think also calling a cleanup
function when values change would make the behavior harder to understand at the call site, so
preferred to not support that as I think it would be a footgun..
/* A custom hook that runs an effect when dependencies change, but *not*
* on initial mount. The `fn` will receive the *previous* values of the
* deps as arguments.
*/
export function useOnUpdate(fn, deps) {
if (!Array.isArray(deps) || deps.length === 0) {
throw new Error("useOnUpdate requires an array of deps, which cannot be empty")
}
const prevDeps = useRef(null)
useEffect(() => {
if (prevDeps.current !== null) {
fn(...prevDeps.current)
}
prevDeps.current = deps
}, deps)
}