nandorojo/moti

moti requires expo-linear-gradient when im using react native cli only without expo @nandorojo

Opened this issue · 12 comments

Is there an existing issue for this?

  • I have searched the existing issues

Do you want this issue prioritized?

  • Yes, I have sponsored
  • Not urgent

Current Behavior

getting this weird error while trying to use moti with react native only

my package json contains these packages

"react-native-reanimated": "^3.6.1",
"react-native": "0.72.7",
"moti": "^0.27.2",
Screenshot 2023-12-18 at 7 29 38 PM

Expected Behavior

getting this weird error while trying to use moti with react native only

my package json contains these packages

"react-native-reanimated": "^3.6.1",
"react-native": "0.72.7",
"moti": "^0.27.2",
Screenshot 2023-12-18 at 7 29 38 PM

Steps To Reproduce

create new react native project with npx react-native init project
install moti package
instal reanimated package
use in in the code

Versions

"react-native-reanimated": "^3.6.1",
    "react-native": "0.72.7",
    "moti": "^0.27.2",

Screenshots

Screenshot 2023-12-18 at 7 29 38 PM Screenshot 2023-12-18 at 7 32 35 PM

Reproduction

npx create-react-native-app -t with-moti

@husam868maher @nandorojo same here any fix for this ?

Did you use the non-expo approach from the skeleton on docs?

https://moti.fyi/skeleton#non-expo-users

Hey @nandorojo!

Recently I've also tried to use Skeleton component with react-native-cli & react-native-lineal-gradient but got the same error as @yamak-app

Seems like your Babel plugin isn't working

You added the module resolver one from the docs?

Hi @nandorojo !

Thanks again for such great package for animations. I've resolved
the problem for react-native cli by using this small patch changes:

moti+0.27.2.patch

Can you please send the code inline?

expo.tsx file:

import { LinearGradient } from 'react-native-linear-gradient'
import React from 'react'

import SkeletonNative from './skeleton-new'
import { MotiSkeletonProps } from './types'

export default function SkeletonExpo(
  props: Omit<MotiSkeletonProps, 'Gradient'>
) {
  return <SkeletonNative {...props} Gradient={LinearGradient as any} />
}

SkeletonExpo.Group = SkeletonNative.Group

skeleton.tsx:


import { LinearGradient } from 'react-native-linear-gradient'
import React, { useState, createContext, useContext } from 'react'
import { View, StyleSheet } from 'react-native'

import { View as MotiView } from '../components'
import { AnimatePresence, MotiTransitionProp } from '../core'
import {
  DEFAULT_SKELETON_SIZE as DEFAULT_SIZE,
  defaultDarkColors,
  defaultLightColors,
  baseColors,
} from './shared'
import { MotiSkeletonProps } from './types'

export default function Skeleton(props: MotiSkeletonProps) {
  const skeletonGroupContext = useContext(SkeletonGroupContext)
  const {
    radius = 8,
    children,
    show = skeletonGroupContext ?? !children,
    width,
    height = children ? undefined : DEFAULT_SIZE,
    boxHeight,
    colorMode = 'dark',
    colors = colorMode === 'dark' ? defaultDarkColors : defaultLightColors,
    backgroundColor = colors[0] ??
      colors[1] ??
      baseColors[colorMode]?.secondary,
    backgroundSize = 6,
    disableExitAnimation,
    transition,
  } = props

  const [measuredWidth, setMeasuredWidth] = useState(0)

  const getBorderRadius = () => {
    if (radius === 'square') {
      return 0
    }
    if (radius === 'round') {
      return 99999
    }
    return radius
  }

  const borderRadius = getBorderRadius()

  const getOuterHeight = () => {
    if (boxHeight != null) return boxHeight
    if (show && !children) {
      return height
    }
    return undefined
  }

  const outerHeight = getOuterHeight()

  return (
    <View
      style={{
        height: outerHeight,
        minHeight: height,
        minWidth: width ?? (children ? undefined : DEFAULT_SIZE),
      }}
    >
      {children}
      <AnimatePresence>
        {show && (
          <MotiView
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              borderRadius,
              width: width ?? (children ? '100%' : DEFAULT_SIZE),
              height: height ?? '100%',
              overflow: 'hidden',
            }}
            animate={{
              backgroundColor,
              opacity: 1,
            }}
            transition={{
              type: 'timing',
            }}
            exit={
              !disableExitAnimation && {
                opacity: 0,
              }
            }
            onLayout={({ nativeEvent }) => {
              if (measuredWidth === nativeEvent.layout.width) return

              setMeasuredWidth(nativeEvent.layout.width)
            }}
            pointerEvents="none"
          >
            <AnimatedGradient
              // force a key change to make the loop animation re-mount
              key={`${JSON.stringify(colors)}-${measuredWidth}-${JSON.stringify(
                transition || null
              )}`}
              colors={colors}
              backgroundSize={backgroundSize}
              measuredWidth={measuredWidth}
              transition={transition}
            />
          </MotiView>
        )}
      </AnimatePresence>
    </View>
  )
}

