Skip to content

Commit 104ca39

Browse files
chore: update with 19 stable
1 parent cbbfcca commit 104ca39

File tree

8 files changed

+809
-1239
lines changed

8 files changed

+809
-1239
lines changed

.github/workflows/CI.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
branches: [master]
77

88
jobs:
9-
build-test-lint:
9+
build-lint-test:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v2
@@ -22,4 +22,4 @@ jobs:
2222
run: yarn lint
2323

2424
- name: Run tests
25-
run: yarn test --silent
25+
run: yarn test

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,19 @@ We can take this further by rendering made-up elements that get returned as a re
4242

4343
You can take a snapshot for testing via `React.act` which will wait for effects and suspense to finish.
4444

45-
```jsx
45+
```tsx
4646
import { useState, useEffect, act } from 'react'
4747
import { render } from 'react-nil'
4848

49-
function Test(props) {
49+
declare module 'react' {
50+
namespace JSX {
51+
interface IntrinsicElements {
52+
timestamp: Record<string, unknown>
53+
}
54+
}
55+
}
56+
57+
function Test() {
5058
const [value, setValue] = useState(-1)
5159
useEffect(() => setValue(Date.now()), [])
5260
return <timestamp value={value} />

package.json

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-nil",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "A react custom renderer that renders nothing but logical components",
55
"keywords": [
66
"react",
@@ -38,27 +38,19 @@
3838
},
3939
"sideEffects": false,
4040
"devDependencies": {
41-
"@types/node": "^20.12.12",
42-
"@types/react": "npm:types-react@beta",
43-
"@vitejs/plugin-react": "^4.2.1",
44-
"react": "19.0.0-rc-915b914b3a-20240515",
45-
"suspend-react": "^0.1.3",
46-
"typescript": "^5.4.5",
47-
"vite": "^5.2.11",
48-
"vitest": "^1.6.0"
41+
"@types/node": "^22.10.6",
42+
"@types/react": "^19.0.0",
43+
"react": "^19.0.0",
44+
"typescript": "^5.7.3",
45+
"vite": "^6.0.7",
46+
"vitest": "^2.1.8"
4947
},
5048
"dependencies": {
51-
"@types/react-reconciler": "^0.28.8",
52-
"react-reconciler": "0.31.0-rc-915b914b3a-20240515"
49+
"@types/react-reconciler": "^0.28.9",
50+
"react-reconciler": "^0.31.0"
5351
},
5452
"peerDependencies": {
55-
"react": ">=19.0"
56-
},
57-
"overrides": {
58-
"@types/react": "npm:types-react@beta"
59-
},
60-
"resolutions": {
61-
"@types/react": "npm:types-react@beta"
53+
"react": "^19.0.0"
6254
},
6355
"scripts": {
6456
"build": "vite build",

src/index.test.tsx

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
11
import * as React from 'react'
2-
import { suspend } from 'suspend-react'
3-
import { vi, it, expect } from 'vitest'
4-
import { render, createPortal, type HostContainer } from './index.js'
5-
import type {} from 'react'
6-
import type {} from 'react/jsx-runtime'
7-
import type {} from 'react/jsx-dev-runtime'
8-
9-
// Elevate React warnings
10-
console.warn = console.error = (message: string) => {
11-
throw new Error(message)
12-
}
2+
import { it, expect } from 'vitest'
3+
import { render, createPortal, type HostContainer } from './index'
134

5+
// Let React know that we'll be testing effectful components
146
declare global {
157
var IS_REACT_ACT_ENVIRONMENT: boolean
168
}
17-
18-
// Let React know that we'll be testing effectful components
19-
global.IS_REACT_ACT_ENVIRONMENT = true
20-
21-
// Mock scheduler to test React features
22-
vi.mock('scheduler', () => require('scheduler/unstable_mock'))
9+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
2310

2411
interface ReactProps<T> {
2512
key?: React.Key
@@ -35,31 +22,12 @@ declare module 'react' {
3522
}
3623
}
3724

38-
declare module 'react/jsx-runtime' {
39-
namespace JSX {
40-
interface IntrinsicElements {
41-
element: ReactProps<null> & Record<string, unknown>
42-
}
43-
}
44-
}
45-
46-
declare module 'react/jsx-dev-runtime' {
47-
namespace JSX {
48-
interface IntrinsicElements {
49-
element: ReactProps<null> & Record<string, unknown>
50-
}
51-
}
52-
}
53-
5425
it('should go through lifecycle', async () => {
5526
const lifecycle: string[] = []
5627

5728
function Test() {
5829
lifecycle.push('render')
59-
React.useImperativeHandle(React.useRef(null), () => {
60-
lifecycle.push('ref')
61-
return null
62-
})
30+
React.useImperativeHandle(React.useRef(undefined), () => void lifecycle.push('ref'))
6331
React.useInsertionEffect(() => void lifecycle.push('useInsertionEffect'), [])
6432
React.useLayoutEffect(() => void lifecycle.push('useLayoutEffect'), [])
6533
React.useEffect(() => void lifecycle.push('useEffect'), [])
@@ -109,7 +77,8 @@ it('should render JSX', async () => {
10977
expect(container.head).toBe(null)
11078

11179
// Suspense
112-
const Test = () => (suspend(async () => null, []), (<element bar />))
80+
const promise = Promise.resolve(null)
81+
const Test = () => (React.use(promise), (<element bar />))
11382
await React.act(async () => (container = render(<Test />)))
11483
expect(container.head).toStrictEqual({ type: 'element', props: { bar: true }, children: [] })
11584

@@ -140,7 +109,8 @@ it('should render text', async () => {
140109
expect(container.head).toBe(null)
141110

142111
// Suspense
143-
const Test = () => (suspend(async () => null, []), (<>three</>))
112+
const promise = Promise.resolve(null)
113+
const Test = () => (React.use(promise), (<>three</>))
144114
await React.act(async () => (container = render(<Test />)))
145115
expect(container.head).toStrictEqual({ type: 'text', props: { value: 'three' }, children: [] })
146116

src/index.tsx

Lines changed: 97 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import { version, type ReactNode, type JSX } from 'react'
1+
import * as React from 'react'
22
import Reconciler from 'react-reconciler'
3-
import { NoEventPriority, DefaultEventPriority, ConcurrentRoot } from 'react-reconciler/constants.js'
3+
import {
4+
// NoEventPriority,
5+
ContinuousEventPriority,
6+
DiscreteEventPriority,
7+
DefaultEventPriority,
8+
ConcurrentRoot,
9+
} from 'react-reconciler/constants.js'
10+
11+
// @ts-ignore
12+
const __DEV__ = /* @__PURE__ */ (() => typeof process !== 'undefined' && process.env.NODE_ENV !== 'production')()
413

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

9-
const createReconciler = Reconciler as unknown as <
18+
function createReconciler<
1019
Type,
1120
Props,
1221
Container,
@@ -56,6 +65,7 @@ const createReconciler = Reconciler as unknown as <
5665
// Undocumented
5766
// https://github.com/facebook/react/pull/26722
5867
NotPendingTransition: TransitionStatus | null
68+
HostTransitionContext: React.Context<TransitionStatus>
5969
// https://github.com/facebook/react/pull/28751
6070
setCurrentUpdatePriority(newPriority: EventPriority): void
6171
getCurrentUpdatePriority(): EventPriority
@@ -66,6 +76,11 @@ const createReconciler = Reconciler as unknown as <
6676
requestPostPaintCallback(callback: (time: number) => void): void
6777
// https://github.com/facebook/react/pull/26025
6878
shouldAttemptEagerTransition(): boolean
79+
// https://github.com/facebook/react/pull/31528
80+
trackSchedulerEvent(): void
81+
// https://github.com/facebook/react/pull/31008
82+
resolveEventType(): null | string
83+
resolveEventTimeStamp(): number
6984

7085
/**
7186
* 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.
@@ -93,12 +108,20 @@ const createReconciler = Reconciler as unknown as <
93108
*/
94109
waitForCommitToBeReady(): ((initiateCommit: Function) => Function) | null
95110
},
96-
) => Reconciler.Reconciler<Container, Instance, TextInstance, SuspenseInstance, PublicInstance>
111+
): Reconciler.Reconciler<Container, Instance, TextInstance, SuspenseInstance, PublicInstance> {
112+
const reconciler = Reconciler(config as any)
113+
114+
reconciler.injectIntoDevTools({
115+
bundleType: __DEV__ ? 1 : 0,
116+
rendererPackageName: 'react-nil',
117+
version: React.version,
118+
})
97119

98-
declare module 'react-reconciler/constants.js' {
99-
const NoEventPriority = 0
120+
return reconciler as any
100121
}
101122

123+
const NoEventPriority = 0
124+
102125
export interface NilNode<P = Record<string, unknown>> {
103126
type: string
104127
props: P
@@ -117,8 +140,8 @@ interface HostConfig {
117140
textInstance: NilNode
118141
suspenseInstance: NilNode
119142
hydratableInstance: never
120-
formInstance: never
121143
publicInstance: null
144+
formInstance: never
122145
hostContext: {}
123146
childSet: never
124147
timeoutHandle: number
@@ -142,7 +165,7 @@ const NO_CONTEXT: HostConfig['hostContext'] = {}
142165

143166
let currentUpdatePriority: number = NoEventPriority
144167

145-
const reconciler = createReconciler<
168+
const reconciler = /* @__PURE__ */ createReconciler<
146169
HostConfig['type'],
147170
HostConfig['props'],
148171
HostConfig['container'],
@@ -159,6 +182,7 @@ const reconciler = createReconciler<
159182
HostConfig['TransitionStatus']
160183
>({
161184
isPrimaryRenderer: false,
185+
warnsIfNotActing: false,
162186
supportsMutation: true,
163187
supportsPersistence: false,
164188
supportsHydration: false,
@@ -182,70 +206,104 @@ const reconciler = createReconciler<
182206
getChildHostContext: () => NO_CONTEXT,
183207
shouldSetTextContent: () => false,
184208
finalizeInitialChildren: () => false,
185-
commitUpdate: (instance, _, __, props) => (instance.props = getInstanceProps(props)),
209+
commitUpdate: (instance, _type, _prevProps, nextProps) => (instance.props = getInstanceProps(nextProps)),
186210
commitTextUpdate: (instance, _, value) => (instance.props.value = value),
187211
prepareForCommit: () => null,
188212
resetAfterCommit() {},
189213
preparePortalMount() {},
190214
clearContainer: (container) => (container.head = null),
191-
warnsIfNotActing: false,
192215
getInstanceFromNode: () => null,
193216
beforeActiveInstanceBlur() {},
194217
afterActiveInstanceBlur() {},
195218
detachDeletedInstance() {},
196219
prepareScopeUpdate() {},
197220
getInstanceFromScope: () => null,
198-
setCurrentUpdatePriority(newPriority) {
221+
shouldAttemptEagerTransition: () => false,
222+
trackSchedulerEvent: () => {},
223+
resolveEventType: () => null,
224+
resolveEventTimeStamp: () => -1.1,
225+
requestPostPaintCallback() {},
226+
maySuspendCommit: () => false,
227+
preloadInstance: () => true, // true indicates already loaded
228+
startSuspendingCommit() {},
229+
suspendInstance() {},
230+
waitForCommitToBeReady: () => null,
231+
NotPendingTransition: null,
232+
HostTransitionContext: /* @__PURE__ */ React.createContext<HostConfig['TransitionStatus']>(null),
233+
setCurrentUpdatePriority(newPriority: number) {
199234
currentUpdatePriority = newPriority
200235
},
201236
getCurrentUpdatePriority() {
202237
return currentUpdatePriority
203238
},
204239
resolveUpdatePriority() {
205-
return currentUpdatePriority || DefaultEventPriority
206-
},
207-
shouldAttemptEagerTransition() {
208-
return false
209-
},
210-
requestPostPaintCallback() {},
211-
maySuspendCommit() {
212-
return false
213-
},
214-
preloadInstance() {
215-
return true // true indicates already loaded
216-
},
217-
startSuspendingCommit() {},
218-
suspendInstance() {},
219-
waitForCommitToBeReady() {
220-
return null
240+
if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority
241+
242+
switch (typeof window !== 'undefined' && window.event?.type) {
243+
case 'click':
244+
case 'contextmenu':
245+
case 'dblclick':
246+
case 'pointercancel':
247+
case 'pointerdown':
248+
case 'pointerup':
249+
return DiscreteEventPriority
250+
case 'pointermove':
251+
case 'pointerout':
252+
case 'pointerover':
253+
case 'pointerenter':
254+
case 'pointerleave':
255+
case 'wheel':
256+
return ContinuousEventPriority
257+
default:
258+
return DefaultEventPriority
259+
}
221260
},
222-
NotPendingTransition: null,
223261
resetFormInstance() {},
224262
})
225263

226-
// Inject renderer meta into devtools
227-
const isProd = typeof process === 'undefined' || process.env?.['NODE_ENV'] === 'production'
228-
reconciler.injectIntoDevTools({
229-
findFiberByHostInstance: () => null,
230-
bundleType: isProd ? 0 : 1,
231-
version,
232-
rendererPackageName: 'react-nil',
233-
})
264+
/**
265+
* Force React to flush any updates inside the provided callback synchronously and immediately.
266+
*/
267+
export function flushSync<R>(fn: () => R): R {
268+
return reconciler.flushSync(fn)
269+
}
270+
271+
// Report when an error was detected in a previous render
272+
// https://github.com/facebook/react/pull/23207
273+
const logRecoverableError = /* @__PURE__ */ (() =>
274+
typeof reportError === 'function'
275+
? // In modern browsers, reportError will dispatch an error event,
276+
// emulating an uncaught JavaScript error.
277+
reportError
278+
: // In older browsers and test environments, fallback to console.error.
279+
console.error)()
234280

235281
const container: HostContainer = { head: null }
236-
const root = reconciler.createContainer(container, ConcurrentRoot, null, false, null, '', console.error, null)
282+
const root = /* @__PURE__ */ (reconciler as any).createContainer(
283+
container, // containerInfo
284+
ConcurrentRoot, // tag
285+
null, // hydrationCallbacks
286+
false, // isStrictMode
287+
null, // concurrentUpdatesByDefaultOverride
288+
'', // identifierPrefix
289+
logRecoverableError, // onUncaughtError
290+
logRecoverableError, // onCaughtError
291+
logRecoverableError, // onRecoverableError
292+
null, // transitionCallbacks
293+
)
237294

238295
/**
239296
* Renders a React element into a `null` root.
240297
*/
241-
export function render(element: ReactNode): HostContainer {
298+
export function render(element: React.ReactNode): HostContainer {
242299
reconciler.updateContainer(element, root, null, undefined)
243300
return container
244301
}
245302

246303
/**
247304
* Renders a React element into a foreign {@link HostContainer}.
248305
*/
249-
export function createPortal(element: ReactNode, container: HostContainer): JSX.Element {
306+
export function createPortal(element: React.ReactNode, container: HostContainer): React.JSX.Element {
307+
// @ts-expect-error
250308
return <>{reconciler.createPortal(element, container, null, null)}</>
251309
}

0 commit comments

Comments
 (0)