Skip to content

Commit b16c53e

Browse files
authored
Fix useFacetCallback mismatch subscriptions (#115)
1 parent 65dd216 commit b16c53e

File tree

2 files changed

+80
-26
lines changed

2 files changed

+80
-26
lines changed

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

+79-23
Original file line numberDiff line numberDiff line change
@@ -234,35 +234,91 @@ it('returns the defaultValue, when provided, if any facet has NO_VALUE and skip
234234
expect(callback).not.toHaveBeenCalledWith()
235235
})
236236

237-
it('should always have the current value of tracked facets', () => {
238-
const facetA = createFacet<string>({ initialValue: NO_VALUE })
237+
describe('regressions', () => {
238+
it('should always have the current value of tracked facets', () => {
239+
const facetA = createFacet<string>({ initialValue: NO_VALUE })
240+
241+
let handler: (event: string) => void = () => {}
242+
243+
const TestComponent = () => {
244+
handler = useFacetCallback(
245+
(a) => (b: string) => {
246+
return a + b
247+
},
248+
[],
249+
[facetA],
250+
)
251+
252+
return null
253+
}
239254

240-
let handler: (event: string) => void = () => {}
255+
// We make sure to be the first listener registered, so this is called before
256+
// the listener within the useFacetCallback (which would have created the issue)
257+
facetA.observe(() => {
258+
const result = handler('string')
259+
expect(result).toBe('newstring')
260+
})
241261

242-
const TestComponent = () => {
243-
handler = useFacetCallback(
244-
(a) => (b: string) => {
245-
return a + b
262+
render(<TestComponent />)
263+
264+
// In this act, the effect within useFacetCallback will be executed, subscribing for changes of the facetA
265+
// Then we set the value, causing the listener above to being called
266+
act(() => {
267+
facetA.set('new')
268+
})
269+
})
270+
271+
it('should always have the current value of tracked facets (even after another component unmounts)', () => {
272+
const facetA = createFacet<string>({
273+
initialValue: NO_VALUE,
274+
275+
// We need to have a value from a startSubscription so that after the last listener is removed, we set the facet back to NO_VALUE
276+
startSubscription: (update) => {
277+
update('value')
278+
return () => {}
246279
},
247-
[],
248-
[facetA],
249-
)
280+
})
250281

251-
return null
252-
}
282+
let handler: (event: string) => void = () => {}
253283

254-
// We make sure to be the first listener registered, so this is called before
255-
// the listener within the useFacetCallback (which would have created the issue)
256-
facetA.observe(() => {
257-
const result = handler('string')
258-
expect(result).toBe('newstring')
259-
})
284+
const TestComponentA = () => {
285+
handler = useFacetCallback(
286+
(a) => (b: string) => {
287+
return a + b
288+
},
289+
[],
290+
[facetA],
291+
)
260292

261-
render(<TestComponent />)
293+
return null
294+
}
262295

263-
// In this act, the effect within useFacetCallback will be executed, subscribing for changes of the facetA
264-
// Then we set the value, causing the listener above to being called
265-
act(() => {
266-
facetA.set('new')
296+
const TestComponentB = () => {
297+
useFacetCallback(() => () => {}, [], [facetA])
298+
299+
return null
300+
}
301+
302+
// We mount both components, both internally calling the useFacetCallback to start subscriptions towards the facetA
303+
const { rerender } = render(
304+
<>
305+
<TestComponentA />
306+
<TestComponentB />
307+
</>,
308+
)
309+
310+
// Then we unmount one of the components, causing it to unsubscribe from the facetA
311+
rerender(
312+
<>
313+
<TestComponentA />
314+
</>,
315+
)
316+
317+
// However, with a prior implementation, a shared instance of a listener (noop) was used across all useFacetCallback usages
318+
// causing a mismatch between calls to observer and unsubscribe.
319+
act(() => {
320+
const result = handler('string')
321+
expect(result).toBe('valuestring')
322+
})
267323
})
268324
})

packages/@react-facet/core/src/hooks/useFacetCallback.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function useFacetCallback<M, Y extends Facet<unknown>[], T extends [...Y]
4646
useLayoutEffect(() => {
4747
// Make sure to start subscriptions, even though we are getting the values directly from them
4848
// We read the values using `.get` to make sure they are always up-to-date
49-
const unsubscribes = facets.map((facet) => facet.observe(noop))
49+
const unsubscribes = facets.map((facet) => facet.observe(() => {}))
5050

5151
return () => {
5252
unsubscribes.forEach((unsubscribe) => unsubscribe())
@@ -74,5 +74,3 @@ export function useFacetCallback<M, Y extends Facet<unknown>[], T extends [...Y]
7474
[callbackMemoized, defaultReturnValue, ...facets],
7575
)
7676
}
77-
78-
const noop = () => {}

0 commit comments

Comments
 (0)