Skip to content

Commit a28570e

Browse files
haveyaseends300
andauthored
Track lazy loaded components (#82)
* Track lazy loaded components * Align whitespacing in tests * Undo changes to track return type * Use React function to create lazy components * Remove function type overload * Call track recursively Co-authored-by: David Sheldrick <[email protected]> --------- Co-authored-by: David Sheldrick <[email protected]>
1 parent dbd0a85 commit a28570e

File tree

3 files changed

+127
-6
lines changed

3 files changed

+127
-6
lines changed

packages/signia-react/src/track.test.tsx

+102-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createRef, forwardRef, memo, useEffect, useImperativeHandle } from 'react'
1+
import { createRef, forwardRef, lazy, memo, Suspense, useEffect, useImperativeHandle } from 'react'
22
import { act, create, ReactTestRenderer } from 'react-test-renderer'
33
import { atom } from 'signia'
44
import { track } from './track.js'
@@ -131,17 +131,19 @@ test('tracked components can use refs', async () => {
131131
expect(ref.current?.handle).toBe('world')
132132
})
133133

134-
test('tracked components update when the state they refernce updates', async () => {
134+
test('tracked components update when the state they reference updates', async () => {
135135
const a = atom('a', 1)
136136

137-
const C = track(function Component() {
137+
const Component = function Component() {
138138
return <>{a.value}</>
139-
})
139+
}
140+
141+
const Tracked = track(Component)
140142

141143
let view: ReactTestRenderer
142144

143145
await act(() => {
144-
view = create(<C />)
146+
view = create(<Tracked />)
145147
})
146148

147149
expect(view!.toJSON()).toMatchInlineSnapshot(`"1"`)
@@ -225,3 +227,98 @@ test("tracked zombie-children don't throw", async () => {
225227
]
226228
`)
227229
})
230+
231+
describe('lazy components', () => {
232+
test("are memo'd when tracked", async () => {
233+
let numRenders = 0
234+
const Component = function Component({ a, b, c }: { a: string; b: string; c: string }) {
235+
numRenders++
236+
return (
237+
<>
238+
{a}
239+
{b}
240+
{c}
241+
</>
242+
)
243+
}
244+
245+
const Lazy = lazy(() => Promise.resolve({ default: Component }))
246+
const TrackedLazy = track(Lazy)
247+
248+
let view: ReactTestRenderer
249+
await act(() => {
250+
view = create(
251+
<Suspense>
252+
<TrackedLazy a="a" b="b" c="c" />
253+
</Suspense>
254+
)
255+
})
256+
257+
expect(view!.toJSON()).toMatchInlineSnapshot(`
258+
[
259+
"a",
260+
"b",
261+
"c",
262+
]
263+
`)
264+
265+
expect(numRenders).toBe(1)
266+
267+
await act(() => {
268+
view!.update(
269+
<Suspense>
270+
<TrackedLazy a="a" b="b" c="c" />
271+
</Suspense>
272+
)
273+
})
274+
275+
expect(numRenders).toBe(1)
276+
277+
await act(() => {
278+
view!.update(
279+
<Suspense>
280+
<TrackedLazy a="a" b="b" c="d" />
281+
</Suspense>
282+
)
283+
})
284+
285+
expect(numRenders).toBe(2)
286+
287+
expect(view!.toJSON()).toMatchInlineSnapshot(`
288+
[
289+
"a",
290+
"b",
291+
"d",
292+
]
293+
`)
294+
})
295+
296+
test('update when the state they reference updates', async () => {
297+
const a = atom('a', 1)
298+
299+
const Component = function Component() {
300+
return <>{a.value}</>
301+
}
302+
303+
const Lazy = lazy(() => Promise.resolve({ default: Component }))
304+
const TrackedLazy = track(Lazy)
305+
306+
let view: ReactTestRenderer
307+
308+
await act(() => {
309+
view = create(
310+
<Suspense>
311+
<TrackedLazy />
312+
</Suspense>
313+
)
314+
})
315+
316+
expect(view!.toJSON()).toMatchInlineSnapshot(`"1"`)
317+
318+
await act(() => {
319+
a.set(2)
320+
})
321+
322+
expect(view!.toJSON()).toMatchInlineSnapshot(`"2"`)
323+
})
324+
})

packages/signia-react/src/track.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, FunctionComponent, memo } from 'react'
1+
import React, { forwardRef, FunctionComponent, lazy, LazyExoticComponent, memo } from 'react'
22
import { useStateTracking } from './useStateTracking.js'
33

44
export const ProxyHandlers = {
@@ -20,9 +20,15 @@ export const ProxyHandlers = {
2020
},
2121
}
2222

23+
export const ReactLazySymbol = Symbol.for('react.lazy')
2324
export const ReactMemoSymbol = Symbol.for('react.memo')
2425
export const ReactForwardRefSymbol = Symbol.for('react.forward_ref')
2526

27+
interface LazyFunctionComponent<T extends FunctionComponent<any>> extends LazyExoticComponent<T> {
28+
_init: (arg: unknown) => FunctionComponent
29+
_payload: { status: number; _result: FunctionComponent }
30+
}
31+
2632
/**
2733
* Returns a tracked version of the given component.
2834
* Any signals whose values are read while the component renders will be tracked.
@@ -54,6 +60,21 @@ export function track<T extends FunctionComponent<any>>(
5460
if ($$typeof === ReactForwardRefSymbol) {
5561
return memo(forwardRef(new Proxy((baseComponent as any).render, ProxyHandlers) as any)) as any
5662
}
63+
if ($$typeof === ReactLazySymbol) {
64+
let result: undefined | FunctionComponent
65+
66+
return memo(
67+
lazy(() => {
68+
if (!result) {
69+
const { _init: init, _payload: payload } =
70+
baseComponent as unknown as LazyFunctionComponent<any>
71+
const loaded = init(payload)
72+
result = track(loaded)
73+
}
74+
return Promise.resolve({ default: result })
75+
})
76+
) as any
77+
}
5778

5879
return memo(new Proxy(baseComponent, ProxyHandlers) as any, compare) as any
5980
}

packages/signia-react/src/wrapJsx.ts

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { track } from './track.js'
2828

2929
const ReactMemoType = Symbol.for('react.memo') // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L30
3030
const ReactForwardRefType = Symbol.for('react.forward_ref')
31+
const ReactLazyType = Symbol.for('react.lazy')
3132
const ProxyInstance = new WeakMap<FunctionComponent<any>, FunctionComponent<any>>()
3233

3334
function proxyFunctionalComponent(Component: FunctionComponent<any>) {
@@ -50,6 +51,8 @@ export function wrapJsx<T>(jsx: T): T {
5051
type = proxyFunctionalComponent(type.type)
5152
} else if (type.$$typeof === ReactForwardRefType) {
5253
type = proxyFunctionalComponent(type)
54+
} else if (type.$$typeof === ReactLazyType) {
55+
type = proxyFunctionalComponent(type)
5356
}
5457
}
5558

0 commit comments

Comments
 (0)