Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useMemoCache): useMemo with cache #1063

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9d528b2
feat: add useMemoCache hook
michaltarasiuk Jan 2, 2023
c95a94c
feat: add example
michaltarasiuk Jan 2, 2023
5d792bd
fix: example of useMemoCache hook
michaltarasiuk Jan 2, 2023
ad7faa9
fix: performence
michaltarasiuk Jan 3, 2023
0054efa
fix: cached item from Array to Set
michaltarasiuk Jan 3, 2023
ed4d919
feat: add test cases for areHookInputsEqual
michaltarasiuk Jan 3, 2023
a10ecee
feat: set downlevelIteration to true
michaltarasiuk Jan 3, 2023
f5dfafa
feat: add custom areHookInputsEqual
michaltarasiuk Jan 3, 2023
2fc981a
fix: deps of cache memo
michaltarasiuk Jan 3, 2023
078f3a1
fix: reference of customAreHookInputsEqual
michaltarasiuk Jan 3, 2023
f0f898d
fix: remove export of objectIs
michaltarasiuk Jan 3, 2023
b9347e0
feat: resolve conflicts
michaltarasiuk Jan 4, 2023
daa1f6e
feat: cover objectIs helper by test cases
michaltarasiuk Jan 4, 2023
a458b1d
feat: cover is helper by test cases
michaltarasiuk Jan 4, 2023
fc2dd98
fix: unstable refference of customAreHookInputsEqual
michaltarasiuk Jan 4, 2023
e253c3a
fix: docs of useMemoCache
michaltarasiuk Jan 4, 2023
8cf0cd1
feat: resolve conflicts
michaltarasiuk Jan 8, 2023
afe9ffd
feat: remove dist
michaltarasiuk Jan 8, 2023
6e6b7c9
feat: adapt to requirements
michaltarasiuk Jan 8, 2023
09c81e6
feat: add assertion before add to cache
michaltarasiuk Jan 8, 2023
22e5240
fix: naming
michaltarasiuk Jan 8, 2023
82b423f
fix: code review fix
michalt-monogo Jan 17, 2023
dfd6913
feat: useMemo to useCustomCompareMemo
michalt-monogo Jan 17, 2023
216a58a
feat: simplify cache
michalt-monogo Jan 17, 2023
6cda934
feat: add max entries logic
michalt-monogo Jan 18, 2023
2c82fc9
fix: index counter
michalt-monogo Jan 19, 2023
ce8ff2f
feat: simplified cache
michalt-monogo Jan 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ Coming from `react-use`? Check out our

- #### Miscellaneous

