Skip to content

Instantly share code, notes, and snippets.

@bingomanatee
Created January 31, 2023 20:03
Show Gist options
  • Save bingomanatee/eb77ff6fb2596a4666e178e5a9ae282e to your computer and use it in GitHub Desktop.
Save bingomanatee/eb77ff6fb2596a4666e178e5a9ae282e to your computer and use it in GitHub Desktop.
a cheat sheet on memoization
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