UNPKG

22 kBMarkdownView Raw
1# memoize-one
2
3A memoization library that only caches the result of the most recent arguments.
4
5[![npm](https://img.shields.io/npm/v/memoize-one.svg)](https://www.npmjs.com/package/memoize-one)
6![types](https://img.shields.io/badge/types-typescript%20%7C%20flow-blueviolet)
7[![minzip](https://img.shields.io/bundlephobia/minzip/memoize-one.svg)](https://www.npmjs.com/package/memoize-one)
8[![Downloads per month](https://img.shields.io/npm/dm/memoize-one.svg)](https://www.npmjs.com/package/memoize-one)
9
10## Rationale
11
12Unlike other memoization libraries, `memoize-one` only remembers the latest arguments and result. No need to worry about cache busting mechanisms such as `maxAge`, `maxSize`, `exclusions` and so on, which can be prone to memory leaks. A function memoized with `memoize-one` simply remembers the last arguments, and if the memoized function is next called with the same arguments then it returns the previous result.
13
14> For working with promises, [@Kikobeats](https://github.com/Kikobeats) has built [async-memoize-one](https://github.com/microlinkhq/async-memoize-one).
15
16## Usage
17
18```js
19// memoize-one uses the default import
20import memoizeOne from 'memoize-one';
21
22function add(a, b) {
23 return a + b;
24}
25const memoizedAdd = memoizeOne(add);
26
27memoizedAdd(1, 2);
28// add function: is called
29// [new value returned: 3]
30
31memoizedAdd(1, 2);
32// add function: not called
33// [cached result is returned: 3]
34
35memoizedAdd(2, 3);
36// add function: is called
37// [new value returned: 5]
38
39memoizedAdd(2, 3);
40// add function: not called
41// [cached result is returned: 5]
42
43memoizedAdd(1, 2);
44// add function: is called
45// [new value returned: 3]
46// 👇
47// While the result of `add(1, 2)` was previously cached
48// `(1, 2)` was not the *latest* arguments (the last call was `(2, 3)`)
49// so the previous cached result of `(1, 3)` was lost
50```
51
52## Installation
53
54```bash
55# yarn
56yarn add memoize-one
57
58# npm
59npm install memoize-one --save
60```
61
62## Function argument equality
63
64By default, we apply our own _fast_ and _relatively naive_ equality function to determine whether the arguments provided to your function are equal. You can see the full code here: [are-inputs-equal.ts](https://github.com/alexreardon/memoize-one/blob/master/src/are-inputs-equal.ts).
65
66(By default) function arguments are considered equal if:
67
681. there is same amount of arguments
692. each new argument has strict equality (`===`) with the previous argument
703. **[special case]** if two arguments are not `===` and they are both `NaN` then the two arguments are treated as equal
71
72What this looks like in practice:
73
74```js
75import memoizeOne from 'memoize-one';
76
77// add all numbers provided to the function
78const add = (...args = []) =>
79 args.reduce((current, value) => {
80 return current + value;
81 }, 0);
82const memoizedAdd = memoizeOne(add);
83```
84
85> 1. there is same amount of arguments
86
87```js
88memoizedAdd(1, 2);
89// the amount of arguments has changed, so underlying add function is called
90memoizedAdd(1, 2, 3);
91```
92
93> 2. new arguments have strict equality (`===`) with the previous argument
94
95```js
96memoizedAdd(1, 2);
97// each argument is `===` to the last argument, so cache is used
98memoizedAdd(1, 2);
99// second argument has changed, so add function is called again
100memoizedAdd(1, 3);
101// the first value is not `===` to the previous first value (1 !== 3)
102// so add function is called again
103memoizedAdd(3, 1);
104```
105
106> 3. **[special case]** if the arguments are not `===` and they are both `NaN` then the argument is treated as equal
107
108```js
109memoizedAdd(NaN);
110// Even though NaN !== NaN these arguments are
111// treated as equal as they are both `NaN`
112memoizedAdd(NaN);
113```
114
115## Custom equality function
116
117You can also pass in a custom function for checking the equality of two sets of arguments
118
119```js
120const memoized = memoizeOne(fn, isEqual);
121```
122
123An equality function should return `true` if the arguments are equal. If `true` is returned then the wrapped function will not be called.
124
125**Tip**: A custom equality function needs to compare `Arrays`. The `newArgs` array will be a new reference every time so a simple `newArgs === lastArgs` will always return `false`.
126
127Equality functions are not called if the `this` context of the function has changed (see below).
128
129Here is an example that uses a [lodash.isEqual](https://lodash.com/docs/4.17.15#isEqual) deep equal equality check
130
131> `lodash.isequal` correctly handles deep comparing two arrays
132
133```js
134import memoizeOne from 'memoize-one';
135import isDeepEqual from 'lodash.isequal';
136
137const identity = (x) => x;
138
139const shallowMemoized = memoizeOne(identity);
140const deepMemoized = memoizeOne(identity, isDeepEqual);
141
142const result1 = shallowMemoized({ foo: 'bar' });
143const result2 = shallowMemoized({ foo: 'bar' });
144
145result1 === result2; // false - different object reference
146
147const result3 = deepMemoized({ foo: 'bar' });
148const result4 = deepMemoized({ foo: 'bar' });
149
150result3 === result4; // true - arguments are deep equal
151```
152
153The equality function needs to conform to the `EqualityFn` `type`:
154
155```ts
156// TFunc is the function being memoized
157type EqualityFn<TFunc extends (...args: any[]) => any> = (
158 newArgs: Parameters<TFunc>,
159 lastArgs: Parameters<TFunc>,
160) => boolean;
161
162// You can import this type
163import type { EqualityFn } from 'memoize-one';
164```
165
166The `EqualityFn` type allows you to create equality functions that are extremely typesafe. You are welcome to provide your own less type safe equality functions.
167
168Here are some examples of equality functions which are ordered by most type safe, to least type safe:
169
170<details>
171 <summary>Example equality function types</summary>
172 <p>
173
174```ts
175// the function we are going to memoize
176function add(first: number, second: number): number {
177 return first + second;
178}
179
180// Some options for our equality function
181// ↑ stronger types
182// ↓ weaker types
183
184// ✅ exact parameters of `add`
185{
186 const isEqual = function (first: Parameters<typeof add>, second: Parameters<typeof add>) {
187 return true;
188 };
189 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
190}
191
192// ✅ tuple of the correct types
193{
194 const isEqual = function (first: [number, number], second: [number, number]) {
195 return true;
196 };
197 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
198}
199
200// ❌ tuple of incorrect types
201{
202 const isEqual = function (first: [number, string], second: [number, number]) {
203 return true;
204 };
205 expectTypeOf<typeof isEqual>().not.toMatchTypeOf<EqualityFn<typeof add>>();
206}
207
208// ✅ array of the correct types
209{
210 const isEqual = function (first: number[], second: number[]) {
211 return true;
212 };
213 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
214}
215
216// ❌ array of incorrect types
217{
218 const isEqual = function (first: string[], second: number[]) {
219 return true;
220 };
221 expectTypeOf<typeof isEqual>().not.toMatchTypeOf<EqualityFn<typeof add>>();
222}
223
224// ✅ tuple of 'unknown'
225{
226 const isEqual = function (first: [unknown, unknown], second: [unknown, unknown]) {
227 return true;
228 };
229 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
230}
231
232// ❌ tuple of 'unknown' of incorrect length
233{
234 const isEqual = function (first: [unknown, unknown, unknown], second: [unknown, unknown]) {
235 return true;
236 };
237 expectTypeOf<typeof isEqual>().not.toMatchTypeOf<EqualityFn<typeof add>>();
238}
239
240// ✅ array of 'unknown'
241{
242 const isEqual = function (first: unknown[], second: unknown[]) {
243 return true;
244 };
245 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
246}
247
248// ✅ spread of 'unknown'
249{
250 const isEqual = function (...first: unknown[]) {
251 return !!first;
252 };
253 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
254}
255
256// ✅ tuple of 'any'
257{
258 const isEqual = function (first: [any, any], second: [any, any]) {
259 return true;
260 };
261 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
262}
263
264// ❌ tuple of 'any' or incorrect size
265{
266 const isEqual = function (first: [any, any, any], second: [any, any]) {
267 return true;
268 };
269 expectTypeOf<typeof isEqual>().not.toMatchTypeOf<EqualityFn<typeof add>>();
270}
271
272// ✅ array of 'any'
273{
274 const isEqual = function (first: any[], second: any[]) {
275 return true;
276 };
277 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
278}
279
280// ✅ two arguments of type any
281{
282 const isEqual = function (first: any, second: any) {
283 return true;
284 };
285 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
286}
287
288// ✅ a single argument of type any
289{
290 const isEqual = function (first: any) {
291 return true;
292 };
293 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
294}
295
296// ✅ spread of any type
297{
298 const isEqual = function (...first: any[]) {
299 return true;
300 };
301 expectTypeOf<typeof isEqual>().toMatchTypeOf<EqualityFn<typeof add>>();
302}
303```
304
305 </p>
306</details>
307
308## `this`
309
310### `memoize-one` correctly respects `this` control
311
312This library takes special care to maintain, and allow control over the the `this` context for **both** the original function being memoized as well as the returned memoized function. Both the original function and the memoized function's `this` context respect [all the `this` controlling techniques](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md):
313
314- new bindings (`new`)
315- explicit binding (`call`, `apply`, `bind`);
316- implicit binding (call site: `obj.foo()`);
317- default binding (`window` or `undefined` in `strict mode`);
318- fat arrow binding (binding to lexical `this`)
319- ignored this (pass `null` as `this` to explicit binding)
320
321### Changes to `this` is considered an argument change
322
323Changes to the running context (`this`) of a function can result in the function returning a different value even though its arguments have stayed the same:
324
325```js
326function getA() {
327 return this.a;
328}
329
330const temp1 = {
331 a: 20,
332};
333const temp2 = {
334 a: 30,
335};
336
337getA.call(temp1); // 20
338getA.call(temp2); // 30
339```
340
341Therefore, in order to prevent against unexpected results, `memoize-one` takes into account the current execution context (`this`) of the memoized function. If `this` is different to the previous invocation then it is considered a change in argument. [further discussion](https://github.com/alexreardon/memoize-one/issues/3).
342
343Generally this will be of no impact if you are not explicity controlling the `this` context of functions you want to memoize with [explicit binding](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#explicit-binding) or [implicit binding](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#implicit-binding). `memoize-One` will detect when you are manipulating `this` and will then consider the `this` context as an argument. If `this` changes, it will re-execute the original function even if the arguments have not changed.
344
345## Clearing the memoization cache
346
347A `.clear()` property is added to memoized functions to allow you to clear it's memoization cache.
348
349This is helpful if you want to:
350
351- Release memory
352- Allow the underlying function to be called again without having to change arguments
353
354```ts
355import memoizeOne from 'memoize-one';
356
357function add(a: number, b: number): number {
358 return a + b;
359}
360
361const memoizedAdd = memoizeOne(add);
362
363// first call - not memoized
364const first = memoizedAdd(1, 2);
365
366// second call - cache hit (underlying function not called)
367const second = memoizedAdd(1, 2);
368
369// 👋 clearing memoization cache
370memoizedAdd.clear();
371
372// third call - not memoized (cache was cleared)
373const third = memoizedAdd(1, 2);
374```
375
376## When your result function `throw`s
377
378> There is no caching when your result function throws
379
380If your result function `throw`s then the memoized function will also throw. The throw will not break the memoized functions existing argument cache. It means the memoized function will pretend like it was never called with arguments that made it `throw`.
381
382```js
383const canThrow = (name: string) => {
384 console.log('called');
385 if (name === 'throw') {
386 throw new Error(name);
387 }
388 return { name };
389};
390
391const memoized = memoizeOne(canThrow);
392
393const value1 = memoized('Alex');
394// console.log => 'called'
395const value2 = memoized('Alex');
396// result function not called
397
398console.log(value1 === value2);
399// console.log => true
400
401try {
402 memoized('throw');
403 // console.log => 'called'
404} catch (e) {
405 firstError = e;
406}
407
408try {
409 memoized('throw');
410 // console.log => 'called'
411 // the result function was called again even though it was called twice
412 // with the 'throw' string
413} catch (e) {
414 secondError = e;
415}
416
417console.log(firstError !== secondError);
418
419const value3 = memoized('Alex');
420// result function not called as the original memoization cache has not been busted
421console.log(value1 === value3);
422// console.log => true
423```
424
425## Function properties
426
427Functions memoized with `memoize-one` do not preserve any properties on the function object.
428
429> This behaviour correctly reflected in the TypeScript types
430
431```ts
432import memoizeOne from 'memoize-one';
433
434function add(a, b) {
435 return a + b;
436}
437add.hello = 'hi';
438
439console.log(typeof add.hello); // string
440
441const memoized = memoizeOne(add);
442
443// hello property on the `add` was not preserved
444console.log(typeof memoized.hello); // undefined
445```
446
447If you feel strongly that `memoize-one` _should_ preserve function properties, please raise an issue. This decision was made in order to keep `memoize-one` as light as possible.
448
449For _now_, the `.length` property of a function is not preserved on the memoized function
450
451```ts
452import memoizeOne from 'memoize-one';
453
454function add(a, b) {
455 return a + b;
456}
457
458console.log(add.length); // 2
459
460const memoized = memoizeOne(add);
461
462console.log(memoized.length); // 0
463```
464
465There is no (great) way to correctly set the `.length` property of the memoized function while also supporting ie11. Once we [remove ie11 support](https://github.com/alexreardon/memoize-one/issues/125) then we will set the `.length` property of the memoized function to match the original function
466
467[→ discussion](https://github.com/alexreardon/memoize-one/pull/124).
468
469## Memoized function `type`
470
471The resulting function you get back from `memoize-one` has *almost* the same `type` as the function that you are memoizing
472
473```ts
474declare type MemoizedFn<TFunc extends (this: any, ...args: any[]) => any> = {
475 clear: () => void;
476 (this: ThisParameterType<TFunc>, ...args: Parameters<TFunc>): ReturnType<TFunc>;
477};
478```
479
480- the same call signature as the function being memoized
481- a `.clear()` function property added
482- other function object properties on `TFunc` as not carried over
483
484You are welcome to use the `MemoizedFn` generic directly from `memoize-one` if you like:
485
486```ts
487import memoize, { MemoizedFn } from 'memoize-one';
488import isDeepEqual from 'lodash.isequal';
489import { expectTypeOf } from 'expect-type';
490
491// Takes any function: TFunc, and returns a Memoized<TFunc>
492function withDeepEqual<TFunc extends (...args: any[]) => any>(fn: TFunc): MemoizedFn<TFunc> {
493 return memoize(fn, isDeepEqual);
494}
495
496function add(first: number, second: number): number {
497 return first + second;
498}
499
500const memoized = withDeepEqual(add);
501
502expectTypeOf<typeof memoized>().toEqualTypeOf<MemoizedFn<typeof add>>();
503```
504
505In this specific example, this type would have been correctly inferred too
506
507```ts
508import memoize, { MemoizedFn } from 'memoize-one';
509import isDeepEqual from 'lodash.isequal';
510import { expectTypeOf } from 'expect-type';
511
512// return type of MemoizedFn<TFunc> is inferred
513function withDeepEqual<TFunc extends (...args: any[]) => any>(fn: TFunc) {
514 return memoize(fn, isDeepEqual);
515}
516
517function add(first: number, second: number): number {
518 return first + second;
519}
520
521const memoized = withDeepEqual(add);
522
523// type test still passes
524expectTypeOf<typeof memoized>().toEqualTypeOf<MemoizedFn<typeof add>>();
525```
526
527## Performance 🚀
528
529### Tiny
530
531`memoize-one` is super lightweight at [![min](https://img.shields.io/bundlephobia/min/memoize-one.svg?label=)](https://www.npmjs.com/package/memoize-one) minified and [![minzip](https://img.shields.io/bundlephobia/minzip/memoize-one.svg?label=)](https://www.npmjs.com/package/memoize-one) gzipped. (`1KB` = `1,024 Bytes`)
532
533### Extremely fast
534
535`memoize-one` performs better or on par with than other popular memoization libraries for the purpose of remembering the latest invocation.
536
537The comparisons are not exhaustive and are primarily to show that `memoize-one` accomplishes remembering the latest invocation really fast. There is variability between runs. The benchmarks do not take into account the differences in feature sets, library sizes, parse time, and so on.
538
539<details>
540 <summary>Expand for results</summary>
541 <p>
542
543node version `16.11.1`
544
545You can run this test in the repo by:
546
5471. Add `"type": "module"` to the `package.json` (why is things so hard)
5482. Run `yarn perf:library-comparison`
549
550**no arguments**
551
552| Position | Library | Operations per second |
553| -------- | -------------------------------------------- | --------------------- |
554| 1 | memoize-one | 80,112,981 |
555| 2 | moize | 72,885,631 |
556| 3 | memoizee | 35,550,009 |
557| 4 | mem (JSON.stringify strategy) | 4,610,532 |
558| 5 | lodash.memoize (JSON.stringify key resolver) | 3,708,945 |
559| 6 | no memoization | 505 |
560| 7 | fast-memoize | 504 |
561
562**single primitive argument**
563
564| Position | Library | Operations per second |
565| -------- | -------------------------------------------- | --------------------- |
566| 1 | fast-memoize | 45,482,711 |
567| 2 | moize | 34,810,659 |
568| 3 | memoize-one | 29,030,828 |
569| 4 | memoizee | 23,467,065 |
570| 5 | mem (JSON.stringify strategy) | 3,985,223 |
571| 6 | lodash.memoize (JSON.stringify key resolver) | 3,369,297 |
572| 7 | no memoization | 507 |
573
574**single complex argument**
575
576| Position | Library | Operations per second |
577| -------- | -------------------------------------------- | --------------------- |
578| 1 | moize | 27,660,856 |
579| 2 | memoize-one | 22,407,916 |
580| 3 | memoizee | 19,546,835 |
581| 4 | mem (JSON.stringify strategy) | 2,068,038 |
582| 5 | lodash.memoize (JSON.stringify key resolver) | 1,911,335 |
583| 6 | fast-memoize | 1,633,855 |
584| 7 | no memoization | 504 |
585
586**multiple primitive arguments**
587
588| Position | Library | Operations per second |
589| -------- | -------------------------------------------- | --------------------- |
590| 1 | moize | 22,366,497 |
591| 2 | memoize-one | 17,241,995 |
592| 3 | memoizee | 9,789,442 |
593| 4 | mem (JSON.stringify strategy) | 3,065,328 |
594| 5 | lodash.memoize (JSON.stringify key resolver) | 2,663,599 |
595| 6 | fast-memoize | 1,219,548 |
596| 7 | no memoization | 504 |
597
598**multiple complex arguments**
599
600| Position | Library | Operations per second |
601| -------- | -------------------------------------------- | --------------------- |
602| 1 | moize | 21,788,081 |
603| 2 | memoize-one | 17,321,248 |
604| 3 | memoizee | 9,595,420 |
605| 4 | lodash.memoize (JSON.stringify key resolver) | 873,283 |
606| 5 | mem (JSON.stringify strategy) | 850,779 |
607| 6 | fast-memoize | 687,863 |
608| 7 | no memoization | 504 |
609
610**multiple complex arguments (spreading arguments)**
611
612| Position | Library | Operations per second |
613| -------- | -------------------------------------------- | --------------------- |
614| 1 | moize | 21,701,537 |
615| 2 | memoizee | 19,463,942 |
616| 3 | memoize-one | 17,027,544 |
617| 4 | lodash.memoize (JSON.stringify key resolver) | 887,816 |
618| 5 | mem (JSON.stringify strategy) | 849,244 |
619| 6 | fast-memoize | 691,512 |
620| 7 | no memoization | 504 |
621
622 </p>
623</details>
624
625## Code health 👍
626
627- Tested with all built in [JavaScript types](https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/types%20%26%20grammar/ch1.md)
628- Written in `Typescript`
629- Correct typing for `Typescript` and `flow` type systems
630- No dependencies