Skip to content

Feat: React 19 Upgrade #2363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: next
Choose a base branch
from
2 changes: 1 addition & 1 deletion docs/app/components/Header/HeaderSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { MenuDocs } from '../Menu/MenuDocs'

import { NavigationSchema } from '../../../scripts/docs/navigation'
import { SiteThemePicker } from '../Site/SiteThemePicker'
import { forwardRef } from 'react'
import {
mainNavigation,
mobileDialogHeader,
Expand All @@ -21,6 +20,7 @@ import {
subNavContainer,
} from './HeaderSidePanel.css'
import { visuallyHidden } from '../../styles/utilities.css'
import { forwardRef } from 'react'

interface HeaderSidePanelProps {
isOpen: boolean
Expand Down
32 changes: 13 additions & 19 deletions docs/app/components/Text/Copy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,17 @@ export interface CopyProps {
tag?: keyof Pick<JSX.IntrinsicElements, 'p' | 'blockquote' | 'div' | 'label'>
}

export const Copy = forwardRef<
| HTMLHeadingElement
| HTMLQuoteElement
| HTMLDivElement
| HTMLLabelElement
| HTMLParagraphElement,
CopyProps
>(({ fontStyle = 'XS', className, children, tag = 'p' }, ref) => {
const Element = tag
export const Copy = forwardRef<any, CopyProps>(
({ fontStyle = 'XS', className, children, tag = 'p' }, ref) => {
const Element = tag

return (
<Element
className={clsx(FontSizes[fontStyle], copy, className)}
// @ts-expect-error – TODO: fix this
ref={ref}
>
{children}
</Element>
)
})
return (
<Element
className={clsx(FontSizes[fontStyle], copy, className)}
ref={ref}
>
{children}
</Element>
)
}
)
2 changes: 1 addition & 1 deletion docs/app/components/Text/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface HeadingProps {
style?: CSSProperties
}

export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(
export const Heading = forwardRef<any, HeadingProps>(
(
{
tag = 'h1',
Expand Down
3 changes: 1 addition & 2 deletions docs/app/components/Text/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ export interface ListProps {
children?: ReactNode
}

export const List = forwardRef<HTMLUListElement | HTMLOListElement, ListProps>(
export const List = forwardRef<HTMLOListElement, ListProps>(
({ tag = 'ul', fontStyle = 'XS', className, children }, ref) => {
const Element = tag

return (
<Element
className={clsx(FontSizes[fontStyle], list, className)}
// @ts-expect-error - TODO: polymorphic refs, woo.
ref={ref}
>
{children}
Expand Down
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@remix-run/serve": "2.15.2",
"@remix-run/server-runtime": "2.15.2",
"@supabase/supabase-js": "2.47.10",
"@use-gesture/react": "^10.3.1",
"@vanilla-extract/css": "1.17.0",
"@vanilla-extract/dynamic": "2.1.2",
"@vanilla-extract/recipes": "0.5.5",
Expand All @@ -44,6 +45,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-select": "5.9.0",
"react-use-measure": "^2.1.1",
"zod": "3.24.1"
},
"devDependencies": {
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@changesets/cli": "2.27.11",
"@commitlint/cli": "19.6.1",
"@commitlint/config-conventional": "19.6.0",
"@react-three/fiber": "8.17.10",
"@react-three/fiber": "^9.1.0",
"@remix-run/dev": "2.15.2",
"@simonsmith/cypress-image-snapshot": "9.1.0",
"@swc/core": "1.10.4",
Expand All @@ -79,8 +79,8 @@
"@types/jest": "29.5.14",
"@types/lodash.clamp": "4.0.9",
"@types/lodash.shuffle": "4.2.9",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react-lazyload": "3.2.3",
"@types/react-native": "0.73.0",
"@types/styled-components": "5.1.34",
Expand All @@ -95,8 +95,8 @@
"mock-raf": "npm:@react-spring/[email protected]",
"prettier": "3.4.2",
"pretty-quick": "4.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-konva": "18.2.10",
"react-native": "0.76.5",
"react-zdog": "1.2.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/animated/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@
"@react-spring/types": "~9.7.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
}
6 changes: 3 additions & 3 deletions packages/animated/src/withAnimated.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import { forwardRef, useRef, Ref, useCallback, useEffect } from 'react'
import { useRef, Ref, useCallback, useEffect, forwardRef } from 'react'
import {
is,
each,
Expand Down Expand Up @@ -27,7 +27,7 @@ export const withAnimated = (Component: any, host: HostConfig) => {
!is.fun(Component) ||
(Component.prototype && Component.prototype.isReactComponent)

return forwardRef((givenProps: any, givenRef: Ref<any>) => {
return forwardRef((givenProps: any, givenRef) => {
const instanceRef = useRef<any>(null)

// The `hasInstance` value is constant, so we can safely avoid
Expand Down Expand Up @@ -66,7 +66,7 @@ export const withAnimated = (Component: any, host: HostConfig) => {

const observer = new PropsObserver(callback, deps)

const observerRef = useRef<PropsObserver>()
const observerRef = useRef<PropsObserver>(null)
useIsomorphicLayoutEffect(() => {
observerRef.current = observer

Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@
"@react-spring/types": "~9.7.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
}
10 changes: 5 additions & 5 deletions packages/core/src/SpringContext.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { render, RenderResult } from '@testing-library/react'
import { SpringContext } from './SpringContext'
import { SpringContextProvider, SpringContext } from './SpringContext'
import { SpringValue } from './SpringValue'
import { useSpring } from './hooks'

Expand All @@ -13,9 +13,9 @@ describe('SpringContext', () => {
}

const update = createUpdater(props => (
<SpringContext {...props}>
<SpringContextProvider {...props}>
<Child />
</SpringContext>
</SpringContextProvider>
))

it('only merges when changed', () => {
Expand All @@ -27,9 +27,9 @@ describe('SpringContext', () => {
}

const getRoot = () => (
<SpringContext {...context}>
<SpringContextProvider {...context}>
<Test />
</SpringContext>
</SpringContextProvider>
)

const expectUpdates = (updates: any[]) => {
Expand Down
41 changes: 18 additions & 23 deletions packages/core/src/SpringContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react'
import { useContext, PropsWithChildren } from 'react'
import { useMemoOne } from '@react-spring/shared'

/**
* This context affects all new and existing `SpringValue` objects
Expand All @@ -13,33 +12,29 @@ export interface SpringContext {
immediate?: boolean
}

export const SpringContext = ({
export const SpringContext = React.createContext<SpringContext>({
pause: false,
immediate: false,
})

export const SpringContextProvider = ({
children,
...props
}: PropsWithChildren<SpringContext>) => {
const inherited = useContext(ctx)
const inherited = useContext(SpringContext)

// Inherited values are dominant when truthy.
const pause = props.pause || !!inherited.pause,
immediate = props.immediate || !!inherited.immediate
const pause = props.pause ?? inherited.pause ?? false
const immediate = props.immediate ?? inherited.immediate ?? false

// Memoize the context to avoid unwanted renders.
props = useMemoOne(() => ({ pause, immediate }), [pause, immediate])

const { Provider } = ctx
return <Provider value={props}>{children}</Provider>
}

const ctx = makeContext(SpringContext, {} as SpringContext)

// Allow `useContext(SpringContext)` in TypeScript.
SpringContext.Provider = ctx.Provider
SpringContext.Consumer = ctx.Consumer

/** Make the `target` compatible with `useContext` */
function makeContext<T>(target: any, init: T): React.Context<T> {
Object.assign(target, React.createContext(init))
target.Provider._context = target
target.Consumer._context = target
return target
const contextValue = React.useMemo(
() => ({ pause, immediate }),
[pause, immediate]
)
return (
<SpringContext.Provider value={contextValue}>
{children}
</SpringContext.Provider>
)
}
2 changes: 1 addition & 1 deletion packages/core/src/hooks/useInView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function useInView<TElement extends HTMLElement>(
args?: IntersectionArgs
) {
const [isInView, setIsInView] = useState(false)
const ref = useRef<TElement>()
const ref = useRef<TElement>(null)

const propsFn = is.fun(props) && props

Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/hooks/useSpring.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import { render, RenderResult } from '@testing-library/react'
import { is } from '@react-spring/shared'
import { Lookup } from '@react-spring/types'
import { SpringContext } from '../SpringContext'
import { SpringContextProvider, SpringContext } from '../SpringContext'
import { SpringValue } from '../SpringValue'
import { SpringRef } from '../SpringRef'
import { useSpring } from './useSpring'
Expand Down Expand Up @@ -140,7 +140,9 @@ function createUpdater(Component: React.ComponentType<{ args: [any, any?] }>) {
})

function renderWithContext(elem: JSX.Element) {
const wrapped = <SpringContext {...context}>{elem}</SpringContext>
const wrapped = (
<SpringContextProvider {...context}>{elem}</SpringContextProvider>
)
if (result) result.rerender(wrapped)
else result = render(wrapped)
return result
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/hooks/useSprings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
// Create a local ref if a props function or deps array is ever passed.
const ref = useMemo(
() => (propsFn || arguments.length == 3 ? SpringRef() : void 0),
[]

Check warning on line 85 in packages/core/src/hooks/useSprings.ts

View workflow job for this annotation

GitHub Actions / Style Checks

React Hook useMemo has a missing dependency: 'propsFn'. Either include it or remove the dependency array
)

interface State {
Expand Down Expand Up @@ -124,11 +124,12 @@
})
},
}),
[]

Check warning on line 127 in packages/core/src/hooks/useSprings.ts

View workflow job for this annotation

GitHub Actions / Style Checks

React Hook useMemo has a missing dependency: 'forceUpdate'. Either include it or remove the dependency array
)

const ctrls = useRef([...state.ctrls])
const updates: any[] = []
const updates = useRef<any[]>(null!)
updates.current ??= []

// Cache old controllers to dispose in the commit phase.
const prevLength = usePrev(length) || 0
Expand All @@ -144,13 +145,13 @@
ctrls.current.length = length

declareUpdates(prevLength, length)
}, [length])

Check warning on line 148 in packages/core/src/hooks/useSprings.ts

View workflow job for this annotation

GitHub Actions / Style Checks

React Hook useMemo has missing dependencies: 'declareUpdates', 'prevLength', and 'ref'. Either include them or remove the dependency array

// Update existing controllers when "deps" are changed.
useMemo(() => {
declareUpdates(0, Math.min(prevLength, length))
// @ts-expect-error – we want to allow passing undefined to useMemo
}, deps)

Check warning on line 154 in packages/core/src/hooks/useSprings.ts

View workflow job for this annotation

GitHub Actions / Style Checks

React Hook useMemo has missing dependencies: 'declareUpdates', 'length', and 'prevLength'. Either include them or remove the dependency array

Check warning on line 154 in packages/core/src/hooks/useSprings.ts

View workflow job for this annotation

GitHub Actions / Style Checks

React Hook useMemo was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies

/** Fill the `updates` array with declarative updates for the given index range. */
function declareUpdates(startIndex: number, endIndex: number) {
Expand All @@ -164,15 +165,17 @@
: (props as any)[i]

if (update) {
updates[i] = declareUpdate(update)
updates.current[i] = declareUpdate(update)
}
}
}

// New springs are created during render so users can pass them to
// their animated components, but new springs aren't cached until the
// commit phase (see the `useIsomorphicLayoutEffect` callback below).
const springs = ctrls.current.map((ctrl, i) => getSprings(ctrl, updates[i]))
const springs = ctrls.current.map((ctrl, i) =>
getSprings(ctrl, updates.current[i])
)

const context = useContext(SpringContext)
const prevContext = usePrev(context)
Expand Down Expand Up @@ -202,7 +205,7 @@
}

// Apply updates created during render.
const update = updates[i]
const update = updates.current[i]
if (update) {
// Update the injected ref if needed.
replaceRef(ctrl, update.ref)
Expand All @@ -214,6 +217,8 @@
} else {
ctrl.start(update)
}

updates.current[i] = null
}
})
})
Expand Down
7 changes: 2 additions & 5 deletions packages/core/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ beforeEach(() => {
requestAnimationFrame: global.mockRaf.raf,
colors,
skipAnimation: false,
// This lets our useTransition hook force its component
// to update from within an "onRest" handler.
batchedUpdates: act,
})
})

Expand Down Expand Up @@ -138,7 +135,7 @@ global.advanceUntil = async test => {
willAdvance: observe,
})

jest.advanceTimersByTime(1000 / 60)
await act(() => jest.advanceTimersByTimeAsync(1000 / 60))
global.mockRaf.step()

// Stop observing after the frame is processed.
Expand All @@ -147,7 +144,7 @@ global.advanceUntil = async test => {
}

// Ensure pending effects are flushed.
await flushMicroTasks()
await act(() => flushMicroTasks())

// Prevent infinite recursion.
if (++steps > 1e3) {
Expand Down
4 changes: 2 additions & 2 deletions packages/parallax/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@react-spring/web": "~9.7.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
}
6 changes: 3 additions & 3 deletions packages/parallax/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const ParallaxLayer = React.memo(

React.useImperativeHandle(ref, () => layer)

const layerRef = useRef<any>()
const layerRef = useRef<any>(null)

const setSticky = (height: number, scrollTop: number) => {
const start = layer.sticky!.start! * height
Expand Down Expand Up @@ -229,8 +229,8 @@ export const Parallax = React.memo(
...rest
} = props

const containerRef = useRef<any>()
const contentRef = useRef<any>()
const containerRef = useRef<any>(null)
const contentRef = useRef<any>(null)

const state: IParallax = useMemoOne(
() => ({
Expand Down
Loading
Loading