2022-08-08

Webでreact-native-reanimatedの設定を行う

Next.js (React Native Web)でreact-native-reanimatedを使えるようにする設定が思いの外大変だったので、記録として残します。

Move

ライブラリのバージョンは以下です。

  • Next.js: 12.2.3
  • react: 18.0.2
  • react-native-reanimated: 2.9.1

React Native Webの設定については以前の記事を参考にしてください。

基本設定

基本的な設定についてはReanimatedのドキュメントを参考にします。

ライブラリをインストールし、

yarn add react-native-reanimated

babel.config.jsの設定を追加します。

module.exports = {
    ...
    plugins: [
        ...
        'react-native-reanimated/plugin',
    ],
};

Web固有の設定

ReanimatedのWeb設定のドキュメント にWebpackの設定が載っているため、これを参考にします。

WebPack DefinePluginの追加

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: {} } })

requestAnimationFrameのpolyfillを追加

ドキュメントではbabel-polyfillの設定がありますがこれが無いとサーバサイドレンダリングでrequestAnimationFrameが呼び出された時にundefinedエラーになります。今回はMoti + Next.jsを参考にbabel-polyfillではなく、raf/polyfillを pages/_app.tsxでimportします。

import "raf/polyfill"

reanimatedのtranspile

next.jsのデフォルトの設定のままだとreanimatedがtranspileされずコンパイル時にエラーが発生してしまうためnext-transpile-moduleを利用してtranspileされるようにします。

const withTM = require("next-transpile-modules")(["react-native-reanimated"])


module.exports = withTM(nextConfig)

babel moduleの追加

そのまま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

reanimate 2.9.1のエラー

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;

Module not foundエラー

今度は次のエラーが出ました。

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を実際に動かしてみる

やっとエラーがでなくなったので、実際にReanimatedの簡単なサンプルを動かしてみます。

Move

ボタンを押すと横にランダムで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

最終更新: 2022-09-27 01:20
筆者: @gaishimo 主にReact Nativeでのアプリ開発を行っています。
© 2021 Omoidasu, Inc. All rights reserved.