Skip to content

Commit

Permalink
Track lazy loaded components (#82)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
haveyaseen and ds300 authored Jan 4, 2024
1 parent dbd0a85 commit a28570e
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 6 deletions.
107 changes: 102 additions & 5 deletions packages/signia-react/src/track.test.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(<C />)
view = create(<Tracked />)
})

expect(view!.toJSON()).toMatchInlineSnapshot(`"1"`)
Expand Down Expand Up @@ -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(
<Suspense>
<TrackedLazy a="a" b="b" c="c" />
</Suspense>
)
})

expect(view!.toJSON()).toMatchInlineSnapshot(`
[
"a",
"b",
"c",
]
`)

expect(numRenders).toBe(1)

await act(() => {
view!.update(
<Suspense>
<TrackedLazy a="a" b="b" c="c" />
</Suspense>
)
})

expect(numRenders).toBe(1)

await act(() => {
view!.update(
<Suspense>
<TrackedLazy a="a" b="b" c="d" />
</Suspense>
)
})

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(
<Suspense>
<TrackedLazy />
</Suspense>
)
})

expect(view!.toJSON()).toMatchInlineSnapshot(`"1"`)

await act(() => {
a.set(2)
})

expect(view!.toJSON()).toMatchInlineSnapshot(`"2"`)
})
})
23 changes: 22 additions & 1 deletion packages/signia-react/src/track.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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<T extends FunctionComponent<any>> extends LazyExoticComponent<T> {
_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.
Expand Down Expand Up @@ -54,6 +60,21 @@ export function track<T extends FunctionComponent<any>>(
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<any>
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
}
3 changes: 3 additions & 0 deletions packages/signia-react/src/wrapJsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>, FunctionComponent<any>>()

function proxyFunctionalComponent(Component: FunctionComponent<any>) {
Expand All @@ -50,6 +51,8 @@ export function wrapJsx<T>(jsx: T): T {
type = proxyFunctionalComponent(type.type)
} else if (type.$$typeof === ReactForwardRefType) {
type = proxyFunctionalComponent(type)
} else if (type.$$typeof === ReactLazyType) {
type = proxyFunctionalComponent(type)
}
}

Expand Down

1 comment on commit a28570e

@vercel
Copy link

@vercel vercel bot commented on a28570e Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

signia – ./

signia-git-main-tldraw.vercel.app
signia.tldraw.dev
signia.vercel.app
signia-tldraw.vercel.app

Please sign in to comment.