From 3a8e509102a9734e3ae7195fc1a5be5d6de40bca Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 20 Mar 2025 13:39:30 +0000 Subject: [PATCH 01/10] feat: Bumped deps and refactored usages of forwardRef --- .../app/components/Header/HeaderSidePanel.tsx | 143 ++--- docs/app/components/Text/Copy.tsx | 26 +- docs/app/components/Text/Heading.tsx | 61 +- docs/app/components/Text/List.tsx | 31 +- docs/package.json | 2 + package.json | 10 +- packages/animated/src/withAnimated.tsx | 8 +- packages/core/src/SpringContext.tsx | 65 ++- packages/core/src/hooks/useInView.ts | 2 +- packages/parallax/src/index.tsx | 532 +++++++++--------- packages/shared/src/hooks/useMemoOne.ts | 2 +- packages/shared/src/hooks/usePrev.ts | 2 +- targets/three/src/index.ts | 1 - targets/web/src/animated.test.tsx | 28 +- yarn.lock | 176 +++++- 15 files changed, 632 insertions(+), 457 deletions(-) diff --git a/docs/app/components/Header/HeaderSidePanel.tsx b/docs/app/components/Header/HeaderSidePanel.tsx index c1b2fc224..6b50d9932 100644 --- a/docs/app/components/Header/HeaderSidePanel.tsx +++ b/docs/app/components/Header/HeaderSidePanel.tsx @@ -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, @@ -26,80 +25,82 @@ interface HeaderSidePanelProps { isOpen: boolean submenu?: NavigationSchema onNavigationClick?: () => void + ref?: React.RefObject } -export const HeaderSidePanel = forwardRef( - ({ isOpen, submenu, onNavigationClick }, ref) => { - const location = useLocation() +export const HeaderSidePanel = ({ + isOpen, + submenu, + onNavigationClick, + ref, +}: HeaderSidePanelProps) => { + const location = useLocation() - const isDocs = location.pathname.includes('/docs') + const isDocs = location.pathname.includes('/docs') - const transitions = useTransition(isOpen, { - from: { - x: '100%', - opacity: 0, - }, - enter: { - x: '0', - opacity: 1, - }, - leave: { - x: '100%', - opacity: 0, - }, - config: { - tension: 210, - friction: 30, - mass: 1, - }, - }) + const transitions = useTransition(isOpen, { + from: { + x: '100%', + opacity: 0, + }, + enter: { + x: '0', + opacity: 1, + }, + leave: { + x: '100%', + opacity: 0, + }, + config: { + tension: 210, + friction: 30, + mass: 1, + }, + }) - const handleNavClick = () => { - if (onNavigationClick) { - onNavigationClick() - } + const handleNavClick = () => { + if (onNavigationClick) { + onNavigationClick() } - - return transitions(({ opacity, x }, item) => - item ? ( - <> - - - - {/* @ts-ignore */} - - -
-
- - - - - - -
- - Main Menu - - -
- - - - -
-
- - ) : null - ) } -) + + return transitions(({ opacity, x }, item) => + item ? ( + <> + + + + {/* @ts-ignore */} + + +
+
+ + + + + + +
+ Main Menu + +
+ + + + +
+
+ + ) : null + ) +} diff --git a/docs/app/components/Text/Copy.tsx b/docs/app/components/Text/Copy.tsx index 44c697fe9..01d09f6f0 100644 --- a/docs/app/components/Text/Copy.tsx +++ b/docs/app/components/Text/Copy.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { forwardRef, ReactNode } from 'react' +import { ReactNode } from 'react' import { copy } from './Copy.css' import * as FontSizes from '../../styles/fontStyles.css' @@ -9,25 +9,21 @@ export interface CopyProps { className?: string children?: ReactNode tag?: keyof Pick + ref?: React.RefObject } -export const Copy = forwardRef< - | HTMLHeadingElement - | HTMLQuoteElement - | HTMLDivElement - | HTMLLabelElement - | HTMLParagraphElement, - CopyProps ->(({ fontStyle = 'XS', className, children, tag = 'p' }, ref) => { +export const Copy = ({ + fontStyle = 'XS', + className, + children, + tag = 'p', + ref, +}: CopyProps) => { const Element = tag return ( - + {children} ) -}) +} diff --git a/docs/app/components/Text/Heading.tsx b/docs/app/components/Text/Heading.tsx index 1527dc7fa..437aad68d 100644 --- a/docs/app/components/Text/Heading.tsx +++ b/docs/app/components/Text/Heading.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, forwardRef, ReactNode } from 'react' +import { CSSProperties, ReactNode } from 'react' import { Link } from 'phosphor-react' @@ -17,37 +17,34 @@ export interface HeadingProps { isLink?: boolean weight?: keyof FontSizes.FontWeights style?: CSSProperties + ref?: React.RefObject } -export const Heading = forwardRef( - ( - { - tag = 'h1', - fontStyle = 'S', - weight = 'default', - className, - children, - isLink = false, - ...restProps - }, - ref - ) => { - const Element = tag +export const Heading = ({ + tag = 'h1', + fontStyle = 'S', + weight = 'default', + className, + children, + isLink = false, + ref, + ...restProps +}: HeadingProps) => { + const Element = tag - return ( - - {children} - {isLink ? : null} - - ) - } -) + return ( + + {children} + {isLink ? : null} + + ) +} diff --git a/docs/app/components/Text/List.tsx b/docs/app/components/Text/List.tsx index ecef793db..7fd38be89 100644 --- a/docs/app/components/Text/List.tsx +++ b/docs/app/components/Text/List.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { forwardRef, ReactNode } from 'react' +import { ReactNode } from 'react' import * as FontSizes from '../../styles/fontStyles.css' import { descriptiveList, list } from './List.css' @@ -8,23 +8,24 @@ export interface ListProps { fontStyle?: keyof FontSizes.FontSizes className?: string children?: ReactNode + ref?: React.RefObject } -export const List = forwardRef( - ({ tag = 'ul', fontStyle = 'XS', className, children }, ref) => { - const Element = tag +export const List = ({ + tag = 'ul', + fontStyle = 'XS', + className, + children, + ref, +}: ListProps) => { + const Element = tag - return ( - - {children} - - ) - } -) + return ( + + {children} + + ) +} interface DescriptiveListProps { data: [title: string, item: ReactNode][] diff --git a/docs/package.json b/docs/package.json index 032793171..3ced67a02 100644 --- a/docs/package.json +++ b/docs/package.json @@ -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", @@ -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": { diff --git a/package.json b/package.json index 6429c5e7f..c1826114c 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -95,8 +95,8 @@ "mock-raf": "npm:@react-spring/mock-raf@1.1.1", "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", diff --git a/packages/animated/src/withAnimated.tsx b/packages/animated/src/withAnimated.tsx index ba715d8dd..071717c4f 100644 --- a/packages/animated/src/withAnimated.tsx +++ b/packages/animated/src/withAnimated.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { forwardRef, useRef, Ref, useCallback, useEffect } from 'react' +import { useRef, Ref, useCallback, useEffect } from 'react' import { is, each, @@ -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) => { + return (givenProps: any, givenRef: Ref) => { const instanceRef = useRef(null) // The `hasInstance` value is constant, so we can safely avoid @@ -66,7 +66,7 @@ export const withAnimated = (Component: any, host: HostConfig) => { const observer = new PropsObserver(callback, deps) - const observerRef = useRef() + const observerRef = useRef(null) useIsomorphicLayoutEffect(() => { observerRef.current = observer @@ -94,7 +94,7 @@ export const withAnimated = (Component: any, host: HostConfig) => { const usedProps = host.getComponentProps(props.getValue()) return - }) + } } class PropsObserver { diff --git a/packages/core/src/SpringContext.tsx b/packages/core/src/SpringContext.tsx index 8e0a48d6f..0667c310a 100644 --- a/packages/core/src/SpringContext.tsx +++ b/packages/core/src/SpringContext.tsx @@ -13,33 +13,54 @@ export interface SpringContext { immediate?: boolean } -export const SpringContext = ({ - children, - ...props -}: PropsWithChildren) => { - const inherited = useContext(ctx) +export const SpringContext = makeRenderableContext< + SpringContext, + PropsWithChildren +>( + Context => + ({ children, ...props }) => { + const inherited = useContext(Context) - // Inherited values are dominant when truthy. - const pause = props.pause || !!inherited.pause, - immediate = props.immediate || !!inherited.immediate + // Inherited values are dominant when truthy. + const pause = props.pause || !!inherited.pause + const immediate = props.immediate || !!inherited.immediate - // Memoize the context to avoid unwanted renders. - props = useMemoOne(() => ({ pause, immediate }), [pause, immediate]) + // Memoize the context to avoid unwanted renders. + props = useMemoOne(() => ({ pause, immediate }), [pause, immediate]) - const { Provider } = ctx - return {children} + return {children} + }, + {} as SpringContext +) + +interface RenderableContext extends React.ProviderExoticComponent

{ + Provider: RenderableContext + Consumer: React.Consumer + displayName?: string } -const ctx = makeContext(SpringContext, {} as SpringContext) +/** Make the `target` compatible with `useContext` */ +function makeRenderableContext( + target: (context: React.Context) => React.FunctionComponent

, + init: T +): RenderableContext { + let context = React.createContext(init) + context = Object.assign(target(context), context) + + // https://github.com/facebook/react/pull/28226 + if ('_context' in context.Provider) { + context.Provider._context = context + } else { + // @ts-ignore React 18 types disallow this + context.Provider = context + } -// Allow `useContext(SpringContext)` in TypeScript. -SpringContext.Provider = ctx.Provider -SpringContext.Consumer = ctx.Consumer + if ('_context' in context.Consumer) { + context.Consumer._context = context + } else { + // @ts-expect-error + context.Consumer = context + } -/** Make the `target` compatible with `useContext` */ -function makeContext(target: any, init: T): React.Context { - Object.assign(target, React.createContext(init)) - target.Provider._context = target - target.Consumer._context = target - return target + return context as unknown as RenderableContext } diff --git a/packages/core/src/hooks/useInView.ts b/packages/core/src/hooks/useInView.ts index 7d26d2c3d..90dc6c906 100644 --- a/packages/core/src/hooks/useInView.ts +++ b/packages/core/src/hooks/useInView.ts @@ -35,7 +35,7 @@ export function useInView( args?: IntersectionArgs ) { const [isInView, setIsInView] = useState(false) - const ref = useRef() + const ref = useRef(null) const propsFn = is.fun(props) && props diff --git a/packages/parallax/src/index.tsx b/packages/parallax/src/index.tsx index 74e05932a..f5bf39701 100644 --- a/packages/parallax/src/index.tsx +++ b/packages/parallax/src/index.tsx @@ -73,135 +73,139 @@ export interface ParallaxLayerProps extends ViewProps { speed?: number /** Layer will be sticky between these two offsets, all other props are ignored */ sticky?: StickyConfig + ref?: React.RefObject } export const ParallaxLayer = React.memo( - React.forwardRef( - ( - { horizontal, factor = 1, offset = 0, speed = 0, sticky, ...rest }, - ref - ) => { - // Our parent controls our height and position. - const parent = useContext(ParentContext) - - // This is how we animate. - const ctrl = useMemoOne(() => { - let translate - if (sticky) { - const start = sticky.start || 0 - translate = start * parent.space - } else { - const targetScroll = Math.floor(offset) * parent.space - const distance = parent.space * offset + targetScroll * speed - translate = -(parent.current * speed) + distance - } - type Animated = { space: number; translate: number } - return new Controller({ - space: sticky ? parent.space : parent.space * factor, - translate, - }) - }, []) - - // Create the layer. - const layer = useMemoOne( - () => ({ - horizontal: - horizontal === undefined || sticky ? parent.horizontal : horizontal, - sticky: undefined, - isSticky: false, - setPosition(height, scrollTop, immediate = false) { - if (sticky) { - setSticky(height, scrollTop) - } else { - const targetScroll = Math.floor(offset) * height - const distance = height * offset + targetScroll * speed - ctrl.start({ - translate: -(scrollTop * speed) + distance, - config: parent.config, - immediate, - }) - } - }, - setHeight(height, immediate = false) { + ({ + horizontal, + factor = 1, + offset = 0, + speed = 0, + sticky, + ref, + ...rest + }: ParallaxLayerProps) => { + // Our parent controls our height and position. + const parent = useContext(ParentContext) + + // This is how we animate. + const ctrl = useMemoOne(() => { + let translate + if (sticky) { + const start = sticky.start || 0 + translate = start * parent.space + } else { + const targetScroll = Math.floor(offset) * parent.space + const distance = parent.space * offset + targetScroll * speed + translate = -(parent.current * speed) + distance + } + type Animated = { space: number; translate: number } + return new Controller({ + space: sticky ? parent.space : parent.space * factor, + translate, + }) + }, []) + + // Create the layer. + const layer = useMemoOne( + () => ({ + horizontal: + horizontal === undefined || sticky ? parent.horizontal : horizontal, + sticky: undefined, + isSticky: false, + setPosition(height, scrollTop, immediate = false) { + if (sticky) { + setSticky(height, scrollTop) + } else { + const targetScroll = Math.floor(offset) * height + const distance = height * offset + targetScroll * speed ctrl.start({ - space: sticky ? height : height * factor, + translate: -(scrollTop * speed) + distance, config: parent.config, immediate, }) - }, - }), - [] - ) - - useOnce(() => { - if (sticky) { - const start = sticky.start || 0 - const end = sticky.end || start + 1 - layer.sticky = { start, end } - } - }) + } + }, + setHeight(height, immediate = false) { + ctrl.start({ + space: sticky ? height : height * factor, + config: parent.config, + immediate, + }) + }, + }), + [] + ) + + useOnce(() => { + if (sticky) { + const start = sticky.start || 0 + const end = sticky.end || start + 1 + layer.sticky = { start, end } + } + }) - React.useImperativeHandle(ref, () => layer) + React.useImperativeHandle(ref, () => layer) - const layerRef = useRef() + const layerRef = useRef(null) - const setSticky = (height: number, scrollTop: number) => { - const start = layer.sticky!.start! * height - const end = layer.sticky!.end! * height - const isSticky = scrollTop >= start && scrollTop <= end + const setSticky = (height: number, scrollTop: number) => { + const start = layer.sticky!.start! * height + const end = layer.sticky!.end! * height + const isSticky = scrollTop >= start && scrollTop <= end - if (isSticky === layer.isSticky) return - layer.isSticky = isSticky + if (isSticky === layer.isSticky) return + layer.isSticky = isSticky - const ref = layerRef.current - ref.style.position = isSticky ? 'sticky' : 'absolute' - ctrl.set({ - translate: isSticky ? 0 : scrollTop < start ? start : end, - }) - } + const ref = layerRef.current + ref.style.position = isSticky ? 'sticky' : 'absolute' + ctrl.set({ + translate: isSticky ? 0 : scrollTop < start ? start : end, + }) + } - // Register the layer with our parent. - useOnce(() => { - if (parent) { - parent.layers.add(layer) + // Register the layer with our parent. + useOnce(() => { + if (parent) { + parent.layers.add(layer) + parent.update() + return () => { + parent.layers.delete(layer) parent.update() - return () => { - parent.layers.delete(layer) - parent.update() - } } - }) + } + }) - const translate3d = ctrl.springs.translate.to( - layer.horizontal - ? x => `translate3d(${x}px,0,0)` - : y => `translate3d(0,${y}px,0)` - ) - - return ( - - ) - } - ) + const translate3d = ctrl.springs.translate.to( + layer.horizontal + ? x => `translate3d(${x}px,0,0)` + : y => `translate3d(0,${y}px,0)` + ) + + return ( + + ) + } ) type ConfigProp = SpringConfig | ((key: string) => SpringConfig) @@ -214,179 +218,179 @@ export interface ParallaxProps extends ViewProps { horizontal?: boolean innerStyle?: CSSProperties children: React.ReactNode + ref?: React.RefObject } -export const Parallax = React.memo( - React.forwardRef((props, ref) => { - const [ready, setReady] = useState(false) - const { - pages, - innerStyle: _innerStyle, - config = configs.slow, - enabled = true, - horizontal = false, - children, - ...rest - } = props - - const containerRef = useRef() - const contentRef = useRef() - - const state: IParallax = useMemoOne( - () => ({ - config, - horizontal, - busy: false, - space: 0, - current: 0, - offset: 0, - controller: new Controller({ scroll: 0 }), - layers: new Set(), - container: containerRef, - content: contentRef, - update: () => update(), - scrollTo: offset => scrollTo(offset), - stop: () => state.controller.stop(), - }), - [] - ) - - useEffect(() => { - state.config = config - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config]) +export const Parallax = React.memo((props: ParallaxProps) => { + const [ready, setReady] = useState(false) + const { + pages, + innerStyle: _innerStyle, + config = configs.slow, + enabled = true, + horizontal = false, + children, + ref, + ...rest + } = props + + const containerRef = useRef(null) + const contentRef = useRef(null) + + const state: IParallax = useMemoOne( + () => ({ + config, + horizontal, + busy: false, + space: 0, + current: 0, + offset: 0, + controller: new Controller({ scroll: 0 }), + layers: new Set(), + container: containerRef, + content: contentRef, + update: () => update(), + scrollTo: offset => scrollTo(offset), + stop: () => state.controller.stop(), + }), + [] + ) - React.useImperativeHandle(ref, () => state) + useEffect(() => { + state.config = config + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]) - const update = () => { - const container = containerRef.current - if (!container) return + React.useImperativeHandle(ref, () => state) - const spaceProp = horizontal ? 'clientWidth' : 'clientHeight' - state.space = container[spaceProp] + const update = () => { + const container = containerRef.current + if (!container) return - const scrollType = getScrollType(horizontal) - if (enabled) { - state.current = container[scrollType] - } else { - container[scrollType] = state.current = state.offset * state.space - } + const spaceProp = horizontal ? 'clientWidth' : 'clientHeight' + state.space = container[spaceProp] - const content = contentRef.current - if (content) { - const sizeProp = horizontal ? 'width' : 'height' - content.style[sizeProp] = `${state.space * pages}px` - } + const scrollType = getScrollType(horizontal) + if (enabled) { + state.current = container[scrollType] + } else { + container[scrollType] = state.current = state.offset * state.space + } - state.layers.forEach(layer => { - layer.setHeight(state.space, true) - layer.setPosition(state.space, state.current, true) - }) + const content = contentRef.current + if (content) { + const sizeProp = horizontal ? 'width' : 'height' + content.style[sizeProp] = `${state.space * pages}px` } - const scrollTo = (offset: number) => { - const container = containerRef.current - const scrollType = getScrollType(horizontal) + state.layers.forEach(layer => { + layer.setHeight(state.space, true) + layer.setPosition(state.space, state.current, true) + }) + } - state.offset = offset + const scrollTo = (offset: number) => { + const container = containerRef.current + const scrollType = getScrollType(horizontal) - state.controller.set({ scroll: state.current }) - state.controller.stop().start({ - scroll: offset * state.space, - config, - onChange({ value: { scroll } }: any) { - container[scrollType] = scroll - }, + state.offset = offset + + state.controller.set({ scroll: state.current }) + state.controller.stop().start({ + scroll: offset * state.space, + config, + onChange({ value: { scroll } }: any) { + container[scrollType] = scroll + }, + }) + } + + const onScroll = (event: any) => { + if (!state.busy) { + state.busy = true + state.current = event.target[getScrollType(horizontal)] + raf.onStart(() => { + state.layers.forEach(layer => + layer.setPosition(state.space, state.current) + ) + state.busy = false }) } + } - const onScroll = (event: any) => { - if (!state.busy) { - state.busy = true - state.current = event.target[getScrollType(horizontal)] - raf.onStart(() => { - state.layers.forEach(layer => - layer.setPosition(state.space, state.current) - ) - state.busy = false - }) - } + useEffect(() => state.update()) + useOnce(() => { + setReady(true) + + const onResize = () => { + const update = () => state.update() + raf.onFrame(update) + setTimeout(update, 150) // Some browsers don't fire on maximize! } - useEffect(() => state.update()) - useOnce(() => { - setReady(true) + window.addEventListener('resize', onResize, false) + return () => window.removeEventListener('resize', onResize, false) + }) - const onResize = () => { - const update = () => state.update() - raf.onFrame(update) - setTimeout(update, 150) // Some browsers don't fire on maximize! + const overflow: React.CSSProperties = enabled + ? { + overflowY: horizontal ? 'hidden' : 'scroll', + overflowX: horizontal ? 'scroll' : 'hidden', + } + : { + overflowY: 'hidden', + overflowX: 'hidden', } - window.addEventListener('resize', onResize, false) - return () => window.removeEventListener('resize', onResize, false) - }) - - const overflow: React.CSSProperties = enabled - ? { - overflowY: horizontal ? 'hidden' : 'scroll', - overflowX: horizontal ? 'scroll' : 'hidden', - } - : { - overflowY: 'hidden', - overflowX: 'hidden', - } - - return ( - - {ready && ( - <> - - - {mapChildrenRecursive( - children, - (child: any) => !child.props.sticky && child - )} - - + return ( + + {ready && ( + <> + {mapChildrenRecursive( children, - (child: any) => child.props.sticky && child + (child: any) => !child.props.sticky && child )} - - )} - - ) - }) -) + + + {mapChildrenRecursive( + children, + (child: any) => child.props.sticky && child + )} + + + )} + + ) +}) diff --git a/packages/shared/src/hooks/useMemoOne.ts b/packages/shared/src/hooks/useMemoOne.ts index 4d0274384..a06b84d05 100644 --- a/packages/shared/src/hooks/useMemoOne.ts +++ b/packages/shared/src/hooks/useMemoOne.ts @@ -14,7 +14,7 @@ export function useMemoOne(getResult: () => T, inputs?: any[]): T { }) ) - const committed = useRef>() + const committed = useRef>(null) const prevCache = committed.current let cache = prevCache diff --git a/packages/shared/src/hooks/usePrev.ts b/packages/shared/src/hooks/usePrev.ts index 9c97e064c..bf44a12d3 100644 --- a/packages/shared/src/hooks/usePrev.ts +++ b/packages/shared/src/hooks/usePrev.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react' /** Use a value from the previous render */ export function usePrev(value: T): T | undefined { - const prevRef = useRef() + const prevRef = useRef(null) useEffect(() => { prevRef.current = value }) diff --git a/targets/three/src/index.ts b/targets/three/src/index.ts index 7840ae51a..450a68c96 100644 --- a/targets/three/src/index.ts +++ b/targets/three/src/index.ts @@ -19,7 +19,6 @@ addEffect(() => { }) const host = createHost(primitives, { - // @ts-expect-error r3f related applyAnimatedValues: applyProps, }) diff --git a/targets/web/src/animated.test.tsx b/targets/web/src/animated.test.tsx index 6c7cf2c5d..82301972a 100644 --- a/targets/web/src/animated.test.tsx +++ b/targets/web/src/animated.test.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { forwardRef } from 'react' import { render } from '@testing-library/react' import createMockRaf, { MockRaf } from 'mock-raf' import { Globals } from '@react-spring/shared' @@ -27,14 +26,16 @@ describe('animated component', () => { expect(queryByTitle('Foo')).toBeTruthy() }) it('wraps a component', () => { - const Name = forwardRef< - HTMLHeadingElement, - { name: string; other: string; children: React.ReactNode } - >((props, ref) => ( -

+ const Name = (props: { + name: string + other: string + children: React.ReactNode + ref?: React.RefObject + }) => ( +

{props.children}

- )) + ) const AnimatedName = a(Name) const child = spring('Animated Text') const name = spring('name') @@ -60,14 +61,15 @@ describe('animated component', () => { expect(div.style.opacity).toBe('1') }) it('accepts Animated values in custom style prop', () => { - const Name = forwardRef< - HTMLHeadingElement, - { style: { color: string; opacity?: number }; children: React.ReactNode } - >((props, ref) => ( -

+ const Name = (props: { + style: { color: string; opacity?: number } + children: React.ReactNode + ref?: React.RefObject + }) => ( +

{props.children}

- )) + ) const AnimatedName = a(Name) const opacity = spring(0.5) const { queryByText } = render( diff --git a/yarn.lock b/yarn.lock index c0fdc04aa..fa45ca266 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4682,6 +4682,7 @@ __metadata: "@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 @@ -4702,6 +4703,7 @@ __metadata: react: 18.3.1 react-dom: 18.3.1 react-select: 5.9.0 + react-use-measure: ^2.1.1 refractor: 4.8.1 rehype-autolink-headings: 7.1.0 rehype-parse: 9.0.1 @@ -4908,6 +4910,48 @@ __metadata: languageName: node linkType: hard +"@react-three/fiber@npm:^9.1.0": + version: 9.1.0 + resolution: "@react-three/fiber@npm:9.1.0" + dependencies: + "@babel/runtime": ^7.17.8 + "@types/react-reconciler": ^0.28.9 + "@types/webxr": "*" + base64-js: ^1.5.1 + buffer: ^6.0.3 + its-fine: ^2.0.0 + react-reconciler: ^0.31.0 + react-use-measure: ^2.1.7 + scheduler: ^0.25.0 + suspend-react: ^0.1.3 + use-sync-external-store: ^1.4.0 + zustand: ^5.0.3 + peerDependencies: + expo: ">=43.0" + expo-asset: ">=8.4" + expo-file-system: ">=11.0" + expo-gl: ">=11.0" + react: ^19.0.0 + react-dom: ^19.0.0 + react-native: ">=0.78" + three: ">=0.156" + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + checksum: b1ace61e7002a3d73213be94f5dcd2ecbfd486c1e84dd051dfd851b3fcaa7cbe1d55c8b8bc7f7c6fb9f1cd733f608f57343e10fa3bc683e4aced566026791775 + languageName: node + linkType: hard + "@remix-run/dev@npm:2.15.2": version: 2.15.2 resolution: "@remix-run/dev@npm:2.15.2" @@ -6047,12 +6091,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:18.3.5": - version: 18.3.5 - resolution: "@types/react-dom@npm:18.3.5" - peerDependencies: - "@types/react": ^18.0.0 - checksum: 95c757684f71e761515c5a11299e5feec550c72bb52975487f360e6f0d359b26454c26eaf2ce45dd22748205aa9b2c2fe0abe7005ebcbd233a7615283ac39a7d +"@types/react-dom@npm:19.0.0": + version: 19.0.0 + resolution: "@types/react-dom@npm:19.0.0" + dependencies: + "@types/react": "*" + checksum: 86945c4d4c4cd82e993acdd380c3d9d8e8ca297228aa72c0fa6af4620abb145e7b12235c9165d569f1b25b5f72d1dbe4b4e3f2419432248de9838b22e94295a2 languageName: node linkType: hard @@ -6092,6 +6136,15 @@ __metadata: languageName: node linkType: hard +"@types/react-reconciler@npm:^0.28.9": + version: 0.28.9 + resolution: "@types/react-reconciler@npm:0.28.9" + peerDependencies: + "@types/react": "*" + checksum: 06257f693c7b148a4258c0d0a958288116100014e7b3c21ceaea2d55a668c71718f79b4105a9a0f35b480f3729e46960b40026d685719f9386b4ed63108dda09 + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.4.0": version: 4.4.10 resolution: "@types/react-transition-group@npm:4.4.10" @@ -6101,7 +6154,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:18.3.18": +"@types/react@npm:*": version: 18.3.18 resolution: "@types/react@npm:18.3.18" dependencies: @@ -6111,6 +6164,15 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:19.0.0": + version: 19.0.0 + resolution: "@types/react@npm:19.0.0" + dependencies: + csstype: ^3.0.2 + checksum: dd7d7388b28fdf78cdf28c88490fe99413a0e1fab33e92afdf862620edc77dfe605ffe48dd3aeffb1de29107ea958a12f6d667236b2ead1affdf609db7152fad + languageName: node + linkType: hard + "@types/semver@npm:^7.5.0": version: 7.5.6 resolution: "@types/semver@npm:7.5.6" @@ -12519,6 +12581,17 @@ __metadata: languageName: node linkType: hard +"its-fine@npm:^2.0.0": + version: 2.0.0 + resolution: "its-fine@npm:2.0.0" + dependencies: + "@types/react-reconciler": ^0.28.9 + peerDependencies: + react: ^19.0.0 + checksum: 887ff10d8dfe8558683d5f68ad963c72a28c6df027c5039de7ec57978e5071c564ef4b00b14ef41e7706e5839a5584cbd480a79a3880f78d7ff826931e5dc22a + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -16886,6 +16959,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:19.0.0": + version: 19.0.0 + resolution: "react-dom@npm:19.0.0" + dependencies: + scheduler: ^0.25.0 + peerDependencies: + react: ^19.0.0 + checksum: 009cc6e575263a0d1906f9dd4aa6532d2d3d0d71e4c2b7777c8fe4de585fa06b5b77cdc2e0fbaa2f3a4a5e5d3305c189ba152153f358ee7da4d9d9ba5d3a8975 + languageName: node + linkType: hard + "react-dropzone@npm:^12.0.0": version: 12.1.0 resolution: "react-dropzone@npm:12.1.0" @@ -17013,6 +17097,17 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:^0.31.0": + version: 0.31.0 + resolution: "react-reconciler@npm:0.31.0" + dependencies: + scheduler: ^0.25.0 + peerDependencies: + react: ^19.0.0 + checksum: 820c4e4003c5615849bf0cda97d8a55b99af2bb59cc0825882b727f0ad0c4bf4581bb3d25e00beca1164203dbc172f0a8c4725e7aa2fb85e025938722384a84e + languageName: node + linkType: hard + "react-reconciler@npm:~0.29.0": version: 0.29.0 resolution: "react-reconciler@npm:0.29.0" @@ -17125,7 +17220,7 @@ __metadata: "@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 @@ -17138,8 +17233,8 @@ __metadata: "@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 @@ -17154,8 +17249,8 @@ __metadata: mock-raf: "npm:@react-spring/mock-raf@1.1.1" 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 @@ -17238,6 +17333,19 @@ __metadata: languageName: node linkType: hard +"react-use-measure@npm:^2.1.1, react-use-measure@npm:^2.1.7": + version: 2.1.7 + resolution: "react-use-measure@npm:2.1.7" + peerDependencies: + react: ">=16.13" + react-dom: ">=16.13" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 5f00c14cf50b0710cdbd27b63a005be20283099d2fa2723a97f3a1cf0b2daedddd67249520c21e49e95348f56428689f3229c343dcb9ed37da58f9c227d29bee + languageName: node + linkType: hard + "react-zdog@npm:1.2.2": version: 1.2.2 resolution: "react-zdog@npm:1.2.2" @@ -17269,6 +17377,13 @@ __metadata: languageName: node linkType: hard +"react@npm:19.0.0": + version: 19.0.0 + resolution: "react@npm:19.0.0" + checksum: 86de15d85b2465feb40297a90319c325cb07cf27191a361d47bcfe8c6126c973d660125aa67b8f4cbbe39f15a2f32efd0c814e98196d8e5b68c567ba40a399c6 + languageName: node + linkType: hard + "read-yaml-file@npm:^1.1.0": version: 1.1.0 resolution: "read-yaml-file@npm:1.1.0" @@ -18053,6 +18168,13 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.25.0": + version: 0.25.0 + resolution: "scheduler@npm:0.25.0" + checksum: b7bb9fddbf743e521e9aaa5198a03ae823f5e104ebee0cb9ec625392bb7da0baa1c28ab29cee4b1e407a94e76acc6eee91eeb749614f91f853efda2613531566 + languageName: node + linkType: hard + "section-matter@npm:^1.0.0": version: 1.0.0 resolution: "section-matter@npm:1.0.0" @@ -20138,6 +20260,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.4.0": + version: 1.4.0 + resolution: "use-sync-external-store@npm:1.4.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: dc3843a1b59ac8bd01417bd79498d4c688d5df8bf4801be50008ef4bfaacb349058c0b1605b5b43c828e0a2d62722d7e861573b3f31cea77a7f23e8b0fc2f7e3 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -21075,6 +21206,27 @@ __metadata: languageName: node linkType: hard +"zustand@npm:^5.0.3": + version: 5.0.3 + resolution: "zustand@npm:5.0.3" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 72da39ac3017726c3562c615a0f76cee0c9ea678d664f82ee7669f8cb5e153ee81059363473094e4154d73a2935ee3459f6792d1ec9d08d2e72ebe641a16a6ba + languageName: node + linkType: hard + "zwitch@npm:^2.0.0, zwitch@npm:^2.0.4": version: 2.0.4 resolution: "zwitch@npm:2.0.4" From 6c4240d3ca2b0469047582a03c5e8e275df596ab Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 20 Mar 2025 13:41:08 +0000 Subject: [PATCH 02/10] feat: disabled workflows --- .github/workflows/experimental.yml | 88 +++++++++--------- .github/workflows/nightly.yml | 138 ++++++++++++++--------------- 2 files changed, 113 insertions(+), 113 deletions(-) diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index 112ed839c..77e33775b 100644 --- a/.github/workflows/experimental.yml +++ b/.github/workflows/experimental.yml @@ -1,44 +1,44 @@ -name: 'Experimental Releases' - -on: - workflow_dispatch: - -env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true - -jobs: - experimental: - name: 'Experimental Release' - if: github.repository == 'pmndrs/react-spring' - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/checkout@v4 - - - name: Setup npmrc - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'yarn' - - - name: Install - run: yarn install --immutable - - - name: Build - run: yarn build-ci - - - run: ./scripts/version-and-publish.sh - env: - VERSION: '${{ github.sha }}' - DIST_TAG: experimental +# name: 'Experimental Releases' + +# on: +# workflow_dispatch: + +# env: +# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} +# TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +# concurrency: +# group: ${{ github.workflow }}-${{ github.ref_name }} +# cancel-in-progress: true + +# jobs: +# experimental: +# name: 'Experimental Release' +# if: github.repository == 'pmndrs/react-spring' +# runs-on: ubuntu-latest +# steps: +# - name: Cancel Previous Runs +# uses: styfle/cancel-workflow-action@0.12.1 +# with: +# access_token: ${{ secrets.GITHUB_TOKEN }} + +# - uses: actions/checkout@v4 + +# - name: Setup npmrc +# run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + +# - uses: actions/setup-node@v4 +# with: +# node-version: 20 +# cache: 'yarn' + +# - name: Install +# run: yarn install --immutable + +# - name: Build +# run: yarn build-ci + +# - run: ./scripts/version-and-publish.sh +# env: +# VERSION: '${{ github.sha }}' +# DIST_TAG: experimental diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 16a42eb84..f378c121e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,81 +1,81 @@ -name: 'Nightly Releases' +# name: 'Nightly Releases' -on: - schedule: - - cron: '0 0 * * *' +# on: +# schedule: +# - cron: '0 0 * * *' -env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} +# env: +# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} +# TURBO_TEAM: ${{ secrets.TURBO_TEAM }} -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true +# concurrency: +# group: ${{ github.workflow }}-${{ github.ref_name }} +# cancel-in-progress: true -jobs: - nightly: - name: 'Nightly Release' - if: github.repository == 'pmndrs/react-spring' - runs-on: ubuntu-latest - outputs: - NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ secrets.GITHUB_TOKEN }} +# jobs: +# nightly: +# name: 'Nightly Release' +# if: github.repository == 'pmndrs/react-spring' +# runs-on: ubuntu-latest +# outputs: +# NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} +# steps: +# - name: Cancel Previous Runs +# uses: styfle/cancel-workflow-action@0.12.1 +# with: +# access_token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v4 +# - uses: actions/checkout@v4 - - name: Check for changes - id: version - run: | - # get latest commit sha - SHA=$(git rev-parse HEAD) - # get first 7 characters of sha - SHORT_SHA=${SHA::7} - # get latest nightly tag - LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-creatordate | head -n 1) - # check if last commit to main would be the nightly tag we're about to create (minus the date) - # if it is, we'll skip the nightly creation - # if not, we'll create a new nightly tag - if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA} ]]; then - echo "🛑 Latest nightly tag is the same as the latest commit sha, skipping nightly release" - exit 1 - else - # v0.0.0-nightly-- - NEXT_VERSION=nightly-${SHORT_SHA} - # set output so it can be used in other jobs - echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT - exit 0 - fi +# - name: Check for changes +# id: version +# run: | +# # get latest commit sha +# SHA=$(git rev-parse HEAD) +# # get first 7 characters of sha +# SHORT_SHA=${SHA::7} +# # get latest nightly tag +# LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-creatordate | head -n 1) +# # check if last commit to main would be the nightly tag we're about to create (minus the date) +# # if it is, we'll skip the nightly creation +# # if not, we'll create a new nightly tag +# if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA} ]]; then +# echo "🛑 Latest nightly tag is the same as the latest commit sha, skipping nightly release" +# exit 1 +# else +# # v0.0.0-nightly-- +# NEXT_VERSION=nightly-${SHORT_SHA} +# # set output so it can be used in other jobs +# echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT +# exit 0 +# fi - - uses: actions/setup-node@v4 - if: steps.version.outputs.NEXT_VERSION - with: - node-version: 20 - cache: 'yarn' +# - uses: actions/setup-node@v4 +# if: steps.version.outputs.NEXT_VERSION +# with: +# node-version: 20 +# cache: 'yarn' - - name: Install - if: steps.version.outputs.NEXT_VERSION - run: yarn install --immutable +# - name: Install +# if: steps.version.outputs.NEXT_VERSION +# run: yarn install --immutable - - name: Build - if: steps.version.outputs.NEXT_VERSION - run: yarn build-ci +# - name: Build +# if: steps.version.outputs.NEXT_VERSION +# run: yarn build-ci - - name: Setup npmrc - if: steps.version.outputs.NEXT_VERSION - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc +# - name: Setup npmrc +# if: steps.version.outputs.NEXT_VERSION +# run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - - run: ./scripts/version-and-publish.sh - if: steps.version.outputs.NEXT_VERSION - env: - VERSION: ${{ steps.version.outputs.NEXT_VERSION }} - DIST_TAG: nightly +# - run: ./scripts/version-and-publish.sh +# if: steps.version.outputs.NEXT_VERSION +# env: +# VERSION: ${{ steps.version.outputs.NEXT_VERSION }} +# DIST_TAG: nightly - - name: Tag - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: '0.0.0-${{ steps.version.outputs.NEXT_VERSION }}' +# - name: Tag +# uses: mathieudutour/github-tag-action@v6.2 +# with: +# github_token: ${{ secrets.GITHUB_TOKEN }} +# custom_tag: '0.0.0-${{ steps.version.outputs.NEXT_VERSION }}' From 065de58134a8edf330a2af674ea3d1da820e0466 Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 20 Mar 2025 14:19:20 +0000 Subject: [PATCH 03/10] feat: Refactored SpringContext --- packages/core/src/SpringContext.test.tsx | 10 +-- packages/core/src/SpringContext.tsx | 76 +++++++--------------- packages/core/src/hooks/useSpring.test.tsx | 6 +- packages/core/src/hooks/useSprings.ts | 11 ++-- packages/core/test/setup.ts | 7 +- 5 files changed, 43 insertions(+), 67 deletions(-) diff --git a/packages/core/src/SpringContext.test.tsx b/packages/core/src/SpringContext.test.tsx index 5b1f3f47a..7f9de3add 100644 --- a/packages/core/src/SpringContext.test.tsx +++ b/packages/core/src/SpringContext.test.tsx @@ -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' @@ -13,9 +13,9 @@ describe('SpringContext', () => { } const update = createUpdater(props => ( - + - + )) it('only merges when changed', () => { @@ -27,9 +27,9 @@ describe('SpringContext', () => { } const getRoot = () => ( - + - + ) const expectUpdates = (updates: any[]) => { diff --git a/packages/core/src/SpringContext.tsx b/packages/core/src/SpringContext.tsx index 0667c310a..c01fdf23d 100644 --- a/packages/core/src/SpringContext.tsx +++ b/packages/core/src/SpringContext.tsx @@ -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 @@ -13,54 +12,29 @@ export interface SpringContext { immediate?: boolean } -export const SpringContext = makeRenderableContext< - SpringContext, - PropsWithChildren ->( - Context => - ({ children, ...props }) => { - const inherited = useContext(Context) - - // Inherited values are dominant when truthy. - const pause = props.pause || !!inherited.pause - const immediate = props.immediate || !!inherited.immediate - - // Memoize the context to avoid unwanted renders. - props = useMemoOne(() => ({ pause, immediate }), [pause, immediate]) - - return {children} - }, - {} as SpringContext -) - -interface RenderableContext extends React.ProviderExoticComponent

{ - Provider: RenderableContext - Consumer: React.Consumer - displayName?: string -} - -/** Make the `target` compatible with `useContext` */ -function makeRenderableContext( - target: (context: React.Context) => React.FunctionComponent

, - init: T -): RenderableContext { - let context = React.createContext(init) - context = Object.assign(target(context), context) - - // https://github.com/facebook/react/pull/28226 - if ('_context' in context.Provider) { - context.Provider._context = context - } else { - // @ts-ignore React 18 types disallow this - context.Provider = context - } - - if ('_context' in context.Consumer) { - context.Consumer._context = context - } else { - // @ts-expect-error - context.Consumer = context - } - - return context as unknown as RenderableContext +export const SpringContext = React.createContext({ + pause: false, + immediate: false, +}) + +export const SpringContextProvider = ({ + children, + ...props +}: PropsWithChildren) => { + const inherited = useContext(SpringContext) + + // Inherited values are dominant when truthy. + const pause = props.pause ?? inherited.pause ?? false + const immediate = props.immediate ?? inherited.immediate ?? false + + // Memoize the context to avoid unwanted renders. + const contextValue = React.useMemo( + () => ({ pause, immediate }), + [pause, immediate] + ) + return ( + + {children} + + ) } diff --git a/packages/core/src/hooks/useSpring.test.tsx b/packages/core/src/hooks/useSpring.test.tsx index c5bd8892c..dbbd19e11 100644 --- a/packages/core/src/hooks/useSpring.test.tsx +++ b/packages/core/src/hooks/useSpring.test.tsx @@ -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' @@ -140,7 +140,9 @@ function createUpdater(Component: React.ComponentType<{ args: [any, any?] }>) { }) function renderWithContext(elem: JSX.Element) { - const wrapped = {elem} + const wrapped = ( + {elem} + ) if (result) result.rerender(wrapped) else result = render(wrapped) return result diff --git a/packages/core/src/hooks/useSprings.ts b/packages/core/src/hooks/useSprings.ts index a1729fda9..258232d80 100644 --- a/packages/core/src/hooks/useSprings.ts +++ b/packages/core/src/hooks/useSprings.ts @@ -128,7 +128,8 @@ export function useSprings( ) const ctrls = useRef([...state.ctrls]) - const updates: any[] = [] + const updates = useRef(null!) + updates.current ??= [] // Cache old controllers to dispose in the commit phase. const prevLength = usePrev(length) || 0 @@ -164,7 +165,7 @@ export function useSprings( : (props as any)[i] if (update) { - updates[i] = declareUpdate(update) + updates.current[i] = declareUpdate(update) } } } @@ -172,7 +173,7 @@ export function useSprings( // 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) @@ -202,7 +203,7 @@ export function useSprings( } // 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) @@ -214,6 +215,8 @@ export function useSprings( } else { ctrl.start(update) } + + updates.current[i] = null } }) }) diff --git a/packages/core/test/setup.ts b/packages/core/test/setup.ts index 3b9af122f..9a5ca59e8 100644 --- a/packages/core/test/setup.ts +++ b/packages/core/test/setup.ts @@ -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, }) }) @@ -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. @@ -147,7 +144,7 @@ global.advanceUntil = async test => { } // Ensure pending effects are flushed. - await flushMicroTasks() + await act(() => flushMicroTasks()) // Prevent infinite recursion. if (++steps > 1e3) { From 1d22c9e2147a893052516579bb54bde0410d1805 Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 20 Mar 2025 14:22:49 +0000 Subject: [PATCH 04/10] feat: Reverted testing change --- packages/core/test/setup.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/test/setup.ts b/packages/core/test/setup.ts index 9a5ca59e8..3b9af122f 100644 --- a/packages/core/test/setup.ts +++ b/packages/core/test/setup.ts @@ -60,6 +60,9 @@ 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, }) }) @@ -135,7 +138,7 @@ global.advanceUntil = async test => { willAdvance: observe, }) - await act(() => jest.advanceTimersByTimeAsync(1000 / 60)) + jest.advanceTimersByTime(1000 / 60) global.mockRaf.step() // Stop observing after the frame is processed. @@ -144,7 +147,7 @@ global.advanceUntil = async test => { } // Ensure pending effects are flushed. - await act(() => flushMicroTasks()) + await flushMicroTasks() // Prevent infinite recursion. if (++steps > 1e3) { From 0c88ea4ba0511bd636e74ce6d36afc13c68a40e0 Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 20 Mar 2025 14:25:32 +0000 Subject: [PATCH 05/10] formatting: useSprings --- packages/core/src/hooks/useSprings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/hooks/useSprings.ts b/packages/core/src/hooks/useSprings.ts index 258232d80..ddb8d2f3e 100644 --- a/packages/core/src/hooks/useSprings.ts +++ b/packages/core/src/hooks/useSprings.ts @@ -173,7 +173,9 @@ export function useSprings( // 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.current[i])) + const springs = ctrls.current.map((ctrl, i) => + getSprings(ctrl, updates.current[i]) + ) const context = useContext(SpringContext) const prevContext = usePrev(context) From c78dad73cbff61113c9c617fafd19dce1a7688b6 Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 20 Mar 2025 15:10:33 +0000 Subject: [PATCH 06/10] ci: enabled workflows again --- .github/workflows/experimental.yml | 88 +++++++++--------- .github/workflows/nightly.yml | 138 ++++++++++++++--------------- 2 files changed, 113 insertions(+), 113 deletions(-) diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index 77e33775b..112ed839c 100644 --- a/.github/workflows/experimental.yml +++ b/.github/workflows/experimental.yml @@ -1,44 +1,44 @@ -# name: 'Experimental Releases' - -# on: -# workflow_dispatch: - -# env: -# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} -# TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref_name }} -# cancel-in-progress: true - -# jobs: -# experimental: -# name: 'Experimental Release' -# if: github.repository == 'pmndrs/react-spring' -# runs-on: ubuntu-latest -# steps: -# - name: Cancel Previous Runs -# uses: styfle/cancel-workflow-action@0.12.1 -# with: -# access_token: ${{ secrets.GITHUB_TOKEN }} - -# - uses: actions/checkout@v4 - -# - name: Setup npmrc -# run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - -# - uses: actions/setup-node@v4 -# with: -# node-version: 20 -# cache: 'yarn' - -# - name: Install -# run: yarn install --immutable - -# - name: Build -# run: yarn build-ci - -# - run: ./scripts/version-and-publish.sh -# env: -# VERSION: '${{ github.sha }}' -# DIST_TAG: experimental +name: 'Experimental Releases' + +on: + workflow_dispatch: + +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + experimental: + name: 'Experimental Release' + if: github.repository == 'pmndrs/react-spring' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.12.1 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v4 + + - name: Setup npmrc + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + + - name: Install + run: yarn install --immutable + + - name: Build + run: yarn build-ci + + - run: ./scripts/version-and-publish.sh + env: + VERSION: '${{ github.sha }}' + DIST_TAG: experimental diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f378c121e..16a42eb84 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,81 +1,81 @@ -# name: 'Nightly Releases' +name: 'Nightly Releases' -# on: -# schedule: -# - cron: '0 0 * * *' +on: + schedule: + - cron: '0 0 * * *' -# env: -# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} -# TURBO_TEAM: ${{ secrets.TURBO_TEAM }} +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref_name }} -# cancel-in-progress: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true -# jobs: -# nightly: -# name: 'Nightly Release' -# if: github.repository == 'pmndrs/react-spring' -# runs-on: ubuntu-latest -# outputs: -# NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} -# steps: -# - name: Cancel Previous Runs -# uses: styfle/cancel-workflow-action@0.12.1 -# with: -# access_token: ${{ secrets.GITHUB_TOKEN }} +jobs: + nightly: + name: 'Nightly Release' + if: github.repository == 'pmndrs/react-spring' + runs-on: ubuntu-latest + outputs: + NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.12.1 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} -# - uses: actions/checkout@v4 + - uses: actions/checkout@v4 -# - name: Check for changes -# id: version -# run: | -# # get latest commit sha -# SHA=$(git rev-parse HEAD) -# # get first 7 characters of sha -# SHORT_SHA=${SHA::7} -# # get latest nightly tag -# LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-creatordate | head -n 1) -# # check if last commit to main would be the nightly tag we're about to create (minus the date) -# # if it is, we'll skip the nightly creation -# # if not, we'll create a new nightly tag -# if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA} ]]; then -# echo "🛑 Latest nightly tag is the same as the latest commit sha, skipping nightly release" -# exit 1 -# else -# # v0.0.0-nightly-- -# NEXT_VERSION=nightly-${SHORT_SHA} -# # set output so it can be used in other jobs -# echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT -# exit 0 -# fi + - name: Check for changes + id: version + run: | + # get latest commit sha + SHA=$(git rev-parse HEAD) + # get first 7 characters of sha + SHORT_SHA=${SHA::7} + # get latest nightly tag + LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-creatordate | head -n 1) + # check if last commit to main would be the nightly tag we're about to create (minus the date) + # if it is, we'll skip the nightly creation + # if not, we'll create a new nightly tag + if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA} ]]; then + echo "🛑 Latest nightly tag is the same as the latest commit sha, skipping nightly release" + exit 1 + else + # v0.0.0-nightly-- + NEXT_VERSION=nightly-${SHORT_SHA} + # set output so it can be used in other jobs + echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT + exit 0 + fi -# - uses: actions/setup-node@v4 -# if: steps.version.outputs.NEXT_VERSION -# with: -# node-version: 20 -# cache: 'yarn' + - uses: actions/setup-node@v4 + if: steps.version.outputs.NEXT_VERSION + with: + node-version: 20 + cache: 'yarn' -# - name: Install -# if: steps.version.outputs.NEXT_VERSION -# run: yarn install --immutable + - name: Install + if: steps.version.outputs.NEXT_VERSION + run: yarn install --immutable -# - name: Build -# if: steps.version.outputs.NEXT_VERSION -# run: yarn build-ci + - name: Build + if: steps.version.outputs.NEXT_VERSION + run: yarn build-ci -# - name: Setup npmrc -# if: steps.version.outputs.NEXT_VERSION -# run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + - name: Setup npmrc + if: steps.version.outputs.NEXT_VERSION + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc -# - run: ./scripts/version-and-publish.sh -# if: steps.version.outputs.NEXT_VERSION -# env: -# VERSION: ${{ steps.version.outputs.NEXT_VERSION }} -# DIST_TAG: nightly + - run: ./scripts/version-and-publish.sh + if: steps.version.outputs.NEXT_VERSION + env: + VERSION: ${{ steps.version.outputs.NEXT_VERSION }} + DIST_TAG: nightly -# - name: Tag -# uses: mathieudutour/github-tag-action@v6.2 -# with: -# github_token: ${{ secrets.GITHUB_TOKEN }} -# custom_tag: '0.0.0-${{ steps.version.outputs.NEXT_VERSION }}' + - name: Tag + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: '0.0.0-${{ steps.version.outputs.NEXT_VERSION }}' From 33aca4665b81355dfbd78f8b092ebd37ff8491f3 Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 3 Apr 2025 11:23:42 +0100 Subject: [PATCH 07/10] revert: reverted forwardRef removal & updated peer deps --- .../app/components/Header/HeaderSidePanel.tsx | 143 +++-- docs/app/components/Text/Copy.tsx | 30 +- docs/app/components/Text/Heading.tsx | 61 +- docs/app/components/Text/List.tsx | 30 +- packages/animated/package.json | 2 +- packages/core/package.json | 2 +- packages/parallax/package.json | 4 +- packages/parallax/src/index.tsx | 532 +++++++++--------- packages/react-spring/package.json | 4 +- packages/shared/package.json | 2 +- targets/konva/package.json | 2 +- targets/three/package.json | 2 +- targets/web/package.json | 4 +- targets/web/src/animated.test.tsx | 34 +- targets/zdog/package.json | 4 +- 15 files changed, 427 insertions(+), 429 deletions(-) diff --git a/docs/app/components/Header/HeaderSidePanel.tsx b/docs/app/components/Header/HeaderSidePanel.tsx index 6b50d9932..a60f42345 100644 --- a/docs/app/components/Header/HeaderSidePanel.tsx +++ b/docs/app/components/Header/HeaderSidePanel.tsx @@ -20,87 +20,86 @@ import { subNavContainer, } from './HeaderSidePanel.css' import { visuallyHidden } from '../../styles/utilities.css' +import { forwardRef } from 'react' interface HeaderSidePanelProps { isOpen: boolean submenu?: NavigationSchema onNavigationClick?: () => void - ref?: React.RefObject } -export const HeaderSidePanel = ({ - isOpen, - submenu, - onNavigationClick, - ref, -}: HeaderSidePanelProps) => { - const location = useLocation() +export const HeaderSidePanel = forwardRef( + ({ isOpen, submenu, onNavigationClick }, ref) => { + const location = useLocation() - const isDocs = location.pathname.includes('/docs') + const isDocs = location.pathname.includes('/docs') - const transitions = useTransition(isOpen, { - from: { - x: '100%', - opacity: 0, - }, - enter: { - x: '0', - opacity: 1, - }, - leave: { - x: '100%', - opacity: 0, - }, - config: { - tension: 210, - friction: 30, - mass: 1, - }, - }) + const transitions = useTransition(isOpen, { + from: { + x: '100%', + opacity: 0, + }, + enter: { + x: '0', + opacity: 1, + }, + leave: { + x: '100%', + opacity: 0, + }, + config: { + tension: 210, + friction: 30, + mass: 1, + }, + }) - const handleNavClick = () => { - if (onNavigationClick) { - onNavigationClick() + const handleNavClick = () => { + if (onNavigationClick) { + onNavigationClick() + } } - } - return transitions(({ opacity, x }, item) => - item ? ( - <> - - - - {/* @ts-ignore */} - - -

-
- - - - - - -
- Main Menu - -
- - - - - - - - ) : null - ) -} + return transitions(({ opacity, x }, item) => + item ? ( + <> + + + + {/* @ts-ignore */} + + +
+
+ + + + + + +
+ + Main Menu + + +
+ + + + +
+
+ + ) : null + ) + } +) diff --git a/docs/app/components/Text/Copy.tsx b/docs/app/components/Text/Copy.tsx index 01d09f6f0..24dbebf83 100644 --- a/docs/app/components/Text/Copy.tsx +++ b/docs/app/components/Text/Copy.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { ReactNode } from 'react' +import { forwardRef, ReactNode } from 'react' import { copy } from './Copy.css' import * as FontSizes from '../../styles/fontStyles.css' @@ -9,21 +9,19 @@ export interface CopyProps { className?: string children?: ReactNode tag?: keyof Pick - ref?: React.RefObject } -export const Copy = ({ - fontStyle = 'XS', - className, - children, - tag = 'p', - ref, -}: CopyProps) => { - const Element = tag +export const Copy = forwardRef( + ({ fontStyle = 'XS', className, children, tag = 'p' }, ref) => { + const Element = tag - return ( - - {children} - - ) -} + return ( + + {children} + + ) + } +) diff --git a/docs/app/components/Text/Heading.tsx b/docs/app/components/Text/Heading.tsx index 437aad68d..743642975 100644 --- a/docs/app/components/Text/Heading.tsx +++ b/docs/app/components/Text/Heading.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, ReactNode } from 'react' +import { CSSProperties, forwardRef, ReactNode } from 'react' import { Link } from 'phosphor-react' @@ -17,34 +17,37 @@ export interface HeadingProps { isLink?: boolean weight?: keyof FontSizes.FontWeights style?: CSSProperties - ref?: React.RefObject } -export const Heading = ({ - tag = 'h1', - fontStyle = 'S', - weight = 'default', - className, - children, - isLink = false, - ref, - ...restProps -}: HeadingProps) => { - const Element = tag +export const Heading = forwardRef( + ( + { + tag = 'h1', + fontStyle = 'S', + weight = 'default', + className, + children, + isLink = false, + ...restProps + }, + ref + ) => { + const Element = tag - return ( - - {children} - {isLink ? : null} - - ) -} + return ( + + {children} + {isLink ? : null} + + ) + } +) diff --git a/docs/app/components/Text/List.tsx b/docs/app/components/Text/List.tsx index 7fd38be89..33c8a4284 100644 --- a/docs/app/components/Text/List.tsx +++ b/docs/app/components/Text/List.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { ReactNode } from 'react' +import { forwardRef, ReactNode } from 'react' import * as FontSizes from '../../styles/fontStyles.css' import { descriptiveList, list } from './List.css' @@ -8,24 +8,22 @@ export interface ListProps { fontStyle?: keyof FontSizes.FontSizes className?: string children?: ReactNode - ref?: React.RefObject } -export const List = ({ - tag = 'ul', - fontStyle = 'XS', - className, - children, - ref, -}: ListProps) => { - const Element = tag +export const List = forwardRef( + ({ tag = 'ul', fontStyle = 'XS', className, children }, ref) => { + const Element = tag - return ( - - {children} - - ) -} + return ( + + {children} + + ) + } +) interface DescriptiveListProps { data: [title: string, item: ReactNode][] diff --git a/packages/animated/package.json b/packages/animated/package.json index 4e925457a..d433c0c7c 100644 --- a/packages/animated/package.json +++ b/packages/animated/package.json @@ -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" } } diff --git a/packages/core/package.json b/packages/core/package.json index fb1c2f430..a14a9de34 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" } } diff --git a/packages/parallax/package.json b/packages/parallax/package.json index 3e7ef6484..5b3437f53 100644 --- a/packages/parallax/package.json +++ b/packages/parallax/package.json @@ -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" } } diff --git a/packages/parallax/src/index.tsx b/packages/parallax/src/index.tsx index f5bf39701..9594c852b 100644 --- a/packages/parallax/src/index.tsx +++ b/packages/parallax/src/index.tsx @@ -73,139 +73,135 @@ export interface ParallaxLayerProps extends ViewProps { speed?: number /** Layer will be sticky between these two offsets, all other props are ignored */ sticky?: StickyConfig - ref?: React.RefObject } export const ParallaxLayer = React.memo( - ({ - horizontal, - factor = 1, - offset = 0, - speed = 0, - sticky, - ref, - ...rest - }: ParallaxLayerProps) => { - // Our parent controls our height and position. - const parent = useContext(ParentContext) - - // This is how we animate. - const ctrl = useMemoOne(() => { - let translate - if (sticky) { - const start = sticky.start || 0 - translate = start * parent.space - } else { - const targetScroll = Math.floor(offset) * parent.space - const distance = parent.space * offset + targetScroll * speed - translate = -(parent.current * speed) + distance - } - type Animated = { space: number; translate: number } - return new Controller({ - space: sticky ? parent.space : parent.space * factor, - translate, - }) - }, []) - - // Create the layer. - const layer = useMemoOne( - () => ({ - horizontal: - horizontal === undefined || sticky ? parent.horizontal : horizontal, - sticky: undefined, - isSticky: false, - setPosition(height, scrollTop, immediate = false) { - if (sticky) { - setSticky(height, scrollTop) - } else { - const targetScroll = Math.floor(offset) * height - const distance = height * offset + targetScroll * speed + React.forwardRef( + ( + { horizontal, factor = 1, offset = 0, speed = 0, sticky, ...rest }, + ref + ) => { + // Our parent controls our height and position. + const parent = useContext(ParentContext) + + // This is how we animate. + const ctrl = useMemoOne(() => { + let translate + if (sticky) { + const start = sticky.start || 0 + translate = start * parent.space + } else { + const targetScroll = Math.floor(offset) * parent.space + const distance = parent.space * offset + targetScroll * speed + translate = -(parent.current * speed) + distance + } + type Animated = { space: number; translate: number } + return new Controller({ + space: sticky ? parent.space : parent.space * factor, + translate, + }) + }, []) + + // Create the layer. + const layer = useMemoOne( + () => ({ + horizontal: + horizontal === undefined || sticky ? parent.horizontal : horizontal, + sticky: undefined, + isSticky: false, + setPosition(height, scrollTop, immediate = false) { + if (sticky) { + setSticky(height, scrollTop) + } else { + const targetScroll = Math.floor(offset) * height + const distance = height * offset + targetScroll * speed + ctrl.start({ + translate: -(scrollTop * speed) + distance, + config: parent.config, + immediate, + }) + } + }, + setHeight(height, immediate = false) { ctrl.start({ - translate: -(scrollTop * speed) + distance, + space: sticky ? height : height * factor, config: parent.config, immediate, }) - } - }, - setHeight(height, immediate = false) { - ctrl.start({ - space: sticky ? height : height * factor, - config: parent.config, - immediate, - }) - }, - }), - [] - ) - - useOnce(() => { - if (sticky) { - const start = sticky.start || 0 - const end = sticky.end || start + 1 - layer.sticky = { start, end } - } - }) + }, + }), + [] + ) + + useOnce(() => { + if (sticky) { + const start = sticky.start || 0 + const end = sticky.end || start + 1 + layer.sticky = { start, end } + } + }) - React.useImperativeHandle(ref, () => layer) + React.useImperativeHandle(ref, () => layer) - const layerRef = useRef(null) + const layerRef = useRef(null) - const setSticky = (height: number, scrollTop: number) => { - const start = layer.sticky!.start! * height - const end = layer.sticky!.end! * height - const isSticky = scrollTop >= start && scrollTop <= end + const setSticky = (height: number, scrollTop: number) => { + const start = layer.sticky!.start! * height + const end = layer.sticky!.end! * height + const isSticky = scrollTop >= start && scrollTop <= end - if (isSticky === layer.isSticky) return - layer.isSticky = isSticky + if (isSticky === layer.isSticky) return + layer.isSticky = isSticky - const ref = layerRef.current - ref.style.position = isSticky ? 'sticky' : 'absolute' - ctrl.set({ - translate: isSticky ? 0 : scrollTop < start ? start : end, - }) - } + const ref = layerRef.current + ref.style.position = isSticky ? 'sticky' : 'absolute' + ctrl.set({ + translate: isSticky ? 0 : scrollTop < start ? start : end, + }) + } - // Register the layer with our parent. - useOnce(() => { - if (parent) { - parent.layers.add(layer) - parent.update() - return () => { - parent.layers.delete(layer) + // Register the layer with our parent. + useOnce(() => { + if (parent) { + parent.layers.add(layer) parent.update() + return () => { + parent.layers.delete(layer) + parent.update() + } } - } - }) - - const translate3d = ctrl.springs.translate.to( - layer.horizontal - ? x => `translate3d(${x}px,0,0)` - : y => `translate3d(0,${y}px,0)` - ) + }) - return ( - - ) - } + const translate3d = ctrl.springs.translate.to( + layer.horizontal + ? x => `translate3d(${x}px,0,0)` + : y => `translate3d(0,${y}px,0)` + ) + + return ( + + ) + } + ) ) type ConfigProp = SpringConfig | ((key: string) => SpringConfig) @@ -218,179 +214,179 @@ export interface ParallaxProps extends ViewProps { horizontal?: boolean innerStyle?: CSSProperties children: React.ReactNode - ref?: React.RefObject } -export const Parallax = React.memo((props: ParallaxProps) => { - const [ready, setReady] = useState(false) - const { - pages, - innerStyle: _innerStyle, - config = configs.slow, - enabled = true, - horizontal = false, - children, - ref, - ...rest - } = props - - const containerRef = useRef(null) - const contentRef = useRef(null) - - const state: IParallax = useMemoOne( - () => ({ - config, - horizontal, - busy: false, - space: 0, - current: 0, - offset: 0, - controller: new Controller({ scroll: 0 }), - layers: new Set(), - container: containerRef, - content: contentRef, - update: () => update(), - scrollTo: offset => scrollTo(offset), - stop: () => state.controller.stop(), - }), - [] - ) - - useEffect(() => { - state.config = config - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config]) +export const Parallax = React.memo( + React.forwardRef((props, ref) => { + const [ready, setReady] = useState(false) + const { + pages, + innerStyle: _innerStyle, + config = configs.slow, + enabled = true, + horizontal = false, + children, + ...rest + } = props + + const containerRef = useRef(null) + const contentRef = useRef(null) + + const state: IParallax = useMemoOne( + () => ({ + config, + horizontal, + busy: false, + space: 0, + current: 0, + offset: 0, + controller: new Controller({ scroll: 0 }), + layers: new Set(), + container: containerRef, + content: contentRef, + update: () => update(), + scrollTo: offset => scrollTo(offset), + stop: () => state.controller.stop(), + }), + [] + ) - React.useImperativeHandle(ref, () => state) + useEffect(() => { + state.config = config + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]) - const update = () => { - const container = containerRef.current - if (!container) return + React.useImperativeHandle(ref, () => state) - const spaceProp = horizontal ? 'clientWidth' : 'clientHeight' - state.space = container[spaceProp] + const update = () => { + const container = containerRef.current + if (!container) return - const scrollType = getScrollType(horizontal) - if (enabled) { - state.current = container[scrollType] - } else { - container[scrollType] = state.current = state.offset * state.space - } + const spaceProp = horizontal ? 'clientWidth' : 'clientHeight' + state.space = container[spaceProp] - const content = contentRef.current - if (content) { - const sizeProp = horizontal ? 'width' : 'height' - content.style[sizeProp] = `${state.space * pages}px` - } + const scrollType = getScrollType(horizontal) + if (enabled) { + state.current = container[scrollType] + } else { + container[scrollType] = state.current = state.offset * state.space + } - state.layers.forEach(layer => { - layer.setHeight(state.space, true) - layer.setPosition(state.space, state.current, true) - }) - } + const content = contentRef.current + if (content) { + const sizeProp = horizontal ? 'width' : 'height' + content.style[sizeProp] = `${state.space * pages}px` + } - const scrollTo = (offset: number) => { - const container = containerRef.current - const scrollType = getScrollType(horizontal) + state.layers.forEach(layer => { + layer.setHeight(state.space, true) + layer.setPosition(state.space, state.current, true) + }) + } - state.offset = offset + const scrollTo = (offset: number) => { + const container = containerRef.current + const scrollType = getScrollType(horizontal) - state.controller.set({ scroll: state.current }) - state.controller.stop().start({ - scroll: offset * state.space, - config, - onChange({ value: { scroll } }: any) { - container[scrollType] = scroll - }, - }) - } + state.offset = offset - const onScroll = (event: any) => { - if (!state.busy) { - state.busy = true - state.current = event.target[getScrollType(horizontal)] - raf.onStart(() => { - state.layers.forEach(layer => - layer.setPosition(state.space, state.current) - ) - state.busy = false + state.controller.set({ scroll: state.current }) + state.controller.stop().start({ + scroll: offset * state.space, + config, + onChange({ value: { scroll } }: any) { + container[scrollType] = scroll + }, }) } - } - useEffect(() => state.update()) - useOnce(() => { - setReady(true) - - const onResize = () => { - const update = () => state.update() - raf.onFrame(update) - setTimeout(update, 150) // Some browsers don't fire on maximize! + const onScroll = (event: any) => { + if (!state.busy) { + state.busy = true + state.current = event.target[getScrollType(horizontal)] + raf.onStart(() => { + state.layers.forEach(layer => + layer.setPosition(state.space, state.current) + ) + state.busy = false + }) + } } - window.addEventListener('resize', onResize, false) - return () => window.removeEventListener('resize', onResize, false) - }) + useEffect(() => state.update()) + useOnce(() => { + setReady(true) - const overflow: React.CSSProperties = enabled - ? { - overflowY: horizontal ? 'hidden' : 'scroll', - overflowX: horizontal ? 'scroll' : 'hidden', - } - : { - overflowY: 'hidden', - overflowX: 'hidden', + const onResize = () => { + const update = () => state.update() + raf.onFrame(update) + setTimeout(update, 150) // Some browsers don't fire on maximize! } - return ( - - {ready && ( - <> - + window.addEventListener('resize', onResize, false) + return () => window.removeEventListener('resize', onResize, false) + }) + + const overflow: React.CSSProperties = enabled + ? { + overflowY: horizontal ? 'hidden' : 'scroll', + overflowX: horizontal ? 'scroll' : 'hidden', + } + : { + overflowY: 'hidden', + overflowX: 'hidden', + } + + return ( + + {ready && ( + <> + + + {mapChildrenRecursive( + children, + (child: any) => !child.props.sticky && child + )} + + {mapChildrenRecursive( children, - (child: any) => !child.props.sticky && child + (child: any) => child.props.sticky && child )} - - - {mapChildrenRecursive( - children, - (child: any) => child.props.sticky && child - )} - - - )} - - ) -}) + + )} + + ) + }) +) diff --git a/packages/react-spring/package.json b/packages/react-spring/package.json index 69614b18a..f5c724f5c 100644 --- a/packages/react-spring/package.json +++ b/packages/react-spring/package.json @@ -49,8 +49,8 @@ "@react-spring/zdog": "~9.7.4" }, "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" }, "scripts": { "build": "tsup", diff --git a/packages/shared/package.json b/packages/shared/package.json index 635e79254..c24acc83f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -46,7 +46,7 @@ "@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" }, "scripts": { "build": "tsup", diff --git a/targets/konva/package.json b/targets/konva/package.json index 02553fcd1..901782d34 100644 --- a/targets/konva/package.json +++ b/targets/konva/package.json @@ -48,7 +48,7 @@ }, "peerDependencies": { "konva": ">=2.6", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-konva": "^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0" }, "scripts": { diff --git a/targets/three/package.json b/targets/three/package.json index ca8812cfa..117d9be2e 100644 --- a/targets/three/package.json +++ b/targets/three/package.json @@ -48,7 +48,7 @@ }, "peerDependencies": { "@react-three/fiber": ">=6.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "three": ">=0.126" }, "scripts": { diff --git a/targets/web/package.json b/targets/web/package.json index db8174850..18298a867 100644 --- a/targets/web/package.json +++ b/targets/web/package.json @@ -47,8 +47,8 @@ "@react-spring/types": "~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" }, "scripts": { "build": "tsup", diff --git a/targets/web/src/animated.test.tsx b/targets/web/src/animated.test.tsx index 82301972a..922c923f5 100644 --- a/targets/web/src/animated.test.tsx +++ b/targets/web/src/animated.test.tsx @@ -26,16 +26,18 @@ describe('animated component', () => { expect(queryByTitle('Foo')).toBeTruthy() }) it('wraps a component', () => { - const Name = (props: { - name: string - other: string - children: React.ReactNode - ref?: React.RefObject - }) => ( -

+ const Name = React.forwardRef< + HTMLHeadingElement, + { + name: string + other: string + children: React.ReactNode + } + >((props, ref) => ( +

{props.children}

- ) + )) const AnimatedName = a(Name) const child = spring('Animated Text') const name = spring('name') @@ -61,15 +63,17 @@ describe('animated component', () => { expect(div.style.opacity).toBe('1') }) it('accepts Animated values in custom style prop', () => { - const Name = (props: { - style: { color: string; opacity?: number } - children: React.ReactNode - ref?: React.RefObject - }) => ( -

+ const Name = React.forwardRef< + HTMLHeadingElement, + { + style: { color: string; opacity?: number } + children: React.ReactNode + } + >((props, ref) => ( +

{props.children}

- ) + )) const AnimatedName = a(Name) const opacity = spring(0.5) const { queryByText } = render( diff --git a/targets/zdog/package.json b/targets/zdog/package.json index 17bb53051..454d003e4 100644 --- a/targets/zdog/package.json +++ b/targets/zdog/package.json @@ -47,8 +47,8 @@ "@react-spring/types": "~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", "react-zdog": ">=1.0", "zdog": ">=1.0" }, From eecd6723a2505ec975b3abd80fb7c9f073a7056c Mon Sep 17 00:00:00 2001 From: kierancap Date: Thu, 3 Apr 2025 11:31:20 +0100 Subject: [PATCH 08/10] fix: removed act batching and used act for timer advancing and flushing tasks --- packages/core/test/setup.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/core/test/setup.ts b/packages/core/test/setup.ts index 3b9af122f..9a5ca59e8 100644 --- a/packages/core/test/setup.ts +++ b/packages/core/test/setup.ts @@ -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, }) }) @@ -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. @@ -147,7 +144,7 @@ global.advanceUntil = async test => { } // Ensure pending effects are flushed. - await flushMicroTasks() + await act(() => flushMicroTasks()) // Prevent infinite recursion. if (++steps > 1e3) { From 8d61c6da8b6ed8f286d8c95b1d052fba4b009b22 Mon Sep 17 00:00:00 2001 From: kierancap Date: Fri, 11 Apr 2025 21:52:59 +0100 Subject: [PATCH 09/10] fix: readded forwardRef in withAnimated --- packages/animated/src/withAnimated.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/animated/src/withAnimated.tsx b/packages/animated/src/withAnimated.tsx index 071717c4f..6ed68132d 100644 --- a/packages/animated/src/withAnimated.tsx +++ b/packages/animated/src/withAnimated.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useRef, Ref, useCallback, useEffect } from 'react' +import { useRef, Ref, useCallback, useEffect, forwardRef } from 'react' import { is, each, @@ -27,7 +27,7 @@ export const withAnimated = (Component: any, host: HostConfig) => { !is.fun(Component) || (Component.prototype && Component.prototype.isReactComponent) - return (givenProps: any, givenRef: Ref) => { + return forwardRef((givenProps: any, givenRef) => { const instanceRef = useRef(null) // The `hasInstance` value is constant, so we can safely avoid @@ -94,7 +94,7 @@ export const withAnimated = (Component: any, host: HostConfig) => { const usedProps = host.getComponentProps(props.getValue()) return - } + }) } class PropsObserver { From d0527975a163d5bb9502b2e42ef3147067fc6a7f Mon Sep 17 00:00:00 2001 From: kierancap Date: Sun, 20 Apr 2025 10:39:00 +0100 Subject: [PATCH 10/10] fix: updated lock file --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index fa45ca266..d42d4538b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4613,7 +4613,7 @@ __metadata: "@react-spring/shared": ~9.7.5 "@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 languageName: unknown linkType: soft @@ -4625,7 +4625,7 @@ __metadata: "@react-spring/shared": ~9.7.5 "@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 languageName: unknown linkType: soft @@ -4730,7 +4730,7 @@ __metadata: "@react-spring/types": ~9.7.5 peerDependencies: konva: ">=2.6" - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-konva: ^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0 languageName: unknown linkType: soft @@ -4756,8 +4756,8 @@ __metadata: "@react-spring/shared": ~9.7.5 "@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 languageName: unknown linkType: soft @@ -4774,7 +4774,7 @@ __metadata: "@react-spring/rafz": ~9.7.5 "@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 languageName: unknown linkType: soft @@ -4788,7 +4788,7 @@ __metadata: "@react-spring/types": ~9.7.5 peerDependencies: "@react-three/fiber": ">=6.0" - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 three: ">=0.126" languageName: unknown linkType: soft @@ -4808,8 +4808,8 @@ __metadata: "@react-spring/shared": ~9.7.5 "@react-spring/types": ~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 languageName: unknown linkType: soft @@ -4822,8 +4822,8 @@ __metadata: "@react-spring/shared": ~9.7.5 "@react-spring/types": ~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 react-zdog: ">=1.0" zdog: ">=1.0" languageName: unknown @@ -17276,8 +17276,8 @@ __metadata: "@react-spring/web": ~9.7.4 "@react-spring/zdog": ~9.7.4 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 languageName: unknown linkType: soft