Skip to content

Commit 7105eff

Browse files
authored
Adds support for custom equality checks on unwrap (#123)
1 parent ef76953 commit 7105eff

File tree

2 files changed

+74
-13
lines changed

2 files changed

+74
-13
lines changed

packages/@react-facet/core/src/hooks/useFacetUnwrap.spec.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,45 @@ it('does not trigger a re-render when changing a facet from undefined to undefin
217217

218218
expect(renderedMock).toHaveBeenCalledTimes(0)
219219
})
220+
221+
it('supports custom equality checks', () => {
222+
const value = {}
223+
const demoFacet = createFacet({ initialValue: value })
224+
225+
// Dummy equality check that always returns its not equal
226+
const check = jest.fn().mockReturnValue(false)
227+
const equalityCheck = jest.fn().mockReturnValue(check)
228+
229+
const renderedMock = jest.fn()
230+
231+
const ComponentWithFacetEffect = () => {
232+
useFacetUnwrap(demoFacet, equalityCheck)
233+
renderedMock()
234+
return null
235+
}
236+
237+
render(<ComponentWithFacetEffect />)
238+
239+
// initialize equality checks once
240+
expect(equalityCheck).toHaveBeenCalledTimes(1)
241+
242+
// but check for it twice, once upon initialization, then another on the first observed value
243+
expect(check).toHaveBeenCalledTimes(2)
244+
expect(check).toHaveBeenNthCalledWith(1, value)
245+
expect(check).toHaveBeenNthCalledWith(2, value)
246+
247+
// as the custom equality check always returns false, we render twice on mount
248+
expect(renderedMock).toHaveBeenCalledTimes(2)
249+
250+
jest.clearAllMocks()
251+
252+
// If we update with the same object,
253+
act(() => {
254+
demoFacet.set(value)
255+
})
256+
257+
expect(equalityCheck).toHaveBeenCalledTimes(0) // equality check was already initialized
258+
expect(check).toHaveBeenCalledTimes(1) // but the check should be executed
259+
expect(check).toHaveBeenCalledWith(value) // passing the value (which should be the same)
260+
expect(renderedMock).toHaveBeenCalledTimes(1) // and since the equality check always returns "false", we have a render
261+
})
Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { useLayoutEffect, useState } from 'react'
2-
import { FacetProp, isFacet, Value, NoValue } from '../types'
2+
import { FacetProp, isFacet, Value, NoValue, EqualityCheck, NO_VALUE } from '../types'
3+
import { defaultEqualityCheck } from '../equalityChecks'
34

45
/**
56
* Hook that allows consuming values from a Facet
67
* It acts as a regular react state, triggering a re-render of the component
78
*
89
* @param facet
910
*/
10-
export function useFacetUnwrap<T extends Value>(prop: FacetProp<T>): T | NoValue {
11+
export function useFacetUnwrap<T extends Value>(
12+
prop: FacetProp<T>,
13+
equalityCheck: EqualityCheck<T> = defaultEqualityCheck,
14+
): T | NoValue {
1115
const [state, setState] = useState<{ value: T | NoValue }>(() => {
1216
if (!isFacet(prop)) return { value: prop }
1317

@@ -18,12 +22,17 @@ export function useFacetUnwrap<T extends Value>(prop: FacetProp<T>): T | NoValue
1822

1923
useLayoutEffect(() => {
2024
if (isFacet(prop)) {
25+
// Initialize the equalityCheck
26+
const isEqual = equalityCheck()
27+
const startValue = prop.get()
28+
if (startValue !== NO_VALUE) {
29+
isEqual(startValue)
30+
}
31+
2132
return prop.observe((value) => {
2233
setState((previousState) => {
2334
const { value: previousValue } = previousState
2435

25-
const typeofValue = typeof previousValue
26-
2736
/**
2837
* Performs this equality check locally to prevent triggering two consecutive renderings
2938
* for facets that have immutable values (unfortunately we can't handle mutable values).
@@ -34,22 +43,32 @@ export function useFacetUnwrap<T extends Value>(prop: FacetProp<T>): T | NoValue
3443
* - Once on initialization of the useState above
3544
* - And another time on this observe initialization
3645
*/
37-
if (
38-
(typeofValue === 'number' ||
39-
typeofValue === 'string' ||
40-
typeofValue === 'boolean' ||
41-
value === undefined ||
42-
value === null) &&
43-
value === previousValue
44-
) {
46+
if (equalityCheck === defaultEqualityCheck) {
47+
const typeofValue = typeof previousValue
48+
49+
if (
50+
(typeofValue === 'number' ||
51+
typeofValue === 'string' ||
52+
typeofValue === 'boolean' ||
53+
value === undefined ||
54+
value === null) &&
55+
value === previousValue
56+
) {
57+
return previousState
58+
}
59+
60+
return { value }
61+
}
62+
63+
if (previousValue !== NO_VALUE && isEqual(previousValue)) {
4564
return previousState
4665
}
4766

4867
return { value }
4968
})
5069
})
5170
}
52-
}, [prop])
71+
}, [prop, equalityCheck])
5372

5473
return isFacet(prop) ? state.value : prop
5574
}

0 commit comments

Comments
 (0)