A card flip animation in React Native with Hooks

Josh Frank
6 min readAug 13, 2021

Before I begin, I should mention it’s possible I won’t be blogging as frequently in the near future thanks to an unbelievable new opportunity! It’s not final yet but I’m so excited to share more soon!

I love long-form writing, but it’s a little exhausting and soon I’ll have even less free time for it. So, when I’m back to blogging, this Medium account will look a little different. It’ll be more a notepad of ideas, which fits the way I think anyway, and not as many essays (though I still have to finish my Express/Sequelize/PostreSQL tutorial).

Now that’s out of the way, let’s get back to familiar territory for this blog: React, Poker and front-end design…

Background

I’ve blogged about React before but never about React Native. React Native is to mobile app development what React is to web app development: the two are sister frameworks, both created by Facebook engineers, with similar purpose, concepts and principles. (React is not the same as React Native and copy-pasting code from one into the other often doesn’t work!)

Forging ahead with a mobile Poker game in React Native, I wanted to implement a card-flip animation for playing card images:

The tutorials I saw for doing this are great, but I’ve never seen an updated walkthrough for React Native’s hooks. Big shout-out to Jason Brown whose approach in this awesome lecture video I updated for this brief walkthrough. I love hooks in React — let’s see if they’re just as awesome in React Native!

Components in React Native

A quick look at Card.js will show you a bit of the latest and greatest from React Native’s component library:

import { Image, View, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {  ...  return (
<View
style={ style.cardWrapper }
>
<Image
style={ style.cardFront }
source={ require( ... ) }
/>
<Image
style={ style.cardBack } }
source={ require( ... ) }
/>
</View>
);
}const style = StyleSheet.create( {
cardWrapper: { ... },
cardFront: {
...
position: "absolute",
},
cardBack: {
...
backfaceVisibility: "hidden"
},
};

Some important notes:

  • The basic building block of React Native is the View, which functions as an all-purpose div or section tag. Native also gives us other familiar DOM elements like Text, TextInputs, Switches, Buttons and several kinds of Lists in addition to the Images we see above. Soon we’ll respond to finger touches by replacing View with a component called Pressable— an update to an existing component called Touchable.
  • You can probably tell from the dispatch prop that yes, a dialect of Redux is available in React Native — again, different from Redux in React. A full tutorial on Redux in React Native is beyond this article’s scope so I’m not covering it. Consult the Redux docs or find one of the many great tutorials floating around if you’d like to learn more.
  • We don’t style React Native with CSS: it gives us a StyleSheet class instead, initialized with a JS object where each style is a key; values are themselves objects nearly identical to React style objects, with options like margin and position and display familiar from CSS. We then apply those styles to a component with the style={} attribute, which is broadly similar to className in React.
  • Notice in our component, we have two images ( for the card front and back); see that in our StyleSheet we place the cardFront on top with position: "absolute" and use backfaceVisibility to hide the cardBack when the card is flipped.
  • Assets like local images must be require()d! Also note that you must hard-write sources for every require(): you can’t use require() with an interpolated string in React Native, as in: require( `./assets/images/logos/${ colorName }.png` ).

The Animated component

Let’s get a big round of applause now for the main event: the Animated class & components. It’s a solid and snazzy little CSS animation library with decent range, and we’ll get started using it with some definitions:

import { Animated, Image, View, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {const flipAnimation = useRef( new Animated.Value( 0 ) ).current;let flipRotation = 0;
flipAnimation.addListener( ( { value } ) => flipRotation = value );
const flipToFrontStyle = {
transform: [
{ rotateY: flipAnimation.interpolate( {
inputRange: [ 0, 180 ],
outputRange: [ "0deg", "180deg" ]
} ) }
]
};
const flipToBackStyle = {
transform: [
{ rotateY: flipAnimation.interpolate( {
inputRange: [ 0, 180 ],
outputRange: [ "180deg", "360deg" ]
} ) }
]
};
...return ( ... );}

I think this brain bomb will make more sense after a breakdown:

  • First let’s import Animated and create a new Animated.Value() that starts at 0. Animated.Value()s help us keep track of where we are in an animation. Before hooks, we would have done this by creating something like this.animation— but here we’ll use the hook equivalent, useRef().current. I’ve said before that useRef() is a godsend in desktop React and it’s just as useful here.
  • We’re also keeping track of where we are in an animation using Animated.Value().addListener(), which does… exactly what it says on the box, giving us use of a flipRotation variable so we can tell if a card has been flipped yet. Note that in addListener, the { value } must be deconstructed from the callback’s arguments!
  • We need some additional styles, one each for the front and back, and unfortunately the nesting here is an affront to decency. (You can define more variables but I don’t think it helps much.) Each style is an object with a single key, transform, corresponding to an array of transform objects.
  • Keep in mind, transforms like rotateY need text strings as values — so these rotateY objects use a function called Animated.Value().interpolate(), which translates an Animated.Value() to a string of anywhere from "0deg" to "180deg"to "360deg" (depending on where we happen to be in the animation). Pretty cool, huh?

Now let’s write some functions to trigger the animations…

import { Animated, Image, View, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {const flipAnimation = useRef( new Animated.Value( 0 ) ).current;let flipRotation = 0;
flipAnimation.addListener( ( { value } ) => flipRotation = value );
const flipToFrontStyle = { ... };const flipToBackStyle = { ... };const flipToFront = () => {
Animated.timing( flipAnimation, {
toValue: 180,
duration: 300,
useNativeDriver: true,
} ).start();
};
const flipToBack = () => {
Animated.timing( flipAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true,
} ).start();
};
...return ( ... );}

using Animated.timing().start(). It takes two arguments: our Animated.Value, and a config object with the value we’re animating to and duration in milliseconds. You also need useNativeDriver: true or else you’ll get an angry yellow Animated: `useNativeDriver` was not specified error!

Finally we’re ready to change our Images to Animated.Images, add our animation styles, and change View to Pressable to make each card respond to a finger press by flipping back and forth:

import { Animated, Pressable, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {const flipAnimation = useRef( new Animated.Value( 0 ) ).current;let flipRotation = 0;
flipAnimation.addListener( ( { value } ) => flipRotation = value );
const flipToFrontStyle = { ... };const flipToBackStyle = { ... };const flipToFront = () => { ... };const flipToBack = () => { ... };...return (
<Pressable
style={ style.cardWrapper }
onPress={ () => !!flipRotation ? flipToBack() : flipToFront() }
>
<Animated.Image
style={ { ...style.cardFront, ...flipToBackStyle } }
source={ require( <card front>) }
/>
<Animated.Image
style={ { ...style.cardBack, ...flipToFrontStyle } }
source={ require( <card back> ) }
/>
</Pressable>
);
}

Since we keep track of the card’s flipRotation with Animated.Value().addListener(), we can use it to track if a card’s been flipped or not. Replace the ternary in the callback with an if () {} else {} for multiple lines (to add a dispatch to a Redux state, for example).

Conclusion

The impression I got at first from React Native was that of a scaled-down React, but I realize now there’s plenty of room for creativity with React Native’s toolkit. And it’s easy to take advantage of the power and flexibility of Animated!

It’ll be a little while until next time, whenever that is… until then, take care of yourself, take time for yourself, and always keep learning a little something new whenever you can!

--

--

Josh Frank

Oh geez, Josh Frank decided to go to Flatiron? He must be insane…