From 2f9981138a30b5e924149756fdda7a7b5dcd0146 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 4 Sep 2022 22:42:18 -0500 Subject: [PATCH] refactor!: only halt traverseFiber on boolean, harden Fiber types (#5) --- README.md | 13 +++++-- src/index.tsx | 54 +++++++++++++++----------- tests/index.test.tsx | 90 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 121 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 69c5eb5..9181b08 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ function Component() { ### useContainer -Returns the current react-reconciler `Container` or the Fiber created from `Reconciler.createContainer`. +Returns the current react-reconciler container info passed to `Reconciler.createContainer`. -In react-dom, `container.containerInfo` will point to the root DOM element; in react-three-fiber, it will point to the root Zustand store. +In react-dom, a container will point to the root DOM element; in react-three-fiber, it will point to the root Zustand store. ```tsx import * as React from 'react' @@ -73,7 +73,7 @@ function Component() { React.useLayoutEffect(() => { //
(e.g. react-dom) - console.log(container.containerInfo) + console.log(container) }, [container]) } ``` @@ -166,5 +166,10 @@ Traverses up or down through a `Fiber`, return `true` to stop and select a node. import { type Fiber, traverseFiber } from 'its-fine' const ascending = true -const prevElement: Fiber = traverseFiber(fiber, ascending, (node: Fiber) => node.type === 'element') + +const parentDiv: Fiber | undefined = traverseFiber( + fiber as Fiber, + ascending, + (node: Fiber) => node.type === 'div', +) ``` diff --git a/src/index.tsx b/src/index.tsx index 5ef8374..a0ff0de 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,25 +4,29 @@ import type ReactReconciler from 'react-reconciler' /** * Represents a react-internal Fiber node. */ -export type Fiber = ReactReconciler.Fiber +export type Fiber = Omit & { stateNode: T } /** * Represents a {@link Fiber} node selector for traversal. */ -export type FiberSelector = (node: Fiber) => boolean | void +export type FiberSelector = (node: Fiber) => boolean | void /** * Traverses up or down through a {@link Fiber}, return `true` to stop and select a node. */ -export function traverseFiber(fiber: Fiber, ascending: boolean, selector: FiberSelector): Fiber | undefined { +export function traverseFiber( + fiber: Fiber, + ascending: boolean, + selector: FiberSelector, +): Fiber | undefined { let halted = false - let selected: Fiber | undefined + let selected: Fiber | undefined - let node = fiber[ascending ? 'return' : 'child'] + let node = ascending ? fiber.return : fiber.child let sibling = fiber.sibling while (node) { while (sibling) { - halted ||= !!selector(sibling) + halted ||= selector(sibling) === true if (halted) { selected = sibling break @@ -31,13 +35,13 @@ export function traverseFiber(fiber: Fiber, ascending: boolean, selector: FiberS sibling = sibling.sibling } - halted ||= !!selector(node) + halted ||= selector(node) === true if (halted) { selected = node break } - node = node[ascending ? 'return' : 'child'] + node = ascending ? node.return : node.child } return selected @@ -54,28 +58,32 @@ const { ReactCurrentOwner } = (React as unknown as ReactInternal).__SECRET_INTER /** * Returns the current react-internal {@link Fiber}. This is an implementation detail of [react-reconciler](https://github.com/facebook/react/tree/main/packages/react-reconciler). */ -export function useFiber(): Fiber { - const [fiber] = React.useState(() => ReactCurrentOwner.current!) +export function useFiber(): Fiber { + const [fiber] = React.useState>(() => ReactCurrentOwner.current!) return fiber } /** - * Represents a reconciler container. + * Represents a react-reconciler container instance. */ -export interface Container extends Fiber { - /** Represents container state passed to {@link ReactReconciler.Reconciler.createContainer}. */ +export interface ContainerInstance { containerInfo: T } /** - * Returns the current react-reconciler {@link Container} or the Fiber created from {@link ReactReconciler.Reconciler.createContainer}. + * Returns the current react-reconciler container info passed to {@link ReactReconciler.Reconciler.createContainer}. * - * In react-dom, {@link Container.containerInfo} will point to the root DOM element; in react-three-fiber, it will point to the root Zustand store. + * In react-dom, a container will point to the root DOM element; in react-three-fiber, it will point to the root Zustand store. */ -export function useContainer(): Container { +export function useContainer(): T { const fiber = useFiber() const container = React.useMemo( - () => traverseFiber(fiber, true, (node) => node.type == null && node.stateNode.containerInfo != null)!.stateNode, + () => + traverseFiber>( + fiber, + true, + (node) => node.type == null && node.stateNode?.containerInfo != null, + )!.stateNode.containerInfo, [fiber], ) @@ -89,13 +97,13 @@ export function useContainer(): Container { */ export function useNearestChild(): React.MutableRefObject { const fiber = useFiber() - const instance = React.useRef() + const childRef = React.useRef() React.useLayoutEffect(() => { - instance.current = traverseFiber(fiber, false, (node) => typeof node.type === 'string')?.stateNode + childRef.current = traverseFiber(fiber, false, (node) => typeof node.type === 'string')?.stateNode }, [fiber]) - return instance + return childRef } /** @@ -105,13 +113,13 @@ export function useNearestChild(): React.MutableRefObject(): React.MutableRefObject { const fiber = useFiber() - const instance = React.useRef() + const parentRef = React.useRef() React.useLayoutEffect(() => { - instance.current = traverseFiber(fiber, true, (node) => typeof node.type === 'string')?.stateNode + parentRef.current = traverseFiber(fiber, true, (node) => typeof node.type === 'string')?.stateNode }, [fiber]) - return instance + return parentRef } /** diff --git a/tests/index.test.tsx b/tests/index.test.tsx index b5c253d..bc0b6a3 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react' import { describe, expect, it } from 'vitest' -import { act, render, type HostContainer, type NilNode } from 'react-nil' +import { type NilNode, type HostContainer, act, render } from 'react-nil' import { create } from 'react-test-renderer' import { type Fiber, + type ContainerInstance, + traverseFiber, useFiber, - type Container, useContainer, useNearestChild, useNearestParent, @@ -22,6 +23,8 @@ interface PrimitiveProps { name?: string } +type Primitive = NilNode + declare global { namespace JSX { interface IntrinsicElements { @@ -75,27 +78,96 @@ describe('useFiber', () => { }) }) +describe('traverseFiber', () => { + it('iterates descending through a fiber', async () => { + let fiber!: Fiber + + function Test() { + fiber = useFiber() + return + } + await act(async () => { + render( + + + , + ) + }) + + const traversed = [] as unknown as [child: Fiber] + traverseFiber(fiber, false, (node) => void traversed.push(node)) + + expect(traversed.length).toBe(1) + + const [child] = traversed + expect(child.stateNode.props.name).toBe('child') + }) + + it('iterates ascending through a fiber', async () => { + let fiber!: Fiber + let container!: HostContainer + + function Test() { + fiber = useFiber() + return + } + await act(async () => { + container = render( + + + , + ) + }) + + const traversed = [] as unknown as [ + parent: Fiber, + rootContainer: Fiber>, + ] + traverseFiber(fiber, true, (node) => void traversed.push(node)) + + expect(traversed.length).toBe(2) + + const [parent, rootContainer] = traversed + expect(parent.stateNode.props.name).toBe('parent') + expect(rootContainer.stateNode.containerInfo).toBe(container) + }) + + it('returns the active node when halted', async () => { + let fiber!: Fiber + let container!: HostContainer + + function Test() { + fiber = useFiber() + return + } + await act(async () => (container = render())) + + const child = traverseFiber(fiber, false, (node) => node.stateNode === container.head) + expect(child!.stateNode.props.name).toBe('child') + }) +}) + describe('useContainer', () => { it('gets the current react-reconciler container', async () => { - let currentContainer!: Container + let rootContainer!: HostContainer let container!: HostContainer function Test() { - currentContainer = useContainer() + rootContainer = useContainer() return null } await act(async () => (container = render())) - expect(currentContainer.containerInfo).toBe(container) + expect(rootContainer).toBe(container) }) }) describe('useNearestChild', () => { it('gets the nearest child instance', async () => { - const instances: React.MutableRefObject | undefined>[] = [] + const instances: React.MutableRefObject[] = [] function Test(props: React.PropsWithChildren) { - instances.push(useNearestChild()) + instances.push(useNearestChild()) return <>{props.children} } @@ -130,10 +202,10 @@ describe('useNearestChild', () => { describe('useNearestParent', () => { it('gets the nearest parent instance', async () => { - const instances: React.MutableRefObject | undefined>[] = [] + const instances: React.MutableRefObject[] = [] function Test(props: React.PropsWithChildren) { - instances.push(useNearestParent()) + instances.push(useNearestParent()) return <>{props.children} }