Skip to content

Commit

Permalink
Merge pull request #1 from mblink/ht-useStableCallback-useStableMemo
Browse files Browse the repository at this point in the history
useStableCallback and useStableMemo
  • Loading branch information
jleider authored Mar 19, 2021
2 parents 1f37692 + 18a925a commit 8ed579e
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 33 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# fp-ts-react-stable-hooks
Stable hooks for react using FP-TS equality checks instead of shallow (reference) object comparison.

By deafult React will perform a JavaScript object reference comparison of two objects, otherwise known as shallow object comparison, which results in extra re-renders on “unchanged” values for fp-ts data types.
By default React will perform a JavaScript object reference comparison of two objects, otherwise known as shallow object comparison, which results in extra re-renders on “unchanged” values for fp-ts data types.

For example: Take an fp-ts type such as `Option` who’s underlying data structure is is `{_tag: "Some", value: 1}`. Compared with another `Option` who's value is also `{_tag: "Some", value: 1}`, will be considered a different object with JavaScript object reference comparison since `O.some(1) !== O.some(1)`. However, an equality function can dive down into the underlying `value` type and prove its equality. Given that, an equality function such as `O.getEq(Eq.eqNumber)` can prove that `O.getEq(Eq.eqNumber).equals(O.some(1), O.some(1)) === true`. Using these stable hooks instead of the basic react hooks will result in fewer unnecessary re-renders when using fp-ts data types.
For example: Take an fp-ts type such as `Option` who’s underlying data structure is is `{_tag: "Some", value: 1}`. Compared with another `Option` who's value is also `{_tag: "Some", value: 1}`, will be considered a different object with JavaScript object reference comparison since `O.some(1) !== O.some(1)`. However, an equality function can dive down into the underlying `value` type and prove its equality. Given that, an equality function such as `O.getEq(Eq.eqNumber)` can prove that `O.getEq(Eq.eqNumber).equals(O.some(1), O.some(1)) === true`. Using these stable hooks instead of the basic react hooks will result in fewer unnecessary re-renders when using fp-ts data types.

## Installation

Expand All @@ -19,7 +19,7 @@ import * as Eq from "fp-ts/Eq";
import * as O from "fp-ts/Option";
import { useStableO } from "fp-ts-react-stable-hooks";

