Skip to content

Commit

Permalink
feat!: React 19 (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett authored Jan 13, 2025
1 parent 310f383 commit d20a76f
Show file tree
Hide file tree
Showing 8 changed files with 1,022 additions and 552 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
branches: [master]

jobs:
build-test-lint:
build-lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand All @@ -18,5 +18,8 @@ jobs:
- name: Check build health
run: yarn build

- name: Check for regressions
run: yarn lint

- name: Run tests
run: yarn test --silent
run: yarn test
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ You can try a small demo here: https://codesandbox.io/s/react-nil-mvpry
The following renders a logical component without a view, it renders nothing, but it has a real lifecycle and is managed by React regardless.

```jsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import { render } from 'react-nil'

function Foo() {
const [active, set] = React.useState(false)
React.useEffect(() => void setInterval(() => set((a) => !a), 1000), [])
const [active, set] = useState(false)
useEffect(() => void setInterval(() => set((a) => !a), 1000), [])

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

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

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

```jsx
import * as React from 'react'
import { act, render } from 'react-nil'
```tsx
import { useState, useEffect, act } from 'react'
import { render } from 'react-nil'

declare module 'react' {
namespace JSX {
interface IntrinsicElements {
timestamp: Record<string, unknown>
}
}
}

function Test(props) {
const [value, setValue] = React.useState(-1)
React.useEffect(() => setValue(Date.now()), [])
function Test() {
const [value, setValue] = useState(-1)
useEffect(() => setValue(Date.now()), [])
return <timestamp value={value} />
}

Expand Down
43 changes: 24 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-nil",
"version": "1.3.1",
"version": "1.3.0",
"description": "A react custom renderer that renders nothing but logical components",
"keywords": [
"react",
Expand All @@ -22,34 +22,39 @@
"dist/*",
"src/*"
],
"type": "module",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"devDependencies": {
"@types/node": "^18.7.14",
"@types/react": "^18.0.17",
"react": "^18.2.0",
"rimraf": "^3.0.2",
"suspend-react": "^0.0.8",
"typescript": "^4.7.4",
"vite": "^3.0.9",
"vitest": "^0.22.1"
"@types/node": "^22.10.6",
"@types/react": "^19.0.0",
"react": "^19.0.0",
"typescript": "^5.7.3",
"vite": "^6.0.7",
"vitest": "^2.1.8"
},
"dependencies": {
"@types/react-reconciler": "^0.26.7",
"react-reconciler": "^0.27.0"
"@types/react-reconciler": "^0.28.9",
"react-reconciler": "^0.31.0"
},
"peerDependencies": {
"react": "^18.0.0"
"react": "^19.0.0"
},
"scripts": {
"build": "rimraf dist && vite build && tsc",
"test": "vitest run"
"build": "vite build",
"test": "vitest run",
"lint": "tsc"
}
}
53 changes: 25 additions & 28 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import * as React from 'react'
import { suspend } from 'suspend-react'
import { vi, it, expect } from 'vitest'
import { act, render, createPortal, type HostContainer } from './index'
import { it, expect } from 'vitest'
import { render, createPortal, type HostContainer } from './index'

// Let React know that we'll be testing effectful components
declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean
}

// Let React know that we'll be testing effectful components
global.IS_REACT_ACT_ENVIRONMENT = true

// Mock scheduler to test React features
vi.mock('scheduler', () => require('scheduler/unstable_mock'))
globalThis.IS_REACT_ACT_ENVIRONMENT = true

interface ReactProps<T> {
key?: React.Key
ref?: React.Ref<T>
children?: React.ReactNode
}

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

function Test() {
lifecycle.push('render')
React.useImperativeHandle(React.useRef(), () => void lifecycle.push('ref'))
React.useImperativeHandle(React.useRef(undefined), () => void lifecycle.push('ref'))
React.useInsertionEffect(() => void lifecycle.push('useInsertionEffect'), [])
React.useLayoutEffect(() => void lifecycle.push('useLayoutEffect'), [])
React.useEffect(() => void lifecycle.push('useEffect'), [])
return null
}
const container: HostContainer = await act(async () => render(<Test />))
const container: HostContainer = await React.act(async () => render(<Test />))

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

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

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

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

// Child mount
await act(async () => {
await React.act(async () => {
container = render(
<element foo>
<element />
Expand All @@ -74,21 +69,22 @@ it('should render JSX', async () => {
})

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

// Unmount
await act(async () => (container = render(<></>)))
await React.act(async () => (container = render(<></>)))
expect(container.head).toBe(null)

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

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

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

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

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

// Unmount
await act(async () => (container = render(<></>)))
await React.act(async () => (container = render(<></>)))
expect(container.head).toBe(null)

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

// Portals
const portalContainer: HostContainer = { head: null }
await act(async () => (container = render(createPortal('four', portalContainer))))
await React.act(async () => (container = render(createPortal('four', portalContainer))))
expect(container.head).toBe(null)
expect(portalContainer.head).toStrictEqual({ type: 'text', props: { value: 'four' }, children: [] })
})
Loading

0 comments on commit d20a76f

Please sign in to comment.