const AnimatedGradient = React.memo(
  function AnimatedGradient({
    measuredWidth,
    colors,
    backgroundSize,
    transition = {},
  }: {
    measuredWidth: number
    colors: string[]
    backgroundSize: number
    transition?: MotiTransitionProp
  }) {
    return (
      <MotiView
        style={StyleSheet.absoluteFillObject}
        from={{ opacity: 0 }}
        transition={{
          type: 'timing',
          duration: 200,
        }}
        animate={
          measuredWidth
            ? {
                opacity: 1,
              }
            : undefined
        }
      >
        <MotiView
          style={[
            StyleSheet.absoluteFillObject,
            {
              width: measuredWidth * backgroundSize,
            },
          ]}
          from={{
            translateX: 0,
          }}
          animate={
            measuredWidth
              ? {
                  translateX: -measuredWidth * (backgroundSize - 1),
                }
              : undefined
          }
          transition={{
            loop: true,
            delay: 200,
            type: 'timing',
            duration: 3000,
            ...(transition as any),
          }}
        >
          <LinearGradient
            colors={colors}
            start={{
              x: 0.1,
              y: 1,
            }}
            end={{
              x: 1,
              y: 1,
            }}
            style={StyleSheet.absoluteFillObject}
          />
        </MotiView>
      </MotiView>
    )
  },
  function propsAreEqual(prev, next) {
    if (prev.measuredWidth !== next.measuredWidth) return false

    if (prev.backgroundSize !== next.backgroundSize) return false

    const didColorsChange = prev.colors.some((color, index) => {
      return color !== next.colors[index]
    })

    if (didColorsChange) return false

    // transition changes will not be respected, but it'll be in the key
    return true
  }
)

const SkeletonGroupContext = createContext<boolean | undefined>(undefined)

function SkeletonGroup({
  children,
  show,
}: {
  children: React.ReactNode
  /**
   * If `true`, all `Skeleton` children components will be shown.
   *
   * If `false`, the `Skeleton` children will be hidden.
   */
  show: boolean
}) {
  return (
    <SkeletonGroupContext.Provider value={show}>
      {children}
    </SkeletonGroupContext.Provider>
  )
}

Skeleton.Group = SkeletonGroup

The only important change here is this new import { LinearGradient } from 'react-native-linear-gradient' instead of expo-linear-gradient.

babel.config.js:

module.exports = function (api) {
  api.cache(true);

 return {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    'react-native-reanimated/plugin',
    [
      'babel-plugin-module-resolver',
      {
        root: ['./src/'],
        alias: [
          { 'moti/skeleton': 'moti/skeleton/react-native-linear-gradient' },
        ],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    ],
  ],
  };
};

I didn't properly tested it with other RN versions. In my case it's working like a charm for both Android/iOS "react-native": "0.72.7"

@nandorojo Let me know if it's working on your side?

By the way, maybe it's better to override this react-native-linear-gradient/expo-linear-gradient dependency directly in json with "overrides" or something similar to it. What do you think?

Please send the patch file inline

What's wrong with the current docs for the skeleton using the Babel plugin? Doesn't it do this for you?

diff --git a/node_modules/moti/src/skeleton/expo.tsx b/node_modules/moti/src/skeleton/expo.tsx
index 1674b89..083824c 100644
--- a/node_modules/moti/src/skeleton/expo.tsx
+++ b/node_modules/moti/src/skeleton/expo.tsx
@@ -1,4 +1,4 @@
-import { LinearGradient } from 'expo-linear-gradient'
+import { LinearGradient } from 'react-native-linear-gradient'
 import React from 'react'
 
 import SkeletonNative from './skeleton-new'
diff --git a/node_modules/moti/src/skeleton/skeleton.tsx b/node_modules/moti/src/skeleton/skeleton.tsx
index 3ef5dd1..0e3bed3 100644
--- a/node_modules/moti/src/skeleton/skeleton.tsx
+++ b/node_modules/moti/src/skeleton/skeleton.tsx
@@ -1,4 +1,4 @@
-import { LinearGradient } from 'expo-linear-gradient'
+import { LinearGradient } from 'react-native-linear-gradient'
 import React, { useState, createContext, useContext } from 'react'
 import { View, StyleSheet } from 'react-native'
 
diff --git a/node_modules/moti/src/skeleton/types.ts b/node_modules/moti/src/skeleton/types.ts
index 7fa9d78..176f224 100644
--- a/node_modules/moti/src/skeleton/types.ts
+++ b/node_modules/moti/src/skeleton/types.ts
@@ -1,4 +1,4 @@
-import { LinearGradient } from 'expo-linear-gradient'
+import { LinearGradient } from 'react-native-linear-gradient'
 import { MotiTransitionProp } from '../core'
 import { baseColors } from './shared'

babel-plugin is working as expected now. Before, I had some troubles with changing babel.config.js.