- [**`useMemoCache`**](https://react-hookz.github.io/web/?path=/docs/miscellaneous-useMemoCache--example)
— Like useMemo but with cache based on dependency list.
- [**`useSyncedRef`**](https://react-hookz.github.io/web/?path=/docs/miscellaneous-usesyncedref--example)
— Like `useRef`, but it returns an immutable ref that contains the actual value.
- [**`useCustomCompareMemo`**](https://react-hookz.github.io/web/?path=/docs/miscellaneous-useCustomCompareMemo--example)
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export * from './useVibrate';
export * from './useSyncedRef';
export * from './useHookableRef';
export * from './useCustomCompareMemo';
export * from './useMemoCache';

// SideEffect
export * from './useLocalStorageValue';
Expand All @@ -75,6 +76,7 @@ export * from './useWindowSize';
export { isStrictEqual, truthyAndArrayPredicate, truthyOrArrayPredicate } from './util/const';
export { EffectCallback, EffectHook } from './util/misc';
export { resolveHookState } from './util/resolveHookState';
export { is, objectIs, areHookInputsEqual } from './util/areHookInputsEqual';

// Types
export * from './types';
57 changes: 57 additions & 0 deletions src/useMemoCache/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useMemo, useRef, useState } from 'react';
import { useMemoCache } from '../..';

const dependencyListMapper = {
true: [1, 3],
false: [2, 4],
};

const getSummary = (numbers: number[]) => numbers.reduce((a, b) => a + b);

const boolToString = (bool: boolean) => String(bool) as 'true' | 'false';

export const Example: React.FC = () => {
const isTruthy = useRef(false);

const [dependencyList, setDependencyList] = useState(
() => dependencyListMapper[boolToString(isTruthy.current)]
);

const memoSpy = useRef(0);
const memoCacheSpy = useRef(0);

const memo = useMemo(() => {
memoSpy.current++;

return getSummary(dependencyList);
}, dependencyList);

const memoCache = useMemoCache(() => {
memoCacheSpy.current++;

return getSummary(dependencyList);
}, dependencyList);

const toggleDependencyList = () => {
isTruthy.current = !isTruthy.current;

setDependencyList(dependencyListMapper[boolToString(isTruthy.current)]);
};

return (
<div>
<section>
<h1>Memo</h1>
<p>summary: {memo}</p>
<p>calls: {memoSpy.current}</p>
</section>
<section>
<h1>Memo with cache</h1>
<p>summary: {memoCache}</p>
<p>calls: {memoCacheSpy.current}</p>
</section>
<button onClick={toggleDependencyList}>toggle dependency list</button>
</div>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example is good, but I think the summary value just being toggled between 4 and 6 is really confusing. I think the example needs to do something a bit more realistic to really illustrate the functionality of the hook. Maybe you could do something like let the user input a number and then count all Fibonacci numbers until that point while displaying the number of calls made to the factory functions below.

Also, make sure that the example is understandable by using the example program and reading the example code. Storybook doesn't show all code that you have in the example.stories.tsx file. For example, now getSummary and dependencyListMapper are not visible in the example code, which prevented me from understanding the example when reading through Storybook.

Lastly, getSummary should be renamed to sum, if you intend to keep it.

28 changes: 28 additions & 0 deletions src/useMemoCache/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './example.stories';
import { ImportPath } from '../../__docs__/ImportPath';

<Meta title="Miscellaneous/useMemoCache" component={Example} />

# useMemoCache

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hook description missing here. You have it in other places and it's good.

#### Example

<Canvas>
<Story story={Example} inline />
</Canvas>

## Reference

```ts
function useMemoCache<State, Deps extends DependencyList>(factory: () => State, deps: Deps): State;
```

#### Importing

<ImportPath />

#### Arguments

- **factory** _`() => unknown`_ - useMemo factory function.
- **deps** _`DependencyList`_ - useMemo dependency list.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function reference and the arguments description are missing the customAreHookInputsEqual argument.

209 changes: 209 additions & 0 deletions src/useMemoCache/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { DependencyList } from 'react';
import { useMemoCache, createMemoCache } from '../..';

describe('useMemoCache', () => {
it('should be defined', () => {
expect(useMemoCache).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useMemoCache(() => 10, []));
expect(result.error).toBeUndefined();
});

it('should return resolved state', () => {
const resolved = 'Hello World';
const lazyInitialize = () => resolved;

const {
result: { current },
} = renderHook(() => useMemoCache(lazyInitialize, []));

expect(current).toBe(resolved);
});

it('should not invoke factory when state is cached', () => {
const spy = jest.fn();
const { result, rerender } = renderHook(
({ dependencyList }) => {
return useMemoCache(() => {
spy();

return dependencyList.reduce((a, b) => a + b);
}, dependencyList);
},
{ initialProps: { dependencyList: [1, 2] } }
);

expect(result.current).toBe(3);
expect(spy).toHaveBeenCalledTimes(1);

rerender({ dependencyList: [3, 4] });

expect(result.current).toBe(7);
expect(spy).toHaveBeenCalledTimes(2);

rerender({ dependencyList: [1, 2] });

expect(result.current).toBe(3);
expect(spy).toHaveBeenCalledTimes(2);
});

it('should invoke when state is not cached (reference case)', () => {
const spy = jest.fn();
const { result, rerender } = renderHook(
({ dependencyList }) => {
return useMemoCache(() => {
spy();

return Object.values(dependencyList[0]);
}, dependencyList);
},
{ initialProps: { dependencyList: [{ a: 1, b: 2 }] } }
);

expect(result.current).toEqual([1, 2]);
expect(spy).toHaveBeenCalledTimes(1);

rerender({ dependencyList: [{ a: 1, b: 2 }] });

expect(result.current).toEqual([1, 2]);
expect(spy).toHaveBeenCalledTimes(2);

rerender({ dependencyList: [{ a: 2, b: 3 }] });

expect(result.current).toEqual([2, 3]);
expect(spy).toHaveBeenCalledTimes(3);
});

it('should work with custom `areHookInputsEqual`', () => {
const spy = jest.fn();
const customAreHookInputsEqual = (nextDeps: DependencyList, prevDeps: DependencyList | null) =>
JSON.stringify(nextDeps) === JSON.stringify(prevDeps);

const { result, rerender } = renderHook(
({ dependencyList }) => {
return useMemoCache(
() => {
spy();

return Object.values(dependencyList[0]);
},
dependencyList,
customAreHookInputsEqual
);
},
{ initialProps: { dependencyList: [{ a: 1, b: 2 }] } }
);

expect(result.current).toEqual([1, 2]);
expect(spy).toHaveBeenCalledTimes(1);

rerender({ dependencyList: [{ a: 1, b: 2 }] });

expect(result.current).toEqual([1, 2]);
expect(spy).toHaveBeenCalledTimes(1);

rerender({ dependencyList: [{ a: 2, b: 3 }] });

expect(result.current).toEqual([2, 3]);
expect(spy).toHaveBeenCalledTimes(2);
});

it('should handle unstable refference of `areHookInputsEqual`', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reference

const spy = jest.fn();
const initialDependencyList = [{ a: 1, b: 2 }];

const { result, rerender } = renderHook(
({ dependencyList }) => {
return useMemoCache(
() => {
spy();

return Object.values(dependencyList[0]);
},
dependencyList,
(nextDeps: DependencyList, prevDeps: DependencyList | null) =>
JSON.stringify(nextDeps) === JSON.stringify(prevDeps)
);
},
{ initialProps: { dependencyList: initialDependencyList } }
);

expect(result.current).toEqual([1, 2]);
expect(spy).toHaveBeenCalledTimes(1);

rerender({ dependencyList: initialDependencyList });

expect(result.current).toEqual([1, 2]);
expect(spy).toHaveBeenCalledTimes(1);
});

describe('createCache', () => {
it(`should return 'none' when there is no cached value`, () => {
const memoCache = createMemoCache();
const dependencyList = [1, 2];

const cachedValue = memoCache.get(dependencyList);

expect(memoCache.isNone(cachedValue)).toBeTruthy();
});

it('should return cached value', () => {
const memoCache = createMemoCache();

const dependencyList = [1, 2];
const value = Object.values(dependencyList);

memoCache.set(dependencyList, value);
const cachedValue = memoCache.get(dependencyList);

expect(cachedValue).toBe(value);
expect(memoCache.isNone(cachedValue)).toBeFalsy();
});

it('should compare dependency list like React', () => {
const memoCache = createMemoCache();

const dependencyList = [1, {}];
const value = Object.values(dependencyList);

memoCache.set(dependencyList, value);
const cachedValue1 = memoCache.get(dependencyList);

expect(cachedValue1).toBe(value);
expect(memoCache.isNone(cachedValue1)).toBeFalsy();

const cachedValue2 = memoCache.get([...dependencyList]);

expect(cachedValue2).toBe(value);
expect(memoCache.isNone(cachedValue2)).toBeFalsy();

const cachedValue3 = memoCache.get([1, {}]);

expect(memoCache.isNone(cachedValue3)).toBeTruthy();
});

it('should work with custom `areHookInputsEqual`', () => {
const memoCache = createMemoCache(
(nextDeps: DependencyList, prevDeps: DependencyList | null) =>
JSON.stringify(nextDeps) === JSON.stringify(prevDeps)
);

const dependencyList = [1, {}];
const value = Object.values(dependencyList);

memoCache.set(dependencyList, value);
const cachedValue1 = memoCache.get(dependencyList);

expect(cachedValue1).toBe(value);
expect(memoCache.isNone(cachedValue1)).toBeFalsy();

const cachedValue2 = memoCache.get([1, {}]);

expect(cachedValue2).toBe(value);
expect(memoCache.isNone(cachedValue1)).toBeFalsy();
});
});
});
13 changes: 13 additions & 0 deletions src/useMemoCache/__tests__/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { useMemoCache } from '../..';

describe('useMemoCache', () => {
it('should be defined', () => {
expect(useMemoCache).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useMemoCache(() => 10, []));
expect(result.error).toBeUndefined();
});
});
Loading