diff --git a/packages/signia-react/src/track.test.tsx b/packages/signia-react/src/track.test.tsx
index eb0b545..9b2c12f 100644
--- a/packages/signia-react/src/track.test.tsx
+++ b/packages/signia-react/src/track.test.tsx
@@ -1,4 +1,4 @@
-import { createRef, forwardRef, memo, useEffect, useImperativeHandle } from 'react'
+import { createRef, forwardRef, lazy, memo, Suspense, useEffect, useImperativeHandle } from 'react'
import { act, create, ReactTestRenderer } from 'react-test-renderer'
import { atom } from 'signia'
import { track } from './track.js'
@@ -131,17 +131,19 @@ test('tracked components can use refs', async () => {
expect(ref.current?.handle).toBe('world')
})
-test('tracked components update when the state they refernce updates', async () => {
+test('tracked components update when the state they reference updates', async () => {
const a = atom('a', 1)
- const C = track(function Component() {
+ const Component = function Component() {
return <>{a.value}>
- })
+ }
+
+ const Tracked = track(Component)
let view: ReactTestRenderer
await act(() => {
- view = create()
+ view = create()
})
expect(view!.toJSON()).toMatchInlineSnapshot(`"1"`)
@@ -225,3 +227,98 @@ test("tracked zombie-children don't throw", async () => {
]
`)
})
+
+describe('lazy components', () => {
+ test("are memo'd when tracked", async () => {
+ let numRenders = 0
+ const Component = function Component({ a, b, c }: { a: string; b: string; c: string }) {
+ numRenders++
+ return (
+ <>
+ {a}
+ {b}
+ {c}
+ >
+ )
+ }
+
+ const Lazy = lazy(() => Promise.resolve({ default: Component }))
+ const TrackedLazy = track(Lazy)
+
+ let view: ReactTestRenderer
+ await act(() => {
+ view = create(
+
+
+
+ )
+ })
+
+ expect(view!.toJSON()).toMatchInlineSnapshot(`
+ [
+ "a",
+ "b",
+ "c",
+ ]
+ `)
+
+ expect(numRenders).toBe(1)
+
+ await act(() => {
+ view!.update(
+
+
+
+ )
+ })
+
+ expect(numRenders).toBe(1)
+
+ await act(() => {
+ view!.update(
+
+
+
+ )
+ })
+
+ expect(numRenders).toBe(2)
+
+ expect(view!.toJSON()).toMatchInlineSnapshot(`
+ [
+ "a",
+ "b",
+ "d",
+ ]
+ `)
+ })
+
+ test('update when the state they reference updates', async () => {
+ const a = atom('a', 1)
+
+ const Component = function Component() {
+ return <>{a.value}>
+ }
+
+ const Lazy = lazy(() => Promise.resolve({ default: Component }))
+ const TrackedLazy = track(Lazy)
+
+ let view: ReactTestRenderer
+
+ await act(() => {
+ view = create(
+
+
+
+ )
+ })
+
+ expect(view!.toJSON()).toMatchInlineSnapshot(`"1"`)
+
+ await act(() => {
+ a.set(2)
+ })
+
+ expect(view!.toJSON()).toMatchInlineSnapshot(`"2"`)
+ })
+})
diff --git a/packages/signia-react/src/track.ts b/packages/signia-react/src/track.ts
index c728edc..4f628c0 100644
--- a/packages/signia-react/src/track.ts
+++ b/packages/signia-react/src/track.ts
@@ -1,4 +1,4 @@
-import React, { forwardRef, FunctionComponent, memo } from 'react'
+import React, { forwardRef, FunctionComponent, lazy, LazyExoticComponent, memo } from 'react'
import { useStateTracking } from './useStateTracking.js'
export const ProxyHandlers = {
@@ -20,9 +20,15 @@ export const ProxyHandlers = {
},
}
+export const ReactLazySymbol = Symbol.for('react.lazy')
export const ReactMemoSymbol = Symbol.for('react.memo')
export const ReactForwardRefSymbol = Symbol.for('react.forward_ref')
+interface LazyFunctionComponent> extends LazyExoticComponent {
+ _init: (arg: unknown) => FunctionComponent
+ _payload: { status: number; _result: FunctionComponent }
+}
+
/**
* Returns a tracked version of the given component.
* Any signals whose values are read while the component renders will be tracked.
@@ -54,6 +60,21 @@ export function track>(
if ($$typeof === ReactForwardRefSymbol) {
return memo(forwardRef(new Proxy((baseComponent as any).render, ProxyHandlers) as any)) as any
}
+ if ($$typeof === ReactLazySymbol) {
+ let result: undefined | FunctionComponent
+
+ return memo(
+ lazy(() => {
+ if (!result) {
+ const { _init: init, _payload: payload } =
+ baseComponent as unknown as LazyFunctionComponent
+ const loaded = init(payload)
+ result = track(loaded)
+ }
+ return Promise.resolve({ default: result })
+ })
+ ) as any
+ }
return memo(new Proxy(baseComponent, ProxyHandlers) as any, compare) as any
}
diff --git a/packages/signia-react/src/wrapJsx.ts b/packages/signia-react/src/wrapJsx.ts
index 519a6db..1174d39 100644
--- a/packages/signia-react/src/wrapJsx.ts
+++ b/packages/signia-react/src/wrapJsx.ts
@@ -28,6 +28,7 @@ import { track } from './track.js'
const ReactMemoType = Symbol.for('react.memo') // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L30
const ReactForwardRefType = Symbol.for('react.forward_ref')
+const ReactLazyType = Symbol.for('react.lazy')
const ProxyInstance = new WeakMap, FunctionComponent>()
function proxyFunctionalComponent(Component: FunctionComponent) {
@@ -50,6 +51,8 @@ export function wrapJsx(jsx: T): T {
type = proxyFunctionalComponent(type.type)
} else if (type.$$typeof === ReactForwardRefType) {
type = proxyFunctionalComponent(type)
+ } else if (type.$$typeof === ReactLazyType) {
+ type = proxyFunctionalComponent(type)
}
}