Created
January 31, 2023 20:03
-
-
Save bingomanatee/eb77ff6fb2596a4666e178e5a9ae282e to your computer and use it in GitHub Desktop.
a cheat sheet on memoization
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
Memoizing -- in general -- is essentially _caching_ -- preserving fixed answers to fixed input. This can | |
make computation faster by eliminating repeated processes that ultimately produce a known result for the same input. | |
This assumes "pure" functions, or in react, pure components. | |
Given that there is a certain overhead in translating html to JSX, caching the JSX for deterministic components saves | |
a significant amount of render time. | |
Similarly, react components only re-render when their arguments change. So, if an argument changes every time a | |
component | |
is encountered, the component will _always_ re-render. given that parameters are compared by reference, this | |
component... | |
```javascript | |
const List = ({ values }) => ( | |
<ul> | |
{values.map((value) => (<li>{value}</li>))} | |
</ul> | |
) | |
const Affirmation = () => { | |
const values = ["i'm good enough", "I'm smart enough", "People like me"]; | |
return <List values={values}/> | |
} | |
``` | |
... will _always_ re-render because the `values` property is re-created every time the first line of Affirmation is | |
used. | |
Sometimes just putting values in constants outside the comonent is the way to go; but what about when they must reflect | |
parameters? | |
You could do something like this: | |
```javascript | |
const List = ({ values }) => ( | |
<ul> | |
{values.map((value) => (<li>{value}</li>))} | |
</ul> | |
) | |
const Affirmation = () => { | |
const [values] = useState(["i'm good enough", "I'm smart enough", "People like me"]); | |
return <List values={values}/> | |
} | |
``` | |
This is, technically, going to freeze the reference of values to a single value for the lifetime of the Affirmation | |
instance. | |
But what about if the value varies by input? (skipping the hack of using useEffect + useState) | |
## `useMemo(factoryFn, dependencies)` | |
This is the best way to memoize values in React: | |
```javascript | |
import { useMemo } from 'react'; | |
const List = ({ values }) => ( | |
<ul> | |
{values.map((value) => (<li>{value}</li>))} | |
</ul> | |
) | |
const Affirmation = ({ max = 3 }) => { | |
const values = useMemo( | |
() => ( | |
["i'm good enough", "I'm smart enough", "People like me"].slice(0, max) | |
), | |
[max] | |
); | |
return <List values={values}/> | |
} | |
``` | |
`useMemo` takes two arguments: | |
* a **factory function** that returns a value | |
* a list of **dependencies** that the function uses to derive a value | |
As a rule, any resource used in the computation of the derived value should be | |
listed in the array of dependencies, | |
## `memo(component) => component` | |
You can also memoize **entire components** by enclosing them in the `memo()` decorator. | |
This will cache the components output based on its input parameter, obviating the need to run the render function | |
entirely. | |
```javascript | |
import { memo } from 'react'; | |
const List = ({ values }) => ( | |
<ul> | |
{values.map((value) => (<li>{value}</li>))} | |
</ul> | |
) | |
const Affirmation = ({ max = 3 }) => { | |
const values = ["i'm good enough", "I'm smart enough", "People like me"].slice(0, max); | |
return <List values={values}/> | |
} | |
export default memo(Affirmation); | |
``` | |
Now, there is no point in memoizing `values` any more -- they will never be recomputed until | |
max changes. (it might make the component _somewhat_ faster to use both, but at this scale, why bother). | |
There _may_ be some efficiency in memoizing `List`, though. | |
## The const of memoization | |
Every time you memoize a value or component in React, you create a cache - and a cache does use memory. | |
In this example, if max varied from 0 to 1000, you would potentially have a million cached bits of DOM | |
stored somewhere in the client's memory. You wouldn't want to memoize based on a value that changed when | |
the user scrolls; you could potentially be asserting _thousands_ of values into a cache, and never repeat | |
the same value. | |
Memoization improves performance when the number of variations is limited, and the same set of parameters | |
are visited more than once. | |
## Improving component memoization through useCallback | |
If your component takes hooks, and your hooks are instantiated every pass through, you will never achiieve | |
efficiency through memoization (and your cache will bloat to fantastic sizes). | |
for instance: | |
```javascript | |
const Field = ({ value, onChange }) => ( | |
<input type="text" value={value} onChange={onChange}/> | |
) | |
const FieldM = memo(Field); | |
const Form = () => { | |
const [firstName, setFirst] = useState(''); | |
const [lastName, setLast] = useState(''); | |
return ( | |
<div> | |
<h2>User Info</h2> | |
<div> | |
<label>First Name</label> | |
<FieldM value={firstName} onChange={(e) => setFirst(e.target.value)}/> | |
</div> | |
<div> | |
<label>Last Name</label> | |
<FieldM value={lastName} onChange={(e) => setLast(e.target.value)}/> | |
</div> | |
</div> | |
); | |
} | |
``` | |
This may look like you are creating an efficient cache, but in reality, every time Form runs, the FieldM cache will | |
have a new entry. This is because the handler `onChange` is created every time Form is rendered (twice). | |
Memoizing functions is slightly different than memoizing other things. Partly this is due to the way hooks work. | |
And also, `useMemo(() => ()=>{...},[])` is dorky. | |
The fix is to use UseCallback: | |
```javascript | |
const Field = ({ value, onChange }) => ( | |
<input type="text" value={value} onChange={onChange}/> | |
) | |
const FieldM = memo(Field); | |
const Form = () => { | |
const [firstName, setFirst] = useState(''); | |
const [lastName, setLast] = useState(''); | |
const firstHandle = useCallback( | |
(e) => setFirst(e.target.value), | |
[setFirst] | |
) | |
const lastHandle = useCallback( | |
(e) => setlast(e.target.value), | |
[setFirst] | |
) | |
return ( | |
<div> | |
<h2>User Info</h2> | |
<div> | |
<label>First Name</label> | |
<FieldM value={firstName} onChange={firstHandle}/> | |
</div> | |
<div> | |
<label>Last Name</label> | |
<FieldM value={lastName} onChange={lastHandle}/> | |
</div> | |
</div> | |
); | |
} | |
``` | |
This will now create an efficient cache for Field. | |
## When not to memoize | |
Aside from not using memo/useMemo/useCallback when the range of the parameters is too large, | |
you also shouldn't memoize values when the factory function is _impure_; as in, it varies randomly, | |
or through some other outside value that is beyond your control, such as time (which never repeats). | |
Caching only gives dividends when the same value is repeated. | |
So, why cache the field entries, above? its likely that they will just get typed in once (one character | |
at a time). Well -- remember, _both_ fields are being memoized, so when the `firstName` is being | |
updated one character at a time, the `lastName` field is being passed the same handler and value | |
over and over, so its cached value is reused. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment