Custom Equality Checks
You can create your own custom equality checks to handle specific data structures or comparison logic beyond what the built-in checks provide.
The EqualityCheck Interface
An equality check follows the EqualityCheck<T>
interface:
typescript
interface EqualityCheck<T> {(): (current: T) => boolean}
How Equality Checks Work
Equality checks use a two-function closure pattern:
- Initializer function
() =>
- Called once to set up the checker and initialize state - Checker function
(current: T) =>
- Called each time a value needs to be checked, maintains state across calls
The checker function compares the current value with the previous value stored in the closure and returns:
true
- Values are equal, skip facet updatefalse
- Values differ, trigger facet update
First call behavior: Always returns false
(no previous value to compare).
When using custom equality checks with useFacetMap
or useFacetMemo
, you must maintain a stable reference to the equality check function. If the equality check reference changes on every render, the facet gets recreated and all internal state is lost.
Two approaches to maintain stable references:
1. Define outside the component (preferred for static configurations):
tsx
// ✅ CORRECT - Defined outside component, stable across all rendersconst temperatureCheck = tolerantNumberEqualityCheck(0.5)const Component = () => {const result = useFacetMap((temp) => temp * 2, [], [tempFacet], temperatureCheck)}
2. Use useMemo
inside the component (for dynamic configurations):
tsx
const Component = ({ tolerance }: { tolerance: number }) => {// ✅ CORRECT - Memoized, stable reference unless tolerance changesconst equalityCheck = useMemo(() => tolerantNumberEqualityCheck(tolerance), [tolerance])const result = useFacetMap((temp) => temp * 2, [], [tempFacet], equalityCheck)}
Common mistake:
tsx
const Component = () => {// ❌ WRONG - Creates new reference every render, loses state!const result = useFacetMap((temp) => temp * 2,[],[tempFacet],tolerantNumberEqualityCheck(0.5), // New function reference on every render)}
Basic Custom Equality Check
Here's a simple example for case-insensitive string comparison:
tsx
import {EqualityCheck ,Option ,NO_VALUE } from '@react-facet/core'// Simple equality check without parametersexport constcaseInsensitiveStringCheck :EqualityCheck <string> = () => {letprevious :Option <string> =NO_VALUE return (current : string) => {constcurrentLower =current .toLowerCase ()constpreviousLower =previous ===NO_VALUE ?NO_VALUE :previous .toLowerCase ()if (previousLower !==currentLower ) {previous =current return false}return true}}// Usage exampleconstequalityCheck =caseInsensitiveStringCheck ()console .log (equalityCheck ('Hello')) // false (first call)console .log (equalityCheck ('HELLO')) // true (case insensitive match)console .log (equalityCheck ('hello')) // true (still matches)console .log (equalityCheck ('World')) // false (different value)
Advanced Custom Equality Checks
Parameterized Equality Check
Add an outer function to accept configuration parameters:
tsx
import {EqualityCheck } from '@react-facet/core'// Custom equality check that treats numbers within a tolerance as equalexport consttolerantNumberEqualityCheck = (tolerance : number = 0.01):EqualityCheck <number> => {return () => {letpreviousValue : number | undefinedreturn (currentValue : number) => {if (previousValue ===undefined ) {previousValue =currentValue return false}constisEqual =Math .abs (currentValue -previousValue ) <tolerance previousValue =currentValue returnisEqual }}}// Usage exampleconstequalityCheck =tolerantNumberEqualityCheck (0.5)()console .log (equalityCheck (1.0)) // false (first call)console .log (equalityCheck (1.4)) // true (within 0.5 tolerance)console .log (equalityCheck (2.0)) // false (outside tolerance)
Deep Object Equality Check
For comparing objects with nested structures:
tsx
import {EqualityCheck } from '@react-facet/core'typeDeepObject =Record <string, unknown>export constdeepObjectEqualityCheck :EqualityCheck <DeepObject > = () => {letpreviousValue :DeepObject | undefinedreturn (currentValue :DeepObject ) => {if (previousValue ===undefined ) {previousValue =currentValue return false}// Simple deep equality check (consider using a library like lodash for production)constisEqual =JSON .stringify (previousValue ) ===JSON .stringify (currentValue )previousValue =currentValue returnisEqual }}
Using JSON.stringify
for deep equality is convenient but can be slow for large objects. Consider using a dedicated deep-equality library like lodash.isEqual
for production code.
Custom Array Equality Check
Check arrays by specific criteria (e.g., by ID, ignoring order):
tsx
import {EqualityCheck } from '@react-facet/core'typeItem = {id : string;value : number }// Check if arrays have same items by ID, ignoring orderexport constarrayByIdEqualityCheck :EqualityCheck <Item []> = () => {letpreviousIds :Set <string> | undefinedreturn (currentValue :Item []) => {constcurrentIds = newSet (currentValue .map ((item ) =>item .id ))if (previousIds ===undefined ) {previousIds =currentIds return false}// Check if sets are equalif (previousIds .size !==currentIds .size ) {previousIds =currentIds return false}for (constid ofcurrentIds ) {if (!previousIds .has (id )) {previousIds =currentIds return false}}previousIds =currentIds return true}}
Date Comparison
Compare dates by day, ignoring time:
tsx
import {EqualityCheck } from '@react-facet/core'export constsameDayEqualityCheck :EqualityCheck <Date > = () => {letpreviousDay : string | undefinedreturn (currentValue :Date ) => {constcurrentDay =currentValue .toDateString ()if (previousDay ===undefined ) {previousDay =currentDay return false}constisEqual =previousDay ===currentDay previousDay =currentDay returnisEqual }}// Usageconstcheck =sameDayEqualityCheck ()console .log (check (newDate ('2025-10-16T10:00:00'))) // false (first call)console .log (check (newDate ('2025-10-16T14:30:00'))) // true (same day)console .log (check (newDate ('2025-10-17T10:00:00'))) // false (different day)
Partial Object Comparison
Compare only specific fields of an object:
tsx
import {EqualityCheck } from '@react-facet/core'typeUser = {id : numbername : stringlastLogin :Date preferences : object}// Only compare id and name, ignore other fieldsexport constuserIdentityEqualityCheck :EqualityCheck <User > = () => {letpreviousId : number | undefinedletpreviousName : string | undefinedreturn (currentValue :User ) => {if (previousId ===undefined ) {previousId =currentValue .id previousName =currentValue .name return false}constisEqual =previousId ===currentValue .id &&previousName ===currentValue .name previousId =currentValue .id previousName =currentValue .name returnisEqual }}
Using Custom Equality Checks with Facets
Custom equality checks work with any facet hook that accepts an equality check parameter:
tsx
import {useMemo } from 'react'import {useFacetMap ,useFacetWrap ,EqualityCheck } from '@react-facet/core'// Custom equality check factoryconsttolerantNumberEqualityCheck = (tolerance : number = 0.01):EqualityCheck <number> => {return () => {letpreviousValue : number | undefinedreturn (currentValue : number) => {if (previousValue ===undefined ) {previousValue =currentValue return false}constisEqual =Math .abs (currentValue -previousValue ) <tolerance previousValue =currentValue returnisEqual }}}constTemperatureMonitor = () => {consttempFacet =useFacetWrap (20.0)// ✅ Memoize the equality check to maintain stable referenceconstequalityCheck =useMemo (() =>tolerantNumberEqualityCheck (0.5), [])constroundedTemp =useFacetMap ((temp ) =>Math .round (temp * 10) / 10, // Returns a number which is passed into the equality checker[],[tempFacet ],equalityCheck ,)return (<div ><fast-text text ={useFacetMap ((t ) => `${t }°C`, [], [roundedTemp ])} /></div >)}
The equality check type must match the return type of the mapping function, not the input facet type. The equality check receives the value returned by the mapping function and compares it with the previous returned value.
Best Practices
1. Always Store the Previous Value
The pattern requires maintaining state across calls:
tsx
// ✅ Good - stores previous valueexport const myCheck: EqualityCheck<T> = () => {let previous: T | undefinedreturn (current: T) => {if (previous === undefined) {previous = currentreturn false}const isEqual = /* comparison logic */ (previous = current) // Update stored valuereturn isEqual}}
2. Return false
on First Call
The first comparison should always indicate a change:
tsx
return (current: T) => {if (previous === undefined) {previous = currentreturn false // ✅ First call always returns false}// ... rest of logic}
3. Update Previous Value After Comparison
Always update the stored value, even when values are equal:
tsx
return (current: T) => {// ... comparison logicconst isEqual = /* check if equal */ (previous = current) // ✅ Always update, regardless of resultreturn isEqual}
4. Consider Performance
Complex comparisons run frequently; keep them efficient:
tsx
// ❌ Avoid expensive operationsexport const slowCheck: EqualityCheck<bigData> = () => {let previous: bigData | undefinedreturn (current: bigData) => {// Avoid deep cloning or expensive serialization in hot pathconst isEqual = JSON.parse(JSON.stringify(previous)) === JSON.parse(JSON.stringify(current))// ...}}// ✅ Optimize for common caseexport const fastCheck: EqualityCheck<bigData> = () => {let previousHash: string | undefinedreturn (current: bigData) => {const currentHash = cheapHash(current) // Fast hash functionconst isEqual = previousHash === currentHashpreviousHash = currentHashreturn isEqual}}
5. Avoid Side Effects
Equality checks should be pure functions:
tsx
// ❌ Avoid side effectsexport const badCheck: EqualityCheck<T> = () => {return (current: T) => {console.log('Checking value:', current) // ❌ Side effectupdateGlobalState(current) // ❌ Side effect// ...}}// ✅ Pure functionexport const goodCheck: EqualityCheck<T> = () => {let previous: T | undefinedreturn (current: T) => {// Only comparison logic, no side effectsconst isEqual = /* ... */ (previous = current)return isEqual}}
6. Handle Edge Cases
Consider undefined
, null
, and empty values:
tsx
export const robustCheck: EqualityCheck<T | null> = () => {let previous: T | null | undefinedreturn (current: T | null) => {if (previous === undefined) {previous = currentreturn false}// Handle null values explicitlyif (previous === null && current === null) {return true}if (previous === null || current === null) {previous = currentreturn false}// Normal comparisonconst isEqual = /* compare non-null values */ (previous = current)return isEqual}}
7. Use TypeScript for Type Safety
Leverage TypeScript to ensure type correctness:
tsx
// ✅ Generic with constraintsexport const createComparableCheck = <T extends { compare(other: T): boolean }>(): EqualityCheck<T> => {return () => {let previous: T | undefinedreturn (current: T) => {if (previous === undefined) {previous = currentreturn false}const isEqual = current.compare(previous)previous = currentreturn isEqual}}}
Testing Custom Equality Checks
Always test your custom equality checks thoroughly:
tsx
import { describe, it, expect } from '@jest/globals'describe('caseInsensitiveStringCheck', () => {it('returns false on first call', () => {const check = caseInsensitiveStringCheck()expect(check('Hello')).toBe(false)})it('returns true for same case-insensitive value', () => {const check = caseInsensitiveStringCheck()check('Hello')expect(check('HELLO')).toBe(true)expect(check('hello')).toBe(true)})it('returns false for different values', () => {const check = caseInsensitiveStringCheck()check('Hello')expect(check('World')).toBe(false)})})
See Also
- Equality Checks Overview - Guide to all equality checks
createUniformObjectEqualityCheck
- For objects with uniform typescreateObjectWithKeySpecificEqualityCheck
- For objects with mixed typescreateOptionalValueEqualityCheck
- Wrap checks for nullable values