Vilva Athiban P B JavaScript developer. React, Node, GraphQL. Trying to make the web a better place to browse.

Run animations in React and React Native from one codebase

7 min read 2094

Introduction

Animations are important to make an app interactive. They make the user feel more connected with the app.

With React Native gaining momentum, a lot of products are either built with React Native or have migrated to it. However, while code for webpages can be reused in mobile apps in some cases, a major problem is that animations often cannot carry over, as the APIs are completely different.

In this article, we will learn how to leverage React Native file resolution and react-spring (an animation library) to write a single codebase that runs animations in both React and React Native.

We will start by learning about React Native file resolution and react-spring, and continue by building a very basic sample app. In this sample app, we will write one code which will work both in React on the web, and React Native in mobile apps.

React Native file resolution

React Native has an efficient file resolution system that lets us write platform-specific or native-specific code. It will detect when a file has a .ios.js or .android.js extension and load the relevant platform(Android/iOS) file when required from other components. This way we can write two different pieces of code or components for two different platforms.

Likewise, when we create files with a .js or .native.js extension, the .js file is picked up by Node.js and web, whereas the .native.js file is picked up by the React Native Metro bundler.

This gives us the power to build easily maintainable apps and reuse a lot of code across platforms.

Consider a folder of the below structure:

|-TestComponent
  |-index.js
  |-index.native.js

Let’s assume we are using the TestComponent component in the App component as follows:

const App = () => (
  <div>
    <TestComponent />
  </div>
)

When the code is bundled for the browser, an import of TestComponent will import the component from index.js file. If the code is bundled for React Native, however, it picks up index.native.js.

We made a custom demo for .
No really. Click here to check it out.

We will leverage this aspect of the file resolution to write a single animation code for both platforms.

React-spring

React-spring is a spring physics-based animation library that should cover most of your UI-related animation needs. It gives you tools flexible enough to confidently cast your ideas into moving interfaces.

React-spring is a bridge between the two existing React animation libraries: React Motion and Animated. It inherits Animated’s powerful interpolations and performance, as well as React Motion’s ease of use.

The main advantage of react-spring over other animation libraries is its ability to apply animations without relying on React to render updates frame by frame. Animations are based on physics.

There is no need (unless you do so intentionally) to customize duration or easing. It means your animations can handle interruptions and changes pretty seamlessly. The result is smooth, soft, and natural-looking animations.

React-spring is also a cross-platform animation library. It has similar implementations for web, React Native, and other platforms, but as different packages. We can pick the right module based on our requirements. However, they share the same APIs for all platforms. This helps us use react-spring to achieve our goal of one code for all platforms.

We will use the useSpring hook for the sample app we are going to build in this article. useSpring turns values into animated values by either overwriting values to change the animation or updating the values dynamically by passing a callback function using the API.

Building a sample app

Let us use the above concepts we just learned and build a sample app. This sample app will use React for the web, React Native for mobile apps, and react-spring for animation. Functionally it will be a very simple app wherein a text will appear and zoom in on load of the application, both in mobile apps and on the web.

However, we will have to first create the basic building blocks for this app to work. You can manually configure Metro bundler and webpack or use the existing starter kits.

I have used expo and create-react-app merged into single project. The complete working version of the app can be found here.

Let’s see some code now. Consider the following project:

|-src
  |-Box
    |-index.js
    |-Box.js
    |-Box.native.js
  |-Text
    |-index.js
    |-Text.js
    |-Text.native.js
  |-App.js

We have two components here: Box and Text. Both components leverage file resolution in React Native, which enables us to write single component that works for both platforms.

Box

Box is a container component. We will use Box as a container for all the other components or texts.

In web, we use div or section as containers. However, React Native doesn’t support these elements and uses the View component as a container instead. Our aim is to write one code for all platforms, hence we will create this Box component to translate to div or View based on the platform.

Considering the file resolution of React Native, we will create two files for the Box components:

  • Box.js
  • Box.native.js

The Box.js file will be used by the web platform and is just an alias for a div. This will export the div element like so:

// Box.js
export default 'div';

The Box.native.js file will be used by the React Native platform. We import the View component from React Native and export it from the file like so:

// Box.native.js
import {View} from 'react-native'
export default View

We create a common index.js file, so it’s easy and more readable to import and use the Box component in other files. If you see here, we don’t explicitly mention whether Box is imported from .js or .native.js file.

We let the bundlers decide, and just export the component in index.js file, so it can be used by other components:

// index.js
import Box from './Box';
export default Box

Text

The Text component will be used to add text to both web and mobile apps. In web, we use elements like h1, h2, or p for displaying text, whereas in React Native we use the Text component. This Text component will now translate to p or Text based on the platform.

We will create two files for the Text component:

  • Text.js
  • Text.native.js

The Text.js file will be used by the web platform and is just an alias for p. This will export the p element like so:

// Text.js
export default 'p';

The Text.native.js file will be used by the React Native platform. We import the Text component from React Native and export it from the file here:

// Text.native.js
import {Text} from 'react-native'
export default Text

