Skip to content

Commit

Permalink
new api (#52)
Browse files Browse the repository at this point in the history
Closes #47 
Closes #39 

Biting the bullet and changing the api for super flexibility
  • Loading branch information
alexreardon authored Dec 17, 2018
1 parent b8d0413 commit 9386b93
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 312 deletions.
165 changes: 28 additions & 137 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,166 +68,57 @@ const memoizeOne = require('memoize-one').default;

## Custom equality function

The default equality function is a simple shallow equal check
You can also pass in a custom function for checking the equality of two sets of arguments

```js
const simpleIsEqual: EqualityFn = (a: mixed, b: mixed): boolean => a === b;
const memoized = memoizeOne(fn, isEqual);
type EqualityFn = (newArgs: mixed[], oldArgs: mixed[]) => boolean;
```

You can also pass in a custom function for checking the equality of two items.
An equality function should return `true` if the arguments are equal. If `true` is returned then the wrapped function will not be called.

The default equality function is a shallow equal check of all arguments (each argument is compared with `===`). The default equality function also does not check anything if the length of the arguments changes. You are welcome to decide if you want to return `false` if the `length` of the arguments is not equal

```js
type EqualityFn = (newValue: mixed, oldValue: mixed) => boolean;
const simpleIsEqual: EqualityFn = (
newArgs: mixed[],
lastArgs: mixed[],
): boolean =>
newArgs.length === lastArgs.length &&
newArgs.every(
(newArg: mixed, index: number): boolean =>
shallowEqual(newArg, lastArgs[index]),
);
```

> I am considering changing the custom equality api to `(newArgs: mixed[], lastArgs: mixed[], lastResult: T)`
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`.

Equality functions are not called if the `this` context of the function has changed (see below).

Here is an example that uses a `lodash.isequal` deep equal equality check

Here is an example that uses a deep equal check
> `lodash.isequal` correctly handles deep comparing two arrays
```js
import memoizeOne from 'memoize-one';
import deepEqual from 'lodash.isEqual';
import isDeepEqual from 'lodash.isequal';

const identity = x => x;

const defaultMemoization = memoizeOne(identity);
const customMemoization = memoizeOne(identity, deepEqual);
const shallowMemoized = memoizeOne(identity);
const deepMemoized = memoizeOne(identity, isDeepEqual);

const result1 = defaultMemoization({ foo: 'bar' });
const result2 = defaultMemoization({ foo: 'bar' });
const result1 = shallowMemoized({ foo: 'bar' });
const result2 = shallowMemoized({ foo: 'bar' });

result1 === result2; // false - difference reference

const result3 = customMemoization({ foo: 'bar' });
const result4 = customMemoization({ foo: 'bar' });
const result3 = deepMemoized({ foo: 'bar' });
const result4 = deepMemoized({ foo: 'bar' });

result3 === result4; // true - arguments are deep equal
```

### Custom equality function behaviour

- The equality function is only called if the `this` context of the function has not changed, and the `length` of the arguments has not changed.
- The equality function is used to compare the value of every individual argument by index.

Memoized function calls

- First call: `memoized(0, 1)`
- arguments: `0, 1`

- Second call: `memoized(0, 2)`
- arguments: `0, 2`

Equality function calls:

- First call: `isEqual(0, 0)` `(newArg, oldArg)`
- Second call: `isEqual(1, 2)` `(newArg, oldArg)`

### Custom equality function higher order functions

> ⚠️ Generally you will not need to do this. There are some rare use cases where additional information in your custom equality function can be useful. We have optimised the custom equality function api for common use cases. There is some additional work you will need to do to unlock more advanced behaviours.
We do not provide extra details to custom equality functions such as argument `index` for [compatibility reasons](https://github.com/alexreardon/memoize-one/issues/47). However, you can add extra information yourself to your custom equality functions with a higher order function (wrapping a function in another function).

#### Example: `index`

Here is an example of a higher order function that allow you to pass an `index` to your custom equality function.

```js
// this function will do some special checking for the second argument
const customIsEqual = (
newValue: mixed,
oldValue: mixed,
index: number,
): boolean => {
if (index === 1) {
if (!isDate(newValue) || !isDate(oldValue)) {
return false;
}
return newValue.getTime() === oldValue.getTime();
}

return newValue === oldValue;
};

const getMemoizedWithIndex = (fn: Function) => {
let argIndex: number = 0;
const withIndex = (newValue: mixed, oldValue: mixed) =>
customIsEqual(newValue, oldValue, argIndex++);
const memoized = memoizeOne(fn, withIndex);

// every time this function is called it will reset our argIndex
return (...args: mixed[]) => {
argIndex = 0;
return memoized(...args);
};
};

const memoized = getMemoizedWithIndex(myFunc);
```

#### Example: equality function per argument

A nice pattern is to create a helper that applies different equality functions for the different arguments. Thanks [@Noviny!](https://github.com/Noviny)

```js
const getMemoized = (fn: Function, equalityChecks: EqualityFn[]) => {
const isEqual = (a: mixed, b: mixed, index: number) => {
return equalityChecks[index](a, b);
};

let argIndex: number = 0;
const withIndex = (newValue: mixed, oldValue: mixed) =>
isEqual(newValue, oldValue, argIndex++);
const memoized = memoizeOne(fn, withIndex);

// every time this function is called it will reset our argIndex
return (...args: mixed[]) => {
argIndex = 0;
return memoized(...args);
};
};

const memoized = getMemoized(myFunc, [isShallowEqual, isDateEqual, isDeepEqual]);

memoized(1, new Date(2019, 1), {foo: 'bar' });
memoized(1, new Date(2019, 1), {foo: 'bar' });
```

#### Example: all `arguments`

Here is an example of a higher order function that allow you to pass a `index` and `arguments` to your custom equality function.

```js
// using this to only memoize calls with 3+ arguments
const customIsEqual = (
newValue: mixed,
oldValue: mixed,
index: number,
args: mixed[],
): boolean => {
if (args.length < 3) {
return false;
}
return newValue === oldValue;
};
const getMemoizedFn = (fn: Function) => {
let args: mixed[] = [];
let argIndex: number = 0;
const withIndex = (newValue: mixed, oldValue: mixed) =>
customIsEqual(newValue, oldValue, argIndex++, args);
const memoized = memoizeOne(fn, withIndex);

// every time this function is called it will reset our args and argIndex
return (...newArgs: mixed[]) => {
args = newArgs;
argIndex = 0;
return memoized(...newArgs);
};
};

const memoized = getMemoizedFn(myFunc);
```

## `this`

### `memoize-one` correctly respects `this` control
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"eslint-plugin-prettier": "^3.0.0",
"flow-bin": "0.89.0",
"jest": "^23.6.0",
"lodash.isequal": "^4.5.0",
"prettier": "1.15.3",
"rimraf": "2.6.2",
"rollup": "^0.68.0",
Expand Down
24 changes: 13 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// @flow
export type EqualityFn = (newValue: mixed, oldValue: mixed) => boolean;
export type EqualityFn = (newArgs: mixed[], lastArgs: mixed[]) => boolean;

const simpleIsEqual: EqualityFn = (newValue: mixed, oldValue: mixed): boolean =>
const shallowEqual = (newValue: mixed, oldValue: mixed): boolean =>
newValue === oldValue;

const simpleIsEqual: EqualityFn = (
newArgs: mixed[],
lastArgs: mixed[],
): boolean =>
newArgs.length === lastArgs.length &&
newArgs.every(
(newArg: mixed, index: number): boolean =>
shallowEqual(newArg, lastArgs[index]),
);

// <ResultFn: (...Array<any>) => mixed>
// The purpose of this typing is to ensure that the returned memoized
// function has the same type as the provided function (`resultFn`).
Expand All @@ -19,17 +29,9 @@ export default function<ResultFn: (...Array<any>) => mixed>(
let lastResult: mixed;
let calledOnce: boolean = false;

const isNewArgEqualToLast = (newArg: mixed, index: number): boolean =>
isEqual(newArg, lastArgs[index]);

// breaking cache when context (this) or arguments change
const result = function(...newArgs: Array<mixed>) {
if (
calledOnce &&
lastThis === this &&
newArgs.length === lastArgs.length &&
newArgs.every(isNewArgEqualToLast)
) {
if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
return lastResult;
}

Expand Down
Loading

0 comments on commit 9386b93

Please sign in to comment.