-
Notifications
You must be signed in to change notification settings - Fork 92
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
base: master
Are you sure you want to change the base?
Changes from 21 commits
9d528b2
c95a94c
5d792bd
ad7faa9
0054efa
ed4d919
a10ecee
f5dfafa
2fc981a
078f3a1
f0f898d
b9347e0
daa1f6e
a458b1d
fc2dd98
e253c3a
8cf0cd1
afe9ffd
6e6b7c9
09c81e6
22e5240
82b423f
dfd6913
216a58a
6cda934
2c82fc9
ce8ff2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
); | ||
}; | ||
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 | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function reference and the arguments description are missing the |
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`', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
}); | ||
}); | ||
}); |
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(); | ||
}); | ||
}); |
There was a problem hiding this comment.
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, nowgetSummary
anddependencyListMapper
are not visible in the example code, which prevented me from understanding the example when reading through Storybook.Lastly,
getSummary
should be renamed tosum
, if you intend to keep it.