Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nandorojo/e84e938568a018e044aadae9d64456ec to your computer and use it in GitHub Desktop.
Save nandorojo/e84e938568a018e044aadae9d64456ec to your computer and use it in GitHub Desktop.
Shared Element Transitions with React Navigation and Expo (2019)

Shared Element Transitions with React Navigation and Expo (2019)

It's 2019, and creating smooth shared element transitions in react native (& expo!) is finally easy.

Ideally, as Pablo Stanley suggests, your app's navigation will use these shared transitions for similar components that appear across screens.

Is it possible to achieve the great experience above using react-native/expo? Now it is.

If you'd rather see the example code right away, check out the Expo snack.

Step 1: Install dependencies

If you don't have an app created yet: run this in your command line:

npm install expo-cli
expo init shared-animation
// select managed, blank project

cd shared-animation

Next, install dependencies:

yarn add react-navigation-shared-element react-navigation react-navigation-stack react-navigation-hooks

For managed expo projects, run this too:

expo install react-native-reanimated react-native-gesture-handler react-native-screens

For non-expo projects, run this instead:

yarn add react-native-reanimated react-native-gesture-handler react-native-screens

Make sure you also properly link the dependencies if you aren't using a managed expo project. (Refer to each dependency's docs to see how to link it).

Step 2: Create the origin screen

We're going to create two screens with an image in each of them, like this:

Create a file at the root of your app directory called Origin.js.

Origin.js (or .tsx if you're using typescript)

import React from 'react'
import { View, TouchableOpacity, Image, StyleSheet } from 'react-native'
// Make sure this import isn't `react-native-shared-element` 👇
import { SharedElement } from 'react-navigation-shared-element'
import { useNavigation } from 'react-navigation-hooks'

const imageSource = { uri: 'https://source.unsplash.com/random' }

export default function Origin() {
  const { navigate } = useNavigation()
  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={() => navigate('Destination')}>
        <SharedElement id="someUniqueId">
          <Image source={imageSource} style={styles.image} />
        </SharedElement>
      </TouchableOpacity>
    </View>
  )
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  image: {
    height: 200,
    width: 200,
  },
})

So, what's going on above?

Navigation: First, we're using react navigation's navigate function from the useNavigation hook. Whenever our image is pressed, it will navigate to the Destination screen, which we're going to create in the next step.

Shared Element: The SharedElement component takes the id prop we give it and matches it with the id of a separate SharedElement component in our Destination screen. This way, it knows which two components should transition between each other.

Step 3: Create the destination screen

Create a file at the root of your app directory called Destination.js .

Destination.js

import React from 'react'
import { View, TouchableOpacity, Image, StyleSheet } from 'react-native'
import { SharedElement } from 'react-navigation-shared-element'

const imageSource = { uri: 'https://source.unsplash.com/random' }

export default function Destination() {
  return (
    <View>
      <SharedElement id="someUniqueId">
        <Image source={imageSource} style={styles.image} />
      </SharedElement>
    </View>
  )
}

Destination.sharedElements = (navigation, otherNavigation, showing) => {
  return ['someUniqueId']
}

const styles = StyleSheet.create({
  image: {
    height: 350,
    width: 350,
  },
})

Well that looks basically the same as the origin file, right?

One key difference is that we added the static sharedElements property to our Destination component. This lets the Destination component know which ids will transition.

It's also important to note that the SharedElement component above has the same id prop as the one in the Origin file.

If you had multiple SharedElement components per screen, they would transition based on this unique id.

Step 4: Configure react navigation

Copy the folowing into your App.js file:

App.js

import { createSharedElementStackNavigator } from 'react-navigation-shared-element'
import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import { useScreens as enableScreens } from 'react-native-screens'

enableScreens()

import Origin from './Origin'
import Destination from './Destination'

const navigator = createSharedElementStackNavigator(
  createStackNavigator, 
  {
    Origin, Destination
  }
)

const App = createAppContainer(navigator)
export default App

If you're on Expo SDK 36 or have react-native-screens 2.x, it will be imported like this instead:

