diff --git a/package.json b/package.json index fd8f9830..9699996d 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@emotion/react": "^11.10.5", "@preconstruct/cli": "^2.3.0", "@radix-ui/react-icons": "^1.1.1", + "@react-three/fiber": "^9.3.0", "@size-limit/preset-big-lib": "^8.1.0", "@stitches/react": "^1.2.8", "@storybook/addon-docs": "10.0.2", @@ -91,6 +92,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "10.0.2", "husky": "^8.0.3", + "jsdom": "^27.1.0", "playwright": "^1.56.1", "playwright-core": "^1.56.1", "pnpm": "^7.25.0", @@ -100,9 +102,9 @@ "react-dom": "^18.2.0", "size-limit": "^8.1.0", "storybook": "^10.0.2", + "three": "^0.169.0", "tsd": "^0.25.0", "typescript": "catalog:", - "jsdom": "^27.1.0", "vitest": "^4.0.8" }, "prettier": { diff --git a/packages/leva/src/hooks/useToggle.ts b/packages/leva/src/hooks/useToggle.ts index b5676b03..7438f093 100644 --- a/packages/leva/src/hooks/useToggle.ts +++ b/packages/leva/src/hooks/useToggle.ts @@ -80,7 +80,7 @@ import { useRef, useEffect, useLayoutEffect } from 'react' export function useToggle(toggled: boolean) { const wrapperRef = useRef(null) const contentRef = useRef(null) - const firstRender = useRef(true) + const prevToggledRef = useRef(null) // this should be fine for SSR since the store is set in useEffect and // therefore the pane doesn't show on first render. @@ -94,9 +94,11 @@ export function useToggle(toggled: boolean) { }, []) useEffect(() => { - // prevents first animation - if (firstRender.current) { - firstRender.current = false + // On initial render (including StrictMode's double-invocation), + // prevToggledRef.current is null, so we skip animation. + // We only animate when toggled actually changes from a previous value. + if (prevToggledRef.current === null || prevToggledRef.current === toggled) { + prevToggledRef.current = toggled return } @@ -120,6 +122,8 @@ export function useToggle(toggled: boolean) { timeout = window.setTimeout(() => (ref.style.height = '0px'), 50) } + prevToggledRef.current = toggled + return () => { ref.removeEventListener('transitionend', fixHeight) clearTimeout(timeout) diff --git a/packages/leva/stories/bug-repros.stories.tsx b/packages/leva/stories/bug-repros.stories.tsx index 118568e9..7769abcd 100644 --- a/packages/leva/stories/bug-repros.stories.tsx +++ b/packages/leva/stories/bug-repros.stories.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react' +import React, { StrictMode, useState } from 'react' import Reset from './components/decorator-reset' import { Meta } from '@storybook/react' -import { Leva, LevaPanel, useControls, useCreateStore } from '../src' +import { Leva, LevaPanel, useControls, useCreateStore, folder } from '../src' export default { title: 'Dev/BugRepro', @@ -102,3 +102,78 @@ export const ConditionalControls = () => { } ConditionalControls.storyName = '540 / conditional controls should work' + +// repro for https://github.com/pmndrs/leva/issues/552 +// Dynamic import to avoid build errors if @react-three/fiber is not installed +let Canvas: any = null +let Box: any = null + +try { + const fiber = require('@react-three/fiber') + Canvas = fiber.Canvas + + // Simple box component that uses leva controls + Box = ({ position }: { position: [number, number, number] }) => { + const { scale, color, wireframe, rotation } = useControls('Mesh Settings', { + scale: { value: 1, min: 0.1, max: 3, step: 0.1 }, + color: '#ff6b6b', + rotation: { value: { x: 0, y: 0, z: 0 }, step: 0.01 }, + wireframe: false, + }) + + return ( + + + + + ) + } +} catch (e) { + // R3F not installed, story will show error message +} + +export const StrictModeWithR3FCanvas = () => { + if (!Canvas) { + return ( +
+

@react-three/fiber not installed

+

+ This story requires @react-three/fiber and three to be installed as dev dependencies. +
+ Run: npm install --save-dev @react-three/fiber three +

+
+ ) + } + + return ( + +
+
+ StrictMode Enabled +

+ Controls should render correctly despite StrictMode's double-invocation. +

+
+ + + + + + +
+
+ ) +} + +StrictModeWithR3FCanvas.storyName = '552 / StrictMode with R3F Canvas should render controls'