diff --git a/packages/@react-facet/core/src/hooks/useFacetUnwrap.spec.tsx b/packages/@react-facet/core/src/hooks/useFacetUnwrap.spec.tsx index 8975a0c1..ec734aa2 100644 --- a/packages/@react-facet/core/src/hooks/useFacetUnwrap.spec.tsx +++ b/packages/@react-facet/core/src/hooks/useFacetUnwrap.spec.tsx @@ -217,3 +217,45 @@ it('does not trigger a re-render when changing a facet from undefined to undefin expect(renderedMock).toHaveBeenCalledTimes(0) }) + +it('supports custom equality checks', () => { + const value = {} + const demoFacet = createFacet({ initialValue: value }) + + // Dummy equality check that always returns its not equal + const check = jest.fn().mockReturnValue(false) + const equalityCheck = jest.fn().mockReturnValue(check) + + const renderedMock = jest.fn() + + const ComponentWithFacetEffect = () => { + useFacetUnwrap(demoFacet, equalityCheck) + renderedMock() + return null + } + + render() + + // initialize equality checks once + expect(equalityCheck).toHaveBeenCalledTimes(1) + + // but check for it twice, once upon initialization, then another on the first observed value + expect(check).toHaveBeenCalledTimes(2) + expect(check).toHaveBeenNthCalledWith(1, value) + expect(check).toHaveBeenNthCalledWith(2, value) + + // as the custom equality check always returns false, we render twice on mount + expect(renderedMock).toHaveBeenCalledTimes(2) + + jest.clearAllMocks() + + // If we update with the same object, + act(() => { + demoFacet.set(value) + }) + + expect(equalityCheck).toHaveBeenCalledTimes(0) // equality check was already initialized + expect(check).toHaveBeenCalledTimes(1) // but the check should be executed + expect(check).toHaveBeenCalledWith(value) // passing the value (which should be the same) + expect(renderedMock).toHaveBeenCalledTimes(1) // and since the equality check always returns "false", we have a render +}) diff --git a/packages/@react-facet/core/src/hooks/useFacetUnwrap.ts b/packages/@react-facet/core/src/hooks/useFacetUnwrap.ts index 53c3ad68..c2bba377 100644 --- a/packages/@react-facet/core/src/hooks/useFacetUnwrap.ts +++ b/packages/@react-facet/core/src/hooks/useFacetUnwrap.ts @@ -1,5 +1,6 @@ import { useLayoutEffect, useState } from 'react' -import { FacetProp, isFacet, Value, NoValue } from '../types' +import { FacetProp, isFacet, Value, NoValue, EqualityCheck, NO_VALUE } from '../types' +import { defaultEqualityCheck } from '../equalityChecks' /** * Hook that allows consuming values from a Facet @@ -7,7 +8,10 @@ import { FacetProp, isFacet, Value, NoValue } from '../types' * * @param facet */ -export function useFacetUnwrap(prop: FacetProp): T | NoValue { +export function useFacetUnwrap( + prop: FacetProp, + equalityCheck: EqualityCheck = defaultEqualityCheck, +): T | NoValue { const [state, setState] = useState<{ value: T | NoValue }>(() => { if (!isFacet(prop)) return { value: prop } @@ -18,12 +22,17 @@ export function useFacetUnwrap(prop: FacetProp): T | NoValue useLayoutEffect(() => { if (isFacet(prop)) { + // Initialize the equalityCheck + const isEqual = equalityCheck() + const startValue = prop.get() + if (startValue !== NO_VALUE) { + isEqual(startValue) + } + return prop.observe((value) => { setState((previousState) => { const { value: previousValue } = previousState - const typeofValue = typeof previousValue - /** * Performs this equality check locally to prevent triggering two consecutive renderings * for facets that have immutable values (unfortunately we can't handle mutable values). @@ -34,14 +43,24 @@ export function useFacetUnwrap(prop: FacetProp): T | NoValue * - Once on initialization of the useState above * - And another time on this observe initialization */ - if ( - (typeofValue === 'number' || - typeofValue === 'string' || - typeofValue === 'boolean' || - value === undefined || - value === null) && - value === previousValue - ) { + if (equalityCheck === defaultEqualityCheck) { + const typeofValue = typeof previousValue + + if ( + (typeofValue === 'number' || + typeofValue === 'string' || + typeofValue === 'boolean' || + value === undefined || + value === null) && + value === previousValue + ) { + return previousState + } + + return { value } + } + + if (previousValue !== NO_VALUE && isEqual(previousValue)) { return previousState } @@ -49,7 +68,7 @@ export function useFacetUnwrap(prop: FacetProp): T | NoValue }) }) } - }, [prop]) + }, [prop, equalityCheck]) return isFacet(prop) ? state.value : prop }