import { enableScreens } from 'react-native-screens'

🥳 Your app now has shared element transitions

That was easy, right?

Run expo start --ios or expo start --android to see the result.

You can also refer to the Expo snack to see it in action in your browser.

...but what if we still want to add some complexity?

Expanding: Using a dynamic SharedElement id

What if the static string "someUniqueId" isn't sufficient? You can also use a dynamic variable as the id prop.

Something this: <SharedElement id={someVariable}>...</SharedElement>

This is a more common usage, since you'll probably get your ids from an API or a list of items.

Here are the changes you'd need to make to your two files.

Origin.js

Our origin file could look something like this:

export default function Origin() {
  const { navigate } = useNavigation()
  const data = [
    { id: 'pizza' }, 
    // ...
  ]
  return (
    <>
      {data.map(item => ( 
        <TouchableOpacity key={item.id} onPress={() => navigate('Destination', { id: item.id })}>
          <SharedElement id={`photo-${item.id}`}> // <-- notice this change
            <Image source={imageSource} style={styles.image} />
          </SharedElement>
        </TouchableOpacity>
      ))}
    </>
  )
}

In our onPress function, we add a second argument that sends an id parameter to the destination screen. If the item's id is pizza, then the SharedElement id would be photo-pizza.

Destination.js

import { useNavigationParam } from 'react-navigation-hooks'

export default function Destination() {
  const id = useNavigationParam('id') // "pizza"
  return (
    <View style={styles.container}>
        <SharedElement id={`photo-${id}`}>
          <Image source={imageSource} style={styles.image} />
        </SharedElement>
    </View>
  )
}

Destination.sharedElements = (navigation, otherNavigation, showing) => {
  const id = navigation.getParam('id') // "pizza"
  return [`photo-${id}`]
}

There are two key changes here:

  1. We access the id in the component with useNavigationParam('id').

  2. We changed the static sharedElements property to access the id parameter and pass our new id along.

In closing

Here's the final Expo snack.

I first working with React Native a year ago, and it's remarkable how quickly it has advanced.

Not long ago, a common mobile design pattern of shared element transitions was a big hurdle with react native.

Thanks to the people behind react-navigation, react-native-screens, react-native-shared-element and expo, creating truly native experiences with react native is becoming a breeze.

Drop a comment with any cool shared transition examples you come up with.

@hroland
Copy link

hroland commented Apr 8, 2020

Well wrote 👏

@nandorojo
Copy link
Author

Thanks!

@vamshi9666
Copy link

Good work.

@Seva98
Copy link

Seva98 commented Nov 15, 2020

Wait, how do you achieve that great example? Snack seems to be broken and following code from here or from Snack doesn't reproduce this result.

@nandorojo
Copy link
Author

nandorojo commented Nov 15, 2020

This was made with React navigation 4 and the old version of React Native shared elements. Maybe if you copy the code yourself into a repo with those versions it’ll work.

If you’re experienced enough with v5, I recommend using that.

@ahmadAlMezaal
Copy link

ahmadAlMezaal commented Jan 4, 2021

@nandorojo Thanks for the tutorial,
Got this Unable to resolve "react-native-shared-element" from "node_modules/react-navigation-shared-element/build/SharedElement.js", even though I have the package react-navigation-shared-element installed and I DID NOT import react-native-shared element for the SharedElement component.

Any suggestions?

@nandorojo
Copy link
Author

nandorojo commented Jan 4, 2021

Hey, the versions are a bit outdated now. I recommend watching Catalin Miron or Will Candillon’s videos on YouTube about shared element transitions with React Navigation v5!

My other suggestion, make sure you’ve installed both packages, including react-native-shared-element

@ahmadAlMezaal
Copy link

Unfortunately getting the same error after following the tutorial. I'm using V4 as well.
Posted a comment on the video and waiting for the response

@benbenzy
Copy link

can i use this with expo router

@nandorojo
Copy link
Author

no, this is outdated. use reanimated 3 transitions for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment