Skip to content

Add support for multiple facets in useFacetCallback and useFacetEffect #14

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

Merged
merged 12 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions examples/benchmarking/src/listMemoFacet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,31 @@ const ListItem = ({ item }: { item: Facet<Data> }) => {
randomWork(health)
},
[],
health,
[health],
)

useFacetEffect(
(name) => {
randomWork(name)
},
[],
name,
[name],
)

useFacetEffect(
(name) => {
randomWork(name)
},
[],
name,
[name],
)

useFacetEffect(
(name) => {
randomWork(name)
},
[],
name,
[name],
)

return null
Expand Down
2 changes: 1 addition & 1 deletion examples/benchmarking/src/overheadFacet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function Performance() {
// and effect that does nothing
},
[],
value,
[value],
)

return null
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-facet/core/src/components/Map.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ it('updates only items that have changed', () => {
const mock = jest.fn()

const ExampleContent = ({ item }: { item: Facet<Input> }) => {
useFacetEffect(mock, [], item)
useFacetEffect(mock, [], [item])
return null
}

Expand Down
70 changes: 64 additions & 6 deletions packages/@react-facet/core/src/hooks/useFacetCallback.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react'
import { render } from '@react-facet/dom-fiber-testing-library'
import { render, act } from '@react-facet/dom-fiber-testing-library'
import { useFacetCallback } from './useFacetCallback'
import { useFacetEffect } from './useFacetEffect'
import { useFacetMap } from './useFacetMap'
Expand All @@ -19,11 +19,11 @@ it('captures the current value of the facet in a function that can be used as ha

const ComponentWithFacetCallback = ({ cb, dependency }: ComponentWithFacetCallbackProps) => {
const handler = useFacetCallback(
(value) => (event) => {
(value) => (event: string) => {
cb(value, dependency, event)
},
[dependency, cb],
demoFacet,
[demoFacet],
)

useEffect(() => {
Expand Down Expand Up @@ -60,7 +60,7 @@ it('properly memoizes the returned facet', () => {

const TestComponent = () => {
const previousCallbackRef = useRef<() => void | NoValue>()
const callback = useFacetCallback(() => () => {}, [], demoFacet)
const callback = useFacetCallback(() => () => {}, [], [demoFacet])

// Check if it is a second render
if (previousCallbackRef.current) {
Expand Down Expand Up @@ -98,15 +98,15 @@ it('should work with uninitialized values', () => {
cb(value)
},
[cb],
internalDemoFacet,
[internalDemoFacet],
)

useFacetEffect(
() => {
handler()
},
[handler],
internalDemoFacet,
[internalDemoFacet],
)

return null
Expand All @@ -120,3 +120,61 @@ it('should work with uninitialized values', () => {
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('valuevalue')
})

it('supports multiple facets', () => {
const facetA = createFacet({ initialValue: 'a' })
const facetB = createFacet({ initialValue: 123 })

const callback = jest.fn()
let handler: (event: string) => void

const TestComponent = ({ dependency }: { dependency: string }) => {
handler = useFacetCallback(
(valueA, valueB) => (event: string) => {
callback(valueA, valueB, dependency, event)
},
[dependency],
[facetA, facetB],
)

return null
}

render(<TestComponent dependency="dependency" />)

act(() => {
handler('event')
})

expect(callback).toHaveBeenCalledWith('a', 123, 'dependency', 'event')
})

it('returns NO_VALUE if any facet has NO_VALUE and skip calling the callback', () => {
const facetA = createFacet({ initialValue: 'a' })
const facetB = createFacet<number>({ initialValue: NO_VALUE })

const callback = jest.fn()
let handler: (event: string) => void

const TestComponent = ({ dependency }: { dependency: string }) => {
handler = useFacetCallback(
(valueA, valueB) => (event: string) => {
callback(valueA, valueB, dependency, event)
},
[dependency],
[facetA, facetB],
)

return null
}

render(<TestComponent dependency="dependency" />)

act(() => {
const result = handler('event')
// verifies that calling the callback returns NO_VALUE
expect(result).toBe(NO_VALUE)
})

expect(callback).not.toHaveBeenCalledWith()
})
164 changes: 137 additions & 27 deletions packages/@react-facet/core/src/hooks/useFacetCallback.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,157 @@
import { useCallback, useRef } from 'react'
import { useCallback, useLayoutEffect, useRef } from 'react'
import { NoValue } from '..'
import { Facet, NO_VALUE, Option } from '../types'
import { useFacetEffect } from './useFacetEffect'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, C extends (...args: any[]) => M | NoValue>(
callback: (v: V) => C,
dependencies: unknown[],
facet: [Facet<V>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, V4, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>, Facet<V4>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, V4, V5, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>, Facet<V4>, Facet<V5>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, V4, V5, V6, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>, Facet<V4>, Facet<V5>, Facet<V6>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, V4, V5, V6, V7, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>, Facet<V4>, Facet<V5>, Facet<V6>, Facet<V7>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, V4, V5, V6, V7, V8, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7, v8: V8) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>, Facet<V4>, Facet<V5>, Facet<V6>, Facet<V7>, Facet<V8>],
): C

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFacetCallback<M, V, V1, V2, V3, V4, V5, V6, V7, V8, V9, C extends (...args: any[]) => M | NoValue>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7, v8: V8, v9: V9) => C,
dependencies: unknown[],
facet: [Facet<V>, Facet<V1>, Facet<V2>, Facet<V3>, Facet<V4>, Facet<V5>, Facet<V6>, Facet<V7>, Facet<V8>, Facet<V9>],
): C

export function useFacetCallback<
M,
V,
V1,
V2,
V3,
V4,
V5,
V6,
V7,
V8,
V9,
V10,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
C extends (...args: any[]) => M | NoValue,
>(
callback: (v: V, v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7, v8: V8, v9: V9, v10: V10) => C,
dependencies: unknown[],
facet: [
Facet<V>,
Facet<V1>,
Facet<V2>,
Facet<V3>,
Facet<V4>,
Facet<V5>,
Facet<V6>,
Facet<V7>,
Facet<V8>,
Facet<V9>,
Facet<V10>,
],
): C

/**
* Creates a callback that depends on the value of a facet.
* Very similar to `useCallback` from `React`
*
* @param callback function callback that receives the current facet value and the arguments passed to the callback
* @param dependencies variable used by the map that are available in scope (similar as dependencies of useCallback)
* @param facet facet that the callback listens to
* @param callback function callback that receives the current facet values and the arguments passed to the callback
* @param dependencies variable used by the callback that are available in scope (similar as dependencies of useCallback)
* @param facets facets that the callback listens to
*
* We pass the dependencies of the callback as the second argument so we can leverage the eslint-plugin-react-hooks option for additionalHooks.
* Having this as the second argument allows the linter to work.
*/
export function useFacetCallback<V, T>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (value: V) => (...args: any[]) => T | NoValue,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dependencies: any[],
facet: Facet<V>,
) {
const facetRef = useRef<Option<V>>(facet.get())

useFacetEffect(
(value) => {
facetRef.current = value
},
[],
facet,
)
export function useFacetCallback<M>(
callback: (...args: unknown[]) => (...args: unknown[]) => M | NoValue,
dependencies: unknown[],
facets: Facet<unknown>[],
): (...args: unknown[]) => M | NoValue {
const facetsRef = useRef<Option<unknown>[]>(facets.map(() => NO_VALUE))

useLayoutEffect(() => {
const unsubscribes = facets.map((facet, index) => {
return facet.observe((value) => {
facetsRef.current[index] = value
})
})

return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe())
}
// We care about each individual facet and if any is a different reference
// eslint-disable-next-line react-hooks/exhaustive-deps
}, facets)

// We care about each individual dependency and if any is a different reference
// eslint-disable-next-line react-hooks/exhaustive-deps
const callbackMemoized = useCallback(callback, dependencies)

// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(...args: any[]): T | NoValue => {
const value = facetRef.current
(...args: unknown[]) => {
const values = facetsRef.current

if (value === NO_VALUE) return NO_VALUE
for (const value of values) {
if (value === NO_VALUE) return NO_VALUE
}

return callbackMemoized(value)(...args)
return callbackMemoized(...values)(...args)
},
[callbackMemoized, facetRef],
[callbackMemoized, facetsRef],
)
}
Loading