From 0b4468adfda19eb22274d56712971cce265a4b50 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sat, 24 Oct 2026 13:54:24 +0100 Subject: [PATCH] fix(ui): stop passing useAnimatedStyle dependencies on native Reanimated 4.6+ ignores the dependency array on native, where the worklets plugin already captures the closure, and warns about it ("Dependencies should only be used on the web"). The array is kept on web, where it is still used when the worklets babel plugin does not run: without it a production web build would compute maxHeight only once and clip the panel on a narrow viewport. Adds a regression test that renders MobileAddonsPanel and asserts useAnimatedStyle receives no dependency array on native and the dependencies on web. Fixes #943 (cherry picked from commit 590b304b6307dac41c8a4d4d265e0c6d91f265f7) --- .changeset/no-native-animated-style-deps.md | 5 ++ .../__mocks__/@gorhom/bottom-sheet.js | 1 + .../@storybook/react-native-theming.js | 5 ++ .../@storybook/react-native-ui-common.js | 5 ++ .../__mocks__/react-native-gesture-handler.js | 3 + .../__mocks__/react-native-reanimated.js | 2 + .../__mocks__/react-native-svg.js | 4 ++ .../react-native-ui/__mocks__/react-native.js | 3 + .../__mocks__/storybook/manager-api.js | 3 + .../src/MobileAddonsPanel.test.tsx | 60 +++++++++++++++++++ .../react-native-ui/src/MobileAddonsPanel.tsx | 16 +++-- 11 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 .changeset/no-native-animated-style-deps.md create mode 100644 packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js create mode 100644 packages/react-native-ui/__mocks__/react-native-gesture-handler.js create mode 100644 packages/react-native-ui/__mocks__/react-native-svg.js create mode 100644 packages/react-native-ui/__mocks__/storybook/manager-api.js create mode 100644 packages/react-native-ui/src/MobileAddonsPanel.test.tsx diff --git a/.changeset/no-native-animated-style-deps.md b/.changeset/no-native-animated-style-deps.md new file mode 100644 index 0000000000..95b3062ffe --- /dev/null +++ b/.changeset/no-native-animated-style-deps.md @@ -0,0 +1,5 @@ +--- +'@storybook/react-native-ui': patch +--- + +The addons panel no longer passes `useAnimatedStyle` dependencies on native, where Reanimated 4.6+ ignores them and warns ("Dependencies should only be used on the web"). diff --git a/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js b/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js index 159d67a596..4fb2e96460 100644 --- a/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js +++ b/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js @@ -9,5 +9,6 @@ module.exports = { __esModule: true, default: renderChildren(), BottomSheetBackdrop: jest.fn(() => null), + BottomSheetModal: jest.fn(() => null), BottomSheetScrollView: jest.fn(() => null), }; diff --git a/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js b/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js index a4b286d681..0bd2c5a96d 100644 --- a/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js +++ b/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js @@ -1,8 +1,13 @@ const theme = { + appBorderColor: '#e0e0e0', + barSelectedColor: '#1b71ba', + color: { mediumdark: '#999999' }, background: { content: '#ffffff' }, textMutedColor: '#999999', }; module.exports = { + // `styled.X(styleFn)` returns a component; host names keep the elements renderable. + styled: { Text: () => 'Text', TouchableOpacity: () => 'TouchableOpacity' }, useTheme: jest.fn(() => theme), }; diff --git a/packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js b/packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js new file mode 100644 index 0000000000..b9c45472f6 --- /dev/null +++ b/packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js @@ -0,0 +1,5 @@ +// `useStyle` memoizes in the real package; recomputing is enough here. +module.exports = { + IconButton: 'IconButton', + useStyle: (styleFactory) => styleFactory(), +}; diff --git a/packages/react-native-ui/__mocks__/react-native-gesture-handler.js b/packages/react-native-ui/__mocks__/react-native-gesture-handler.js new file mode 100644 index 0000000000..18218db7f9 --- /dev/null +++ b/packages/react-native-ui/__mocks__/react-native-gesture-handler.js @@ -0,0 +1,3 @@ +module.exports = { + ScrollView: 'ScrollView', +}; diff --git a/packages/react-native-ui/__mocks__/react-native-reanimated.js b/packages/react-native-ui/__mocks__/react-native-reanimated.js index 45bbd9f0d5..f6db54c864 100644 --- a/packages/react-native-ui/__mocks__/react-native-reanimated.js +++ b/packages/react-native-ui/__mocks__/react-native-reanimated.js @@ -1,4 +1,6 @@ module.exports = { + useAnimatedKeyboard: jest.fn(), useAnimatedStyle: jest.fn(() => ({})), useReducedMotion: jest.fn(() => false), + useSharedValue: jest.fn((initial) => ({ value: initial })), }; diff --git a/packages/react-native-ui/__mocks__/react-native-svg.js b/packages/react-native-ui/__mocks__/react-native-svg.js new file mode 100644 index 0000000000..6fd2745e15 --- /dev/null +++ b/packages/react-native-ui/__mocks__/react-native-svg.js @@ -0,0 +1,4 @@ +module.exports = { + Path: 'Path', + Svg: 'Svg', +}; diff --git a/packages/react-native-ui/__mocks__/react-native.js b/packages/react-native-ui/__mocks__/react-native.js index fb6091e68d..fe7c6b50ba 100644 --- a/packages/react-native-ui/__mocks__/react-native.js +++ b/packages/react-native-ui/__mocks__/react-native.js @@ -1,4 +1,7 @@ module.exports = { Keyboard: { dismiss: jest.fn() }, Platform: { OS: 'ios' }, + Text: 'Text', + View: 'View', + useWindowDimensions: jest.fn(() => ({ width: 390, height: 844, scale: 3, fontScale: 1 })), }; diff --git a/packages/react-native-ui/__mocks__/storybook/manager-api.js b/packages/react-native-ui/__mocks__/storybook/manager-api.js new file mode 100644 index 0000000000..739eb9ac10 --- /dev/null +++ b/packages/react-native-ui/__mocks__/storybook/manager-api.js @@ -0,0 +1,3 @@ +module.exports = { + addons: { getElements: jest.fn(() => ({})) }, +}; diff --git a/packages/react-native-ui/src/MobileAddonsPanel.test.tsx b/packages/react-native-ui/src/MobileAddonsPanel.test.tsx new file mode 100644 index 0000000000..7857b7eb7a --- /dev/null +++ b/packages/react-native-ui/src/MobileAddonsPanel.test.tsx @@ -0,0 +1,60 @@ +import { act } from 'react'; +import { Platform } from 'react-native'; +import { useAnimatedStyle } from 'react-native-reanimated'; +import { createRoot } from 'test-renderer'; +import { MobileAddonsPanel } from './MobileAddonsPanel'; + +const animatedStyle = useAnimatedStyle as unknown as jest.Mock; +const platform = Platform as { OS: string }; + +// shared value, then the window height from the react-native mock and the bottom inset from +// __mocks__/react-native-safe-area-context.js +const deps = [expect.any(Object), 844, 34]; + +const renderPanel = async () => { + const root = createRoot(); + + await act(async () => { + root.render(); + }); + + return root; +}; + +const depsArgument = () => animatedStyle.mock.calls[0][1]; + +describe('MobileAddonsPanel', () => { + const originalOS = platform.OS; + + afterEach(() => { + platform.OS = originalOS; + animatedStyle.mockClear(); + }); + + it.each(['ios', 'android'])( + 'does not pass dependencies to useAnimatedStyle on %s', + async (os) => { + platform.OS = os; + const root = await renderPanel(); + + expect(animatedStyle).toHaveBeenCalled(); + expect(depsArgument()).toBeUndefined(); + + await act(async () => { + root.unmount(); + }); + } + ); + + it('passes dependencies to useAnimatedStyle on web', async () => { + platform.OS = 'web'; + const root = await renderPanel(); + + // web builds without the worklets babel plugin only recompute the style when deps change + expect(depsArgument()).toEqual(deps); + + await act(async () => { + root.unmount(); + }); + }); +}); diff --git a/packages/react-native-ui/src/MobileAddonsPanel.tsx b/packages/react-native-ui/src/MobileAddonsPanel.tsx index 65af43fc40..b22ee18c14 100644 --- a/packages/react-native-ui/src/MobileAddonsPanel.tsx +++ b/packages/react-native-ui/src/MobileAddonsPanel.tsx @@ -60,12 +60,16 @@ export const MobileAddonsPanel = forwardRef { - const extraPadding = Platform.OS === 'android' ? 32 : 16 + insets.bottom; - return { - maxHeight: height - animatedPosition.value - extraPadding, - }; - }, [animatedPosition, height, insets.bottom]); + const adjustedBottomSheetSize = useAnimatedStyle( + () => { + const extraPadding = Platform.OS === 'android' ? 32 : 16 + insets.bottom; + return { + maxHeight: height - animatedPosition.value - extraPadding, + }; + }, + // ignored (and warned about) on native; web without the babel plugin needs them + Platform.OS === 'web' ? [animatedPosition, height, insets.bottom] : undefined + ); const backgroundStyle = useStyle(() => { return {