Like the Box component, we create a common index.js file here as well. This makes it easy and more readable to import, and to use the Text component in other files:

// index.js
import Text from './Text';
export default Text

Now that we have the basic building blocks of the Box and Text components, we can go ahead and build the sample app.

Let’s create a new component, App.js, which will use Box as container and Text to display our title: React Spring Animation. We can also add some styling to the component.

It should look like this:

// App.js

import React from 'react';
import Box from './Box';
import Text from './Text';

function App() {
  return (
    <Box style={{ marginTop: 50}}>
      <Text style={{ fontSize: 50 }}>React Spring Animation</Text>
    </Box>
  );
}
export default App;

The above code creates the same text for both mobile and web, which you can see in the screenshot below. Under the hood, the code is bundled before being executed in any platform.

When webpack bundles the code for web, it uses the Box.js and Text.js files, adds a div element and p element with the text React Spring Animation to the App component, and runs it on the browser.

However, Metro bundles Box.native.js and Text.native.js files and adds View and Text components from React Native into the App component. This gives intended result in mobile apps as well.

So far, this is what our app looks like on mobile and on the web:Screenshot of mobile app that says "react spring animation" without movement Screenshot of webpage that says "react spring animation" without movement

Adding animation

Time to add animation to the above application. Let’s add a “zoom in” animation to the text by increasing the font size.

First, install react-spring as dependency to the project:

npm i react-spring

yarn add react-spring

Next, build an Animated component similar to the Box and Text components. This Animated component will be our single source for all the hooks, APIs, and utils from the react-spring library.

We will structure it similarly to our Box and Text components, thereby using the right hooks and APIs based on each platform.

React-spring has two modules we will be using: react-spring for web and react-spring/native for React Native. In this example we will be using the useSpring hook of react-spring for implementing the animation.

However, we should import useSpring from react-spring for the web and useSpring from react-spring/native for React Native. Hence we cant directly use react-spring.

Considering the facts, we will build an Animated component which will help us write single animation for multiple platforms.

Lets add a new component folder as below:

|-src
  |-Animated
    |-index.js
    |-Animated.js
    |-Animated.native.js

Animated

The Animated component will supply all required hooks and utils from react-spring. In web, we use elements like useSpring and useTransition from react-spring for animations, whereas in React Native we use the same hooks from react-spring/native.

This Animated component will import useSpring from either react-spring or react-spring/native based on the platform.

We will create two files for the Animated component:

  • Animated.js
  • Animated.native.js

The Animated.js file will be used by the web platform and will import useSpring and animated from react-spring and export it:

// Animated.js
export { useSpring, animated } from 'react-spring'

The Animated.native.js file will be used by the React Native platform and will import useSpring and animated from react-spring/native and export it:

// Animated.native.js
export { useSpring, animated } from 'react-spring/native'

We can create a common index.js file now, which will import and export the Animated components like Box and Text. This will make it easy and more readable to import and use the Animated component in other files:

// index.js
export {useSpring, animated} from './Animated'

Now, let’s use the Animated component in App.js to animate the text.

Import useSpring and animated from the Animated component:

import { useSpring, animated } from './Animated';

Then, create an animation enabled component using the Animated function. A component can be animated using react-spring only if it is extended by the Animated function:

const AnimatedText = animated(Text);

Create the animation, which is very similar to the CSS keyframe animation, using the useSpring function.

Keyframes in CSS control the intermediate steps in a CSS animation sequence by defining styles for keyframes (or waypoints) along the animation sequence. Hence, we can determine the style of an element during different phases of an animation.

useSpring works exactly same. You can define how certain styles should look like during the start and end of animations using the from and to properties respectively:

const styles = useSpring({
    from: {
      fontSize: 10,
    },
    to: { 
      fontSize: 50,
     },
  })

Finally, pass the styles to the style attribute of AnimatedText component:

<AnimatedText style={{ ...styles }}>React Spring Animation</AnimatedText>

Putting everything together, App.js should look like the following:

import React from 'react';
import { useSpring, animated } from './Animated';
import Box from './Box';
import Text from './Text';

const AnimatedText = animated(Text)

function App() {
  const styles = useSpring({
    from: {
      fontSize: 10,
    },
    to: { 
      fontSize: 50,
     }
  })
  return (
    <Box style={{ marginTop: 50}}>
      <AnimatedText style={{ ...styles }}>React Spring Animation</AnimatedText>
    </Box>
  );
}

The above code adds a “zoom in” animation to the text for both web and mobile (via React Native).

You can see how it looks in the gifs below:

gif of webpage that says "react spring animation" with zoom in animation gif of mobile app that says "react spring animation" with zoom in animation

Conclusion

Using the above technique, we can create almost any animation only once and reuse it across web and mobile platforms. This enables quick prototyping and easy maintenance. It also facilitates consistent behavior of features across platforms.

Unfortunately, this technique comes with a small caveat: only CSS properties that are supported by both React and React Native can be animated this way. Anything that involves other CSS properties should still maintain two versions of code for both platforms.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Vilva Athiban P B JavaScript developer. React, Node, GraphQL. Trying to make the web a better place to browse.

Leave a Reply