An Animated Guide to Reacts Optional Parameters
Reacts API includes several optional and alternative parameters which provide additional behavior like optimization or other extras that occasionally can come in useful. Let’s look at those as animated diagrams.
Did you know that the React API includes several optional and alternative parameters? Hidden extras that provide ‘bonus behaviors’. Most are meant for optimization and can help you squeeze the most performance out of your React app.
While these are not the most essential parts of React to know, it’s good to have a solid understanding of all the tools at your disposal. So, let’s take a closer look at these hidden extras. We’ll go over them in order of commonness.
useState
doesn’t really have optional parameters but you can optionally trigger special behavior by passing a function instead of a value to useState
itself or its setter function.
I call these alternative parameters, useState
is the only part of React that has these.
React Docs
Components are contained pieces of the UI with logic for both initializing and updating.
This is very convenient. One thing to note is that initialization logic is only for well, initialization.
After that we don’t need to evaluate it anymore.
Initial arguments for hooks, like for useState
, are read only on mount and ignored on updates.
The initial arguments to hooks are still declared on every update though. Most of the time this is not a problem, only when declaring the initial argument is , it will be wasted effort on updates that might contribute to noticable jank.
To avoid recreating the initial state, React lets you pass an initializer function that creates the initial state. React will call it when the component mounts and not on updates 1. Maybe you’ve heard of this strategy as lazy evaluation.
React Docs
Usually you update state by passing a new value to the state-setter function, like this:
Instead of a new value, React also lets you pass a function that calculates the next state based on the previous state; a state updater function.
// |-state updater function-| setCount(prevCount => prevCount + 1)
A state updater function can be more convenient when updating state based on current state.
You no longer need to pass the state variable around to access it for an update.
This helps you avoid adding to a dependency list when updating state based on previous state within a hook with a dependency list
(like useEffect
or useCallback
)
I really like this description from A Complete Guide to useEffect:
When we want to update state based on the previous state, we can use the functional updater form of
setState
. [W]e only truly needed count to transform [state] and “send it back” to React. But React already knows the current [state]. All we needed to tell React is to increment the state — whatever it is right now.That’s exactly what
setCount(prevCount => prevCount + 1)
does. You can think of it as “sending an instruction” to React about how the state should change.
The second benefit is that a state update functions enables queuing of updates. To briefly explain, queuing is applying multiple updates on top of each other from a single event. The React docs has a whole page on it that explains it better than I could, so I’ll just refer you to that: Queueing a Series of State Updates.
Here’s my animated diagram of it.
The React docs calls it an “uncommon use case” 2 and I have to agree. I haven’t needed to batch updates a whole lot, at most a couple of times.
Reusing state updater functions
By convention, state updater functions are writting inline as anonymous arrow functions. To reuse them, you can define state updater functions in module scope. It’s not common but I occasionally quite like it, see my post State Updater Module Functions for more on this pattern.
React Docs
Like useState
, useReducer
supports an initializer function but the way it takes it is different.
Rather than an alternative parameter type, it takes the initializer function as an optional parameter 3.
The initializer is called with the preceding parameter; initArg
. Confusing? That’s why I like diagrams better for explaining this, even more so when they’re animated:
Making a reducers parameters optional
In a way, a reducers parameters are optional. You’re not obliged to use the state
or action
parameters, a reducer only has to return new state.
Ignoring action
or state
parameter makes it an actionless or stateless reducer respectively.
There are reasons to do so, I wrote about that and when that’s useful in Actionless and Stateless Reducers in React.
React Docs
memo
is for optimizing rendering behavior, it lets you skip re-rendering a component when its props are unchanged 4.
I’ve always thought of like this: memo
turns your components props into a dependency list for that component.
React checks the dependencies — i.e. props — every render and will only rerun the component when one has changed.
By default, React will compare each prop with Object.is
5, just like values in dependency lists.
memo
's optional parameter, arePropsEqual
, lets you declare a custom comparison function.
memo
.But most likely you won’t use memo
all that often and its optional parameter very rarely or perhaps never, but now you know about it at least.
When to use memo
is a subject of its own. This bit in the docs is a good guideline:
If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.
That whole section is worth a read.
My personal, hand–wavy advice is You probably don’t need
.
JavaScript engines are highly–optimized beasts and our intuitions on what’s slow are often wrong, at least mine is.
So, a final word of caution from the docs on using a custom comparison function:memo
, or less often than you think
If you [use
arePropsEqual
], use the Performance panel in your browser developer tools to make sure that your comparison function is actually faster than re-rendering the component. You might be surprised.
React Docs
useDebugValue
is a React Hook that lets you add a label to a custom hook in the React DevTools 6.
It’s among the most rarely used hooks. Big chance you’ll never ever use this hook and even bigger chance you’ll never have to use its optional parameter.
It might just be the most obscure part of the React API. Let’s look at it anyway.
Usually you just pass the string to display in the DevTools to useDebugValue
:
Now suppose you want to format some giant grid in state for useDebugValue
.
const grid = [ { a: 10, b: 4, c: 8, ... , j: 4}, { a: 2, b: 8, c: 1, ... , z: 8}, // ... many more rows]
It would be nice to view it without the keys being repeated for each row, as an array of strings like this:
We’ll need a formatGrid
function to format it like that.
useDebugValue(formatGrid(grid));
Great, but you, and maybe a few of your fellow devs are the only ones who will see the result of calling formatGrid
.
Yet it will run for everyone, even for those who don’t look at the React DevTools, and also for users who have no idea of what React even is (and why should they? The app just needs to work).
Once again we need lazy evaluation to defer calling it.
That’s why useDebugValue
accepts an optional format
function.
The format
function takes the preceding parameter; value
, similar to how the init
function of useReducer
takes the preceding parameter.
React won’t call the format
function until the hook is inspected in the React DevTools, sparing some computation for our users.
View this CodeSandbox for live code of this example.
useDebugValue(grid, formatGrid);
React Docs
useSyncExternalStore
is another rarely used hook. It is mostly meant for library authors, it does have some use cases in app code.
useSyncExternalStore
accepts an optional parameter to provide an initial value when the app is run outside of a browser environment, like on a server.
In that case, this parameter is mandatory, an error will be thrown if it’s missing 7.
No animation for this one. I didn’t have good ideas and I just didn’t feel like making one ¯\_(ツ)_/¯. Maybe someday.
We can group React’s optional parameters into two camps: 1) optimization and 2) other additional behavior.
useState
initializeruseReducer
initializeruseDebugValue
formatter
memo
custom comparison function
useSyncExternalStore
sgetServerSnapshot
for SSR supportuseState
state updater queueing series of state updates in one event
Or summarized as a nifty circle pack diagram:
I didn’t discuss the effect dependency lists here (useEffect
, useLayoutEffect
, and I guess useImperativeHandle
too), those are technically optional too.
I don’t really see those as a ‘hidden extra’. More often then not, effects will have dependency lists so I left them out.
We’ve explored the depths of the React API. If you found this useful, share it on your social channels of choice. If you have questions, feel free to send me a tweet.
-
↩
React saves the initial state once and ignores it on the next renders. Although the result of
React Docs: useState — Avoiding recreating the initial statecreateInitialTodos()
is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating large arrays or performing expensive calculations. To solve this, you may pass it as an initializer function touseState
instead: -
↩
It is an uncommon use case, but if you would like to update the same state variable multiple times before the next render, instead of passing the next state value like
React Docs: Queueing a Series of State Updates — Updating the same state multiple times before the next rendersetNumber(number + 1)
, you can pass a function that calculates the next state based on the previous one in the queue, likesetNumber(n => n + 1)
. It is a way to tell React to “do something with the state value” instead of just replacing it. -
↩
React saves the initial state once and ignores it on the next renders. Although the result of
React Docs: useReducer — Avoiding recreating the initial statecreateInitialState(username)
is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating large arrays or performing expensive calculations. To solve this, you may pass it as an initializer function touseReducer
as the third argument instead: -
↩
React Docs: memomemo
lets you skip re-rendering a component when its props are unchanged. Wrap a component inmemo
to get a memoized version of that component. This memoized version of your component will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed. But React may still re-render it: memoization is a performance optimization, not a guarantee. -
↩
optional
React Docs: memo - arePropsEqualarePropsEqual
: A function that accepts two arguments: the component’s previous props, and its new props. It should return true if the old and new props are equal: that is, if the component will render the same output and behave in the same way with the new props as with the old. Otherwise it should return false. Usually, you will not specify this function. By default, React will compare each prop withObject.is
. -
↩
React Docs: useDebugValueuseDebugValue
is a React Hook that lets you add a label to a custom Hook in React DevTools. -
↩
optional
React Docs: useSyncExternalStore - getServerSnapshotgetServerSnapshot
: A function that returns the initial snapshot of the data in the store. It will be used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client. If you omit this argument, rendering the component on the server will throw an error.