// Equality function defaults to Eq.eqStrict so there is no need to provide
// Equality function defaults to Eq.eqStrict so there is no need to provide
// it for primitive data types such as string, number, or boolean
const [data, setData] = useStableO(O.some("foobar"));
```
Expand All @@ -31,7 +31,7 @@ import * as O from "fp-ts/Option";
import { useStable } from "fp-ts-react-stable-hooks";

const [data, setData] = useStable(
O.some({foo: "oof", bar: 1}),
O.some({foo: "oof", bar: 1}),
O.getEq(Eq.eqStruct({foo: Eq.eqString, bar: Eq.eqNumber}))
);
```
Expand All @@ -54,16 +54,18 @@ useStableEffect(() => {
## API

| React Hook | Stable Hook | Description |
|------------|-------------|-------------|
| useState | useStable | Base hook that requires an equality function |
| | useStableE | Helper function which automatically proves the top level equality function for `Either` |
| | useStableO | Helper function which automatically proves the top level equality function for `Option` |
| useEffect | useStableEffect | Base hook that requires an equality function |
|-------------|-------------------|-------------|
| useState | useStable | Base hook that requires an equality function |
| | useStableE | Helper function which automatically proves the top level equality function for `Either` |
| | useStableO | Helper function which automatically proves the top level equality function for `Option` |
| useEffect | useStableEffect | Base hook that requires an equality function |
| useCallback | useStableCallback | Base hook that requires an equality function |
| useMemo | useStableMemo | Base hook that requires an equality function |

## React Hooks Linter
If you already use the recommended react hooks lint rule you can add this to your `eslint` file.
```typescript
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "(useStableEffect)"
"additionalHooks": "(useStableEffect,useStableCallback,useStableMemo)"
}]
```
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fp-ts-react-stable-hooks",
"version": "1.0.3",
"version": "1.1.0",
"description": "Stable hooks for react using FP-TS equality checks instead of shallow (reference) object comparison",
"main": "dist/lib/index.js",
"module": "dist/es2015/index.js",
Expand Down
11 changes: 11 additions & 0 deletions src/callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Eq from 'fp-ts/Eq';
import { useCallback } from 'react';
import { useEqMemoize } from './useEqMemoize';

export const useStableCallback = <A extends ReadonlyArray<unknown>, T extends (...args: any[]) => any>(
callback: T,
dependencies: A,
eq: Eq.Eq<A>
): T => {
return useCallback(callback, useEqMemoize(dependencies, eq));
};
20 changes: 2 additions & 18 deletions src/effect.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
import * as Eq from 'fp-ts/Eq';
import { EffectCallback, useEffect, useRef } from 'react';

// Use effect prior art comes from https://github.com/kentcdodds/use-deep-compare-effect/blob/master/src/index.ts
const useEqMemoize = <A extends ReadonlyArray<unknown>>(value: A, eq: Eq.Eq<A>) => {
const ref = useRef<A>();
const signalRef = useRef<number>(0);

if (ref.current == null) {
ref.current = value;
}

if (!eq.equals(value, ref.current)) {
ref.current = value;
signalRef.current += 1;
}

return [signalRef.current];
};
import { EffectCallback, useEffect } from 'react';
import { useEqMemoize } from './useEqMemoize';

export const useStableEffect = <A extends ReadonlyArray<unknown>>(
callback: EffectCallback,
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { useStable, useStableE, useStableO } from './state';
export { useStableEffect } from './effect';
export { useStableEffect } from './effect';
export { useStableCallback } from './callback';
export { useStableMemo } from './memo';
11 changes: 11 additions & 0 deletions src/memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Eq from 'fp-ts/Eq';
import { useEqMemoize } from './useEqMemoize';
import { useMemo } from 'react';

export const useStableMemo = <A extends ReadonlyArray<unknown>, T>(
factory: () => T,
dependencies: A,
eq: Eq.Eq<A>
): T => {
return useMemo(factory, useEqMemoize(dependencies, eq));
};
19 changes: 19 additions & 0 deletions src/useEqMemoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as Eq from 'fp-ts/Eq';
import { useRef } from 'react';

// Use effect prior art comes from https://github.com/kentcdodds/use-deep-compare-effect/blob/master/src/index.ts
export const useEqMemoize = <A extends ReadonlyArray<unknown>>(value: A, eq: Eq.Eq<A>) => {
const ref = useRef<A>();
const signalRef = useRef<number>(0);

if (ref.current == null) {
ref.current = value;
}

if (!eq.equals(value, ref.current)) {
ref.current = value;
signalRef.current += 1;
}

return [signalRef.current];
};
30 changes: 30 additions & 0 deletions test/callback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as Eq from 'fp-ts/Eq';
import * as O from 'fp-ts/Option';
import { renderHook } from '@testing-library/react-hooks';
import { useStableCallback } from '../src/index';

const o1a = O.some(1);
const o1b = O.some(1);
const o2 = O.some(2);
const nEq = Eq.getTupleEq(O.getEq(Eq.eqNumber));

describe('useStableCallback', () => {
test('should not return a new function if the values are the same', () => {
let o = o1a;
const { result, rerender } = renderHook(() => useStableCallback(() => o, [o], nEq));
o = o1b;
rerender();
expect(result.all[0]).toStrictEqual(result.all[1]);
expect(result.current()).toStrictEqual(o1a);
});

test('should return a new function if the values are different', () => {
let o = o1a;
const { result, rerender } = renderHook(() => useStableCallback((a: number) => O.getOrElse(() => 0)(o) + a, [o], nEq));
expect(result.current(1)).toStrictEqual(2);
o = o2;
rerender();
expect(result.all[0]).not.toStrictEqual(result.all[1]);
expect(result.current(1)).toStrictEqual(3);
});
});
2 changes: 1 addition & 1 deletion test/effect.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Eq from 'fp-ts/Eq';
import * as O from 'fp-ts/Option';
import { renderHook } from '@testing-library/react-hooks';
import { useStableEffect } from '../src/effect';
import { useStableEffect } from '../src/index';

const o1a = O.some(1);
const o1b = O.some(1);
Expand Down
27 changes: 27 additions & 0 deletions test/memo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as Eq from 'fp-ts/Eq';
import * as O from 'fp-ts/Option';
import { renderHook } from '@testing-library/react-hooks';
import { useStableMemo } from '../src/index';

const o1a = O.some(1);
const o1b = O.some(1);
const o2 = O.some(2);
const nEq = Eq.getTupleEq(O.getEq(Eq.eqNumber));

describe('useStableMemo', () => {
test('should not return a value if the values are the same', () => {
let o = o1a;
const { result, rerender } = renderHook(() => useStableMemo(() => o, [o], nEq));
o = o1b;
rerender();
expect(result.current).toStrictEqual(o1a);
});

test('should return a new value if the values are different', () => {
let o = o1a;
const { result, rerender } = renderHook(() => useStableMemo(() => o, [o], nEq));
o = o2;
rerender();
expect(result.all[0]).not.toStrictEqual(result.all[1]);
});
});
2 changes: 1 addition & 1 deletion test/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as E from 'fp-ts/Either';
import * as Eq from 'fp-ts/Eq';
import * as O from 'fp-ts/Option';
import { act, renderHook } from '@testing-library/react-hooks';
import { useStable, useStableE, useStableO } from '../src/state';
import { useStable, useStableE, useStableO } from '../src/index';
import { Dispatch } from 'react';

const o1a = O.some(1);
Expand Down

0 comments on commit 8ed579e

Please sign in to comment.