Skip to content

Commit d20a76f

Browse files
feat!: React 19 (#19)
1 parent 310f383 commit d20a76f

File tree

8 files changed

+1022
-552
lines changed

8 files changed

+1022
-552
lines changed

.github/workflows/CI.yml

Lines changed: 5 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
@@ -18,5 +18,8 @@ jobs:
1818
- name: Check build health
1919
run: yarn build
2020

21+
- name: Check for regressions
22+
run: yarn lint
23+
2124
- name: Run tests
22-
run: yarn test --silent
25+
run: yarn test

README.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ You can try a small demo here: https://codesandbox.io/s/react-nil-mvpry
2424
The following renders a logical component without a view, it renders nothing, but it has a real lifecycle and is managed by React regardless.
2525

2626
```jsx
27-
import * as React from 'react'
27+
import { useState, useEffect } from 'react'
2828
import { render } from 'react-nil'
2929

3030
function Foo() {
31-
const [active, set] = React.useState(false)
32-
React.useEffect(() => void setInterval(() => set((a) => !a), 1000), [])
31+
const [active, set] = useState(false)
32+
useEffect(() => void setInterval(() => set((a) => !a), 1000), [])
3333

3434
// false, true, ...
3535
console.log(active)
@@ -40,15 +40,23 @@ render(<Foo />)
4040

4141
We can take this further by rendering made-up elements that get returned as a reactive JSON tree from `render`.
4242

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

45-
```jsx
46-
import * as React from 'react'
47-
import { act, render } from 'react-nil'
45+
```tsx
46+
import { useState, useEffect, act } from 'react'
47+
import { render } from 'react-nil'
48+
49+
declare module 'react' {
50+
namespace JSX {
51+
interface IntrinsicElements {
52+
timestamp: Record<string, unknown>
53+
}
54+
}
55+
}
4856

49-
function Test(props) {
50-
const [value, setValue] = React.useState(-1)
51-
React.useEffect(() => setValue(Date.now()), [])
57+
function Test() {
58+
const [value, setValue] = useState(-1)
59+
useEffect(() => setValue(Date.now()), [])
5260
return <timestamp value={value} />
5361
}
5462

package.json

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-nil",
3-
"version": "1.3.1",
3+
"version": "1.3.0",
44
"description": "A react custom renderer that renders nothing but logical components",
55
"keywords": [
66
"react",
@@ -22,34 +22,39 @@
2222
"dist/*",
2323
"src/*"
2424
],
25+
"type": "module",
2526
"types": "./dist/index.d.ts",
26-
"main": "./dist/index.js",
27-
"module": "./dist/index.mjs",
27+
"main": "./dist/index.cjs",
28+
"module": "./dist/index.js",
2829
"exports": {
29-
"types": "./dist/index.d.ts",
30-
"require": "./dist/index.js",
31-
"import": "./dist/index.mjs"
30+
"require": {
31+
"types": "./dist/index.d.cts",
32+
"default": "./dist/index.cjs"
33+
},
34+
"import": {
35+
"types": "./dist/index.d.ts",
36+
"default": "./dist/index.js"
37+
}
3238
},
3339
"sideEffects": false,
3440
"devDependencies": {
35-
"@types/node": "^18.7.14",
36-
"@types/react": "^18.0.17",
37-
"react": "^18.2.0",
38-
"rimraf": "^3.0.2",
39-
"suspend-react": "^0.0.8",
40-
"typescript": "^4.7.4",
41-
"vite": "^3.0.9",
42-
"vitest": "^0.22.1"
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"
4347
},
4448
"dependencies": {
45-
"@types/react-reconciler": "^0.26.7",
46-
"react-reconciler": "^0.27.0"
49+
"@types/react-reconciler": "^0.28.9",
50+
"react-reconciler": "^0.31.0"
4751
},
4852
"peerDependencies": {
49-
"react": "^18.0.0"
53+
"react": "^19.0.0"
5054
},
5155
"scripts": {
52-
"build": "rimraf dist && vite build && tsc",
53-
"test": "vitest run"
56+
"build": "vite build",
57+
"test": "vitest run",
58+
"lint": "tsc"
5459
}
5560
}

src/index.test.tsx

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
import * as React from 'react'
2-
import { suspend } from 'suspend-react'
3-
import { vi, it, expect } from 'vitest'
4-
import { act, render, createPortal, type HostContainer } from './index'
2+
import { it, expect } from 'vitest'
3+
import { render, createPortal, type HostContainer } from './index'
54

5+
// Let React know that we'll be testing effectful components
66
declare global {
77
var IS_REACT_ACT_ENVIRONMENT: boolean
88
}
9-
10-
// Let React know that we'll be testing effectful components
11-
global.IS_REACT_ACT_ENVIRONMENT = true
12-
13-
// Mock scheduler to test React features
14-
vi.mock('scheduler', () => require('scheduler/unstable_mock'))
9+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
1510

1611
interface ReactProps<T> {
1712
key?: React.Key
1813
ref?: React.Ref<T>
1914
children?: React.ReactNode
2015
}
2116

22-
declare global {
17+
declare module 'react' {
2318
namespace JSX {
2419
interface IntrinsicElements {
2520
element: ReactProps<null> & Record<string, unknown>
@@ -32,13 +27,13 @@ it('should go through lifecycle', async () => {
3227

3328
function Test() {
3429
lifecycle.push('render')
35-
React.useImperativeHandle(React.useRef(), () => void lifecycle.push('ref'))
30+
React.useImperativeHandle(React.useRef(undefined), () => void lifecycle.push('ref'))
3631
React.useInsertionEffect(() => void lifecycle.push('useInsertionEffect'), [])
3732
React.useLayoutEffect(() => void lifecycle.push('useLayoutEffect'), [])
3833
React.useEffect(() => void lifecycle.push('useEffect'), [])
3934
return null
4035
}
41-
const container: HostContainer = await act(async () => render(<Test />))
36+
const container: HostContainer = await React.act(async () => render(<Test />))
4237

4338
expect(lifecycle).toStrictEqual(['render', 'useInsertionEffect', 'ref', 'useLayoutEffect', 'useEffect'])
4439
expect(container.head).toBe(null)
@@ -48,19 +43,19 @@ it('should render JSX', async () => {
4843
let container!: HostContainer
4944

5045
// Mount
51-
await act(async () => (container = render(<element key={1} foo />)))
46+
await React.act(async () => (container = render(<element key={1} foo />)))
5247
expect(container.head).toStrictEqual({ type: 'element', props: { foo: true }, children: [] })
5348

5449
// Remount
55-
await act(async () => (container = render(<element bar />)))
50+
await React.act(async () => (container = render(<element bar />)))
5651
expect(container.head).toStrictEqual({ type: 'element', props: { bar: true }, children: [] })
5752

5853
// Mutate
59-
await act(async () => (container = render(<element foo />)))
54+
await React.act(async () => (container = render(<element foo />)))
6055
expect(container.head).toStrictEqual({ type: 'element', props: { foo: true }, children: [] })
6156

6257
// Child mount
63-
await act(async () => {
58+
await React.act(async () => {
6459
container = render(
6560
<element foo>
6661
<element />
@@ -74,21 +69,22 @@ it('should render JSX', async () => {
7469
})
7570

7671
// Child unmount
77-
await act(async () => (container = render(<element foo />)))
72+
await React.act(async () => (container = render(<element foo />)))
7873
expect(container.head).toStrictEqual({ type: 'element', props: { foo: true }, children: [] })
7974

8075
// Unmount
81-
await act(async () => (container = render(<></>)))
76+
await React.act(async () => (container = render(<></>)))
8277
expect(container.head).toBe(null)
8378

8479
// Suspense
85-
const Test = () => (suspend(async () => null, []), (<element bar />))
86-
await act(async () => (container = render(<Test />)))
80+
const promise = Promise.resolve(null)
81+
const Test = () => (React.use(promise), (<element bar />))
82+
await React.act(async () => (container = render(<Test />)))
8783
expect(container.head).toStrictEqual({ type: 'element', props: { bar: true }, children: [] })
8884

8985
// Portals
9086
const portalContainer: HostContainer = { head: null }
91-
await act(async () => (container = render(createPortal(<element />, portalContainer))))
87+
await React.act(async () => (container = render(createPortal(<element />, portalContainer))))
9288
expect(container.head).toBe(null)
9389
expect(portalContainer.head).toStrictEqual({ type: 'element', props: {}, children: [] })
9490
})
@@ -97,29 +93,30 @@ it('should render text', async () => {
9793
let container!: HostContainer
9894

9995
// Mount
100-
await act(async () => (container = render(<>one</>)))
96+
await React.act(async () => (container = render(<>one</>)))
10197
expect(container.head).toStrictEqual({ type: 'text', props: { value: 'one' }, children: [] })
10298

10399
// Remount
104-
await act(async () => (container = render(<>one</>)))
100+
await React.act(async () => (container = render(<>one</>)))
105101
expect(container.head).toStrictEqual({ type: 'text', props: { value: 'one' }, children: [] })
106102

107103
// Mutate
108-
await act(async () => (container = render(<>two</>)))
104+
await React.act(async () => (container = render(<>two</>)))
109105
expect(container.head).toStrictEqual({ type: 'text', props: { value: 'two' }, children: [] })
110106

111107
// Unmount
112-
await act(async () => (container = render(<></>)))
108+
await React.act(async () => (container = render(<></>)))
113109
expect(container.head).toBe(null)
114110

115111
// Suspense
116-
const Test = () => (suspend(async () => null, []), (<>three</>))
117-
await act(async () => (container = render(<Test />)))
112+
const promise = Promise.resolve(null)
113+
const Test = () => (React.use(promise), (<>three</>))
114+
await React.act(async () => (container = render(<Test />)))
118115
expect(container.head).toStrictEqual({ type: 'text', props: { value: 'three' }, children: [] })
119116

120117
// Portals
121118
const portalContainer: HostContainer = { head: null }
122-
await act(async () => (container = render(createPortal('four', portalContainer))))
119+
await React.act(async () => (container = render(createPortal('four', portalContainer))))
123120
expect(container.head).toBe(null)
124121
expect(portalContainer.head).toStrictEqual({ type: 'text', props: { value: 'four' }, children: [] })
125122
})

0 commit comments

Comments
 (0)