Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/fiber/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @react-three/fiber

## 9.2.0

### Minor Changes

- 94ece53e17a586465b10ec627cf4799cefa72b3a: Export flushSync

## 9.1.4

### Patch Changes
Expand Down
4 changes: 2 additions & 2 deletions packages/fiber/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@react-three/fiber",
"version": "9.1.4",
"version": "9.2.0",
"description": "A React renderer for Threejs",
"keywords": [
"react",
Expand Down Expand Up @@ -44,7 +44,7 @@
},
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.28.9",
"@types/react-reconciler": "^0.32.0",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
Expand Down
112 changes: 23 additions & 89 deletions packages/fiber/src/core/reconciler.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import * as THREE from 'three'
import * as React from 'react'
import Reconciler from 'react-reconciler'
import {
// NoEventPriority,
ContinuousEventPriority,
DiscreteEventPriority,
DefaultEventPriority,
} from 'react-reconciler/constants'
import { ContinuousEventPriority, DiscreteEventPriority, DefaultEventPriority } from 'react-reconciler/constants'
import { unstable_IdlePriority as idlePriority, unstable_scheduleCallback as scheduleCallback } from 'scheduler'
import {
diffProps,
Expand All @@ -23,10 +18,6 @@ import type { RootStore } from './store'
import { removeInteractivity, type EventHandlers } from './events'
import type { ThreeElement } from '../three-types'

// TODO: upstream to DefinitelyTyped for React 19
// https://github.com/facebook/react/issues/28956
type EventPriority = number

type Fiber = Omit<Reconciler.Fiber, 'alternate'> & { refCleanup: null | (() => void); alternate: Fiber | null }

function createReconciler<
Expand All @@ -45,84 +36,23 @@ function createReconciler<
NoTimeout,
TransitionStatus,
>(
config: Omit<
Reconciler.HostConfig<
Type,
Props,
Container,
Instance,
TextInstance,
SuspenseInstance,
HydratableInstance,
PublicInstance,
HostContext,
null, // updatePayload
ChildSet,
TimeoutHandle,
NoTimeout
>,
'getCurrentEventPriority' | 'prepareUpdate' | 'commitUpdate'
> & {
/**
* This method should mutate the `instance` and perform prop diffing if needed.
*
* The `internalHandle` data structure is meant to be opaque. If you bend the rules and rely on its internal fields, be aware that it may change significantly between versions. You're taking on additional maintenance risk by reading from it, and giving up all guarantees if you write something to it.
*/
commitUpdate?(
instance: Instance,
type: Type,
prevProps: Props,
nextProps: Props,
internalHandle: Reconciler.OpaqueHandle,
): void

// Undocumented
// https://github.com/facebook/react/pull/26722
NotPendingTransition: TransitionStatus | null
HostTransitionContext: React.Context<TransitionStatus>
// https://github.com/facebook/react/pull/28751
setCurrentUpdatePriority(newPriority: EventPriority): void
getCurrentUpdatePriority(): EventPriority
resolveUpdatePriority(): EventPriority
// https://github.com/facebook/react/pull/28804
resetFormInstance(form: FormInstance): void
// https://github.com/facebook/react/pull/25105
requestPostPaintCallback(callback: (time: number) => void): void
// https://github.com/facebook/react/pull/26025
shouldAttemptEagerTransition(): boolean
// https://github.com/facebook/react/pull/31528
trackSchedulerEvent(): void
// https://github.com/facebook/react/pull/31008
resolveEventType(): null | string
resolveEventTimeStamp(): number

/**
* This method is called during render to determine if the Host Component type and props require some kind of loading process to complete before committing an update.
*/
maySuspendCommit(type: Type, props: Props): boolean
/**
* This method may be called during render if the Host Component type and props might suspend a commit. It can be used to initiate any work that might shorten the duration of a suspended commit.
*/
preloadInstance(type: Type, props: Props): boolean
/**
* This method is called just before the commit phase. Use it to set up any necessary state while any Host Components that might suspend this commit are evaluated to determine if the commit must be suspended.
*/
startSuspendingCommit(): void
/**
* This method is called after `startSuspendingCommit` for each Host Component that indicated it might suspend a commit.
*/
suspendInstance(type: Type, props: Props): void
/**
* This method is called after all `suspendInstance` calls are complete.
*
* Return `null` if the commit can happen immediately.
*
* Return `(initiateCommit: Function) => Function` if the commit must be suspended. The argument to this callback will initiate the commit when called. The return value is a cancellation function that the Reconciler can use to abort the commit.
*
*/
waitForCommitToBeReady(): ((initiateCommit: Function) => Function) | null
},
): Reconciler.Reconciler<Container, Instance, TextInstance, SuspenseInstance, PublicInstance> {
config: Reconciler.HostConfig<
Type,
Props,
Container,
Instance,
TextInstance,
SuspenseInstance,
HydratableInstance,
FormInstance,
PublicInstance,
HostContext,
ChildSet,
TimeoutHandle,
NoTimeout,
TransitionStatus
>,
): Reconciler.Reconciler<Container, Instance, TextInstance, SuspenseInstance, FormInstance, PublicInstance> {
const reconciler = Reconciler(config as any)

reconciler.injectIntoDevTools({
Expand Down Expand Up @@ -637,7 +567,11 @@ export const reconciler = /* @__PURE__ */ createReconciler<
suspendInstance() {},
waitForCommitToBeReady: () => null,
NotPendingTransition: null,
HostTransitionContext: /* @__PURE__ */ React.createContext<HostConfig['TransitionStatus']>(null),
// The reconciler types use the internal ReactContext with all the hidden properties
// so we have to cast from the public React.Context type
HostTransitionContext: /* @__PURE__ */ React.createContext<HostConfig['TransitionStatus']>(
null,
) as unknown as Reconciler.ReactContext<HostConfig['TransitionStatus']>,
setCurrentUpdatePriority(newPriority: number) {
currentUpdatePriority = newPriority
},
Expand Down
39 changes: 20 additions & 19 deletions packages/fiber/src/core/renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import * as THREE from 'three'
import * as React from 'react'
import { ConcurrentRoot } from 'react-reconciler/constants'
import * as THREE from 'three'
import { createWithEqualityFn } from 'zustand/traditional'

import type { ThreeElement } from '../three-types'
import { ComputeFunction, EventManager } from './events'
import { useStore } from './hooks'
import { advance, invalidate } from './loop'
import { reconciler, Root } from './reconciler'
import {
Renderer,
createStore,
isRenderer,
context,
RootState,
Size,
createStore,
Dpr,
Performance,
Frameloop,
isRenderer,
Performance,
Renderer,
RootState,
RootStore,
Size,
} from './store'
import { reconciler, Root } from './reconciler'
import { invalidate, advance } from './loop'
import { EventManager, ComputeFunction } from './events'
import {
type Properties,
is,
dispose,
applyProps,
calculateDpr,
EquConfig,
useIsomorphicLayoutEffect,
Camera,
updateCamera,
applyProps,
dispose,
EquConfig,
is,
prepare,
updateCamera,
useIsomorphicLayoutEffect,
useMutableCallback,
} from './utils'
import { useStore } from './hooks'

// Shim for OffscreenCanvas since it was removed from DOM types
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/54988
Expand Down Expand Up @@ -575,8 +575,9 @@ function Portal({ state = {}, children, container }: PortalProps): React.JSX.Ele
* Force React to flush any updates inside the provided callback synchronously and immediately.
* All the same caveats documented for react-dom's `flushSync` apply here (see https://react.dev/reference/react-dom/flushSync).
* Nevertheless, sometimes one needs to render synchronously, for example to keep DOM and 3D changes in lock-step without
* having to revert to a non-React solution.
* having to revert to a non-React solution. Note: this will only flush updates within the `Canvas` root.
*/
export function flushSync<R>(fn: () => R): R {
return reconciler.flushSync(fn)
// @ts-ignore - reconciler types are not maintained
return reconciler.flushSyncFromReconciler(fn)
}
28 changes: 27 additions & 1 deletion packages/fiber/tests/renderer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import * as THREE from 'three'
import { ReconcilerRoot, createRoot, act, extend, ThreeElement, ThreeElements } from '../src/index'
import { ReconcilerRoot, createRoot, act, extend, ThreeElement, ThreeElements, flushSync, useThree } from '../src/index'
import { suspend } from 'suspend-react'

extend(THREE as any)
Expand Down Expand Up @@ -838,4 +838,30 @@ describe('renderer', () => {
const finalUniqueNames = new Set(scene.children.map((child) => child.name))
expect(finalUniqueNames.size).toBe(scene.children.length)
})

it('should update scene synchronously with flushSync', async () => {
let updateSynchronously: (value: number) => void

function TestComponent() {
const [positionX, setPositionX] = React.useState(0)
const scene = useThree((state) => state.scene)

updateSynchronously = React.useCallback(
(value: number) => {
flushSync(() => {
setPositionX(value)
})

expect(scene.children.length).toBe(1)
expect(scene.children[0].position.x).toBe(value)
},
[scene, setPositionX],
)

return <mesh position-x={positionX} />
}

await act(async () => root.render(<TestComponent />))
await act(async () => updateSynchronously(1))
})
})
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2859,6 +2859,11 @@
resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b"
integrity sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==

"@types/react-reconciler@^0.32.0":
version "0.32.0"
resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.32.0.tgz#2152cd4f3fe4aa4f2cc235cff05075470da15b2c"
integrity sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw==

"@types/react@*":
version "18.2.73"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.73.tgz#0579548ad122660d99e00499d22e33b81e73ed94"
Expand Down