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.
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).
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.
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 id
s 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
.
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'
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?
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 id
s 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:
-
We access the id in the component with
useNavigationParam('id')
. -
We changed the static
sharedElements
property to access the id parameter and pass our new id along.
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.
Well wrote 👏