Next.js (React Native Web)でreact-native-reanimatedを使えるようにする設定が思いの外大変だったので、記録として残します。
ライブラリのバージョンは以下です。
React Native Webの設定については以前の記事を参考にしてください。
基本的な設定についてはReanimatedのドキュメントを参考にします。
ライブラリをインストールし、
yarn add react-native-reanimated
babel.config.jsの設定を追加します。
module.exports = {
...
plugins: [
...
'react-native-reanimated/plugin',
],
};
ReanimatedのWeb設定のドキュメント にWebpackの設定が載っているため、これを参考にします。
next.config.jsでWebpack Pluginの設定を追加します。
const nextConfig = {
webpack: config => {
config.plugins = [
...config.plugins,
new webpack.EnvironmentPlugin({ JEST_WORKER_ID: null }),
new webpack.DefinePlugin({
__DEV__: process.env.NODE_ENV === 'development',
}),
]
return config
}
}
module.exports = nextConfig
ドキュメントだとDefinePluginの部分は以下のようになっているのですが、Next.jsの場合これではenvの値がクリアされないため、 __DEV__
を明示的に指定する必要があります。
new webpack.DefinePlugin({ process: { env: {} } })
ドキュメントではbabel-polyfillの設定がありますがこれが無いとサーバサイドレンダリングでrequestAnimationFrameが呼び出された時にundefinedエラーになります。今回はMoti + Next.jsを参考にbabel-polyfillではなく、raf/polyfill
を pages/_app.tsxでimportします。
import "raf/polyfill"
next.jsのデフォルトの設定のままだとreanimatedがtranspileされずコンパイル時にエラーが発生してしまうためnext-transpile-moduleを利用してtranspileされるようにします。
const withTM = require("next-transpile-modules")(["react-native-reanimated"])
module.exports = withTM(nextConfig)
そのままreanimatedを実行しようとするとbabelのモジュールが無いと言われるため、エラーが出たものを都度インストールしていきます。
yarn add --dev @babel/plugin-transform-shorthand-properties @babel/plugin-transform-arrow-functions @babel/plugin-proposal-optional-chaining @babel/plugin-proposal-nullish-coalescing-operator @babel/plugin-transform-template-literals
reanimated 2.9.1では以下のエラーが発生します。
./node_modules/react-native-reanimated/lib/reanimated2/layoutReanimation/animationBuilder/Keyframe.js
TypeError: Cannot read properties of undefined (reading 'params')
(2022/09/27追記)この問題はバージョン2.10.0で修正されているようです。
バグのようなので以下のパッチを適用したら発生しなくなりました。
diff --git a/node_modules/react-native-reanimated/plugin.js b/node_modules/react-native-reanimated/plugin.js
--- a/node_modules/react-native-reanimated/plugin.js
+++ b/node_modules/react-native-reanimated/plugin.js
@@ -7,6 +7,7 @@
* holds a map of function names as keys and array of argument indexes as values which should be automatically workletized(they have to be functions)(starting from 0)
*/
const functionArgsToWorkletize = new Map([
+ ['useFrameCallback', [0]],
['useAnimatedStyle', [0]],
['useAnimatedProps', [0]],
['createAnimatedPropAdapter', [0]],
@@ -64,12 +65,15 @@
'Map',
'Set',
'_log',
- '_updateProps',
+ '_updatePropsPaper',
+ '_updatePropsFabric',
+ '_removeShadowNodeFromRegistry',
'RegExp',
'Error',
'global',
'_measure',
'_scrollTo',
+ '_dispatchCommand',
'_setGestureState',
'_getCurrentTime',
'_eventTimestamp',
@@ -305,13 +309,14 @@
},
});
+ const expression = fun.program.body.find(
+ ({ type }) => type === 'ExpressionStatement'
+ ).expression;
+
const workletFunction = t.functionExpression(
t.identifier(name),
- fun.program.body[0].expression.params,
- prependClosureVariablesIfNecessary(
- closureVariables,
- fun.program.body[0].expression.body
- )
+ expression.params,
+ prependClosureVariablesIfNecessary(closureVariables, expression.body)
);
return generate(workletFunction, { compact: true }).code;
今度は次のエラーが出ました。
error - ./node_modules/react-native-reanimated/lib/reanimated2/platform-specific/RNRenderer.js:3:0
Module not found: Can't resolve 'react-native/Libraries/Renderer/shims/ReactNative'
Import trace for requested module:
./node_modules/react-native-reanimated/lib/createAnimatedComponent.js
./node_modules/react-native-reanimated/lib/Animated.js
./node_modules/react-native-reanimated/lib/index.js
./components/Sample.tsx
./pages/index.tsx
Webpackの設定で以下を追加したら解決しました。
config.resolve.extensions = [
".web.js",
".web.ts",
".web.tsx",
...config.resolve.extensions,
]
やっとエラーがでなくなったので、実際にReanimatedの簡単なサンプルを動かしてみます。
ボタンを押すと横にランダムでSpringアニメーションします。
import { useCallback } from "react"
import { Button, StyleSheet, View } from "react-native"
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated"
export function ReanimatedSample() {
const offset = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
}
}, [offset])
return (
<View>
<Animated.View style={[styles.box, animatedStyles]} />
<View style={styles.buttonWrapper}>
<Button
title="Move"
onPress={useCallback(() => {
console.log("onPress()")
offset.value = withSpring(Math.random() * 255)
}, [offset])}
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
box: {
width: 100,
height: 100,
borderRadius: 16,
backgroundColor: "lightblue",
},
buttonWrapper: { marginTop: 24, width: 100 },
})
都度エラーが出るので心が折れそうになりましたが、なんとか動かすことができました。
当ブログの設定・コードは以下で確認可能です。 https://github.com/gaishimo/omoidasu-tech-blog