Andrew Vo-Nguyen
November 16, 2021
10 min read
1. Initialise a project either using the React Native CLI or Expo.
2. Install the following packages:
3. Sign up to a free account for https://randommer.io/ to generate random names. (Alternatively, you can hard code an array of names for this exercise).
4. Create a .env file with the randommer API key:
1API_KEY=xxxxxxxxxxxxxxxxxx
If you want to use the hardcoded list of names instead, paste this into your code:
1const uniqueNames = [
2 "Zekiel",
3 "Vance",
4 "Buddy",
5 "Tyrell",
6 "Mohammed",
7 "Tehya",
8 "Wisdom",
9 "Ash",
10 "Mikel",
11 "Yazmine",
12 "Brystol",
13 "Merry",
14 "Tennyson",
15 "Jaxyn",
16 "Drea",
17 "Jase",
18 "Izabel",
19 "Christen",
20 "Terron",
21 "Shaylynn",
22 "Nicolle",
23 "Aldo",
24 "Dmitri",
25 "Sammie",
26 "Baltazar",
27 "Taevion",
28 "Jayvin",
29 "Aidyn",
30 "Aron",
31 "Maryann",
32 "Kathia",
33 "Raelynn",
34 "Eleazar",
35 "Atalia",
36 "Arda",
37 "Talitha",
38 "Ailin",
39 "Sione",
40 "Race",
41 "Reef",
42 "Giuseppe",
43 "Kelsy",
44 "Beck",
45 "Evalynn",
46 "Berkley",
47 "Adlee",
48 "Jeison",
49 "Asiyah",
50 "Samanvi",
51 "Elvis",
52 "Malayia",
53 "Mace",
54 "Novaleigh",
55 "Shneur",
56 "Damian",
57 "Kamdyn",
58 "Zayvion",
59 "Yahaira",
60 "Laiyah",
61 "Nitya",
62 "Anna",
63 "Skyleigh",
64 "Tremayne",
65 "Rocio",
66 "Vivianne",
67 "Bowen",
68 "Nathanial",
69 "Jeanne",
70 "Younis",
71 "Gibran",
72 "Emme",
73 "Kylo",
74 "Sergio",
75 "Manar",
76 "Dino",
77 "Devonta",
78 "Alexavier",
79 "Aric",
80 "Eleyna",
81 "Cristal",
82 "Maxim",
83 "Saanvi",
84 "Ayomide",
85 "Haniya",
86 "Alaa",
87 "Avyukth",
88 "Jane",
89 "Graciella",
90 "Livie",
91 "Alijah",
92 "Thalia",
93 "Alanis",
94 "Devon",
95 "Madelin",
96 "Nadir",
97 "Jermiah",
98 "Alesia",
99 "Elienai",
100 "Landon",
101];
For simplicity sake, we will be creating everything on a single page (App.tsx
).
1import {
2 Animated,
3 Dimensions,
4 PanResponder,
5 StyleSheet,
6 Text,
7 TextStyle,
8 View,
9 ViewStyle,
10} from 'react-native';
11import { Fragment, useEffect, useRef, useState } from 'react';
12
13import Config from 'react-native-config';
14import React from 'react';
15import { SvgUri } from 'react-native-svg';
16import axios from 'axios';
Here we declare the shape of our Person type which we will use to render our cards.
1interface Person {
2 name: string;
3 age: number;
4 photo: string;
5}
6
7interface Styles {
8 screenContainer: ViewStyle;
9 counterContainer: ViewStyle;
10 counterText: TextStyle;
11 imageContainer: ViewStyle;
12 nameLabel: TextStyle;
13 ageLabel: TextStyle;
14}
15
16interface CardStyles {
17 card: ViewStyle;
18 leftTextContainer: ViewStyle;
19 leftText: TextStyle;
20 rightTextContainer: ViewStyle;
21 rightText: TextStyle;
22}
Declare an api object for our asynchronous calls to generate names and avatar pictures.
1const api = {
2 names(quantity: number) {
3 return `https://randommer.io/api/Name?nameType=firstname&quantity=${quantity}`;
4 },
5 avatar(name: string) {
6 return `https://avatars.dicebear.com/api/micah/:${name}.svg`;
7 },
8};
Declare local state using useState
hook:
1function App(): JSX.Element {
2 const [personList, setPersonList] = useState<Person[]>([]);
3 const [liked, setLiked] = useState<number>(0);
4 const [disLiked, setDisLiked] = useState<number>(0);
5 const [currentIndex, setCurrentIndex] = useState<number>(0);
6}
7
8export default App;
Here we are maintaining the position of the card by using the Animated
API. The PanResponder
here is created to respond to the gestures of the cards.
onPanResponderMove
keeps track of the current position and onPanResponderMove
will perform an action upon swiping left or right. 120
is the threshold of distance required to perform the action. setCurrentIndex
will update the list of cards of which card should be on top.
We store this data using the useRef
hook instead of local state to prevent re-renders of the JSX elements every time a card moves position.
1const SCREEN_HEIGHT = Dimensions.get('window').height;
2const SCREEN_WIDTH = Dimensions.get('window').width;
3
4const position = useRef(new Animated.ValueXY());
5
6const panResponder = useRef(
7 PanResponder.create({
8 onStartShouldSetPanResponder: () => true,
9 onPanResponderMove: (_e, gestureState) => {
10 position.current.setValue({ x: gestureState.dx, y: gestureState.dy });
11 },
12 onPanResponderRelease: (_e, gestureState) => {
13 if (gestureState.dx > 120) {
14 Animated.spring(position.current, {
15 useNativeDriver: true,
16 toValue: { x: SCREEN_WIDTH + 100, y: gestureState.dy },
17 }).start(() => {
18 setCurrentIndex((prev) => prev + 1);
19 setLiked((prev) => prev + 1);
20 position.current.setValue({ x: 0, y: 0 });
21 });
22 } else if (gestureState.dx < -120) {
23 Animated.spring(position.current, {
24 useNativeDriver: true,
25 toValue: { x: -SCREEN_WIDTH - 100, y: gestureState.dy },
26 }).start(() => {
27 setCurrentIndex((prev) => prev + 1);
28 setDisLiked((prev) => prev + 1);
29 position.current.setValue({ x: 0, y: 0 });
30 });
31 } else {
32 Animated.spring(position.current, {
33 useNativeDriver: true,
34 toValue: { x: 0, y: 0 },
35 friction: 4,
36 }).start();
37 }
38 },
39 }),
40);
Here we will use the useEffect
hook to fetch out data from our API.
1const apiKey = Config.API_KEY;
2
3function generateAge(min: number, max: number) {
4 return Math.floor(Math.random() * (max - min) + min);
5}
6
7useEffect(() => {
8 async function fetchNames() {
9 const response = await axios.get<string[]>(api.names(100), {
10 headers: { 'X-API-Key': apiKey },
11 });
12 if (response.status !== 200) {
13 return;
14 }
15 const uniqueNames = [...new Set(response.data)];
16 const newPersonList = uniqueNames.map<Person>((name) => ({
17 name,
18 age: generateAge(18, 36),
19 photo: api.avatar(name),
20 }));
21 setPersonList(newPersonList);
22 }
23 fetchNames();
24 }, []);
We declare 2 sets of stylesheets. A styles
object and a cardStyles
object. The cardStyles
object will be dynamic and will change based whether the card is the top card and its current gesture position.
By default the array of cards would render one card after another vertically on the screen. We need to make each card's position absolute
****in order to emulate the affect of having cards stacked on top of each other.
Unfortunately animated styles do not play nice with React Native's standard style types, so we'll just have to pop in some ts-ignore
to tell TypeScript we know what we're doing.
1const styles: Styles = StyleSheet.create({
2 screenContainer: {
3 flex: 1,
4 padding: 8,
5 backgroundColor: '#FEFBF3',
6 flexDirection: 'column',
7 justifyContent: 'flex-end',
8 alignItems: 'stretch',
9 },
10 counterContainer: {
11 flexDirection: 'row',
12 justifyContent: 'space-between',
13 padding: 32,
14 },
15 counterText: {
16 fontSize: 32,
17 },
18 imageContainer: {
19 borderRadius: 150,
20 overflow: 'hidden',
21 backgroundColor: '#FED2AA',
22 },
23 nameLabel: {
24 fontSize: 48,
25 fontWeight: 'bold',
26 padding: 16,
27 },
28 ageLabel: {
29 fontSize: 36,
30 padding: 16,
31 },
32});
33
34function getCardStyles(
35 position: Animated.ValueXY,
36 topCard: boolean,
37): CardStyles {
38 let card: ViewStyle = {
39 height: SCREEN_HEIGHT - 192,
40 width: SCREEN_WIDTH - 32,
41 left: 16,
42 top: 64,
43 padding: 12,
44 position: 'absolute',
45 borderWidth: 1,
46 borderColor: '#EBE6E6',
47 borderRadius: 8,
48 backgroundColor: '#E8F6EF',
49 alignItems: 'center',
50 };
51 if (topCard) {
52 card = {
53 ...card,
54 transform: [
55 {
56 // @ts-ignore
57 rotate: position.x.interpolate({
58 inputRange: [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2],
59 outputRange: ['-10deg', '0deg', '10deg'],
60 extrapolate: 'clamp',
61 }),
62 },
63 // @ts-ignore
64 ...position.getTranslateTransform(),
65 ],
66 };
67 } else {
68 card = {
69 ...card,
70 transform: [
71 {
72 // @ts-ignore
73 scale: position.x.interpolate({
74 inputRange: [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2],
75 outputRange: [1, 0.8, 1],
76 extrapolate: 'clamp',
77 }),
78 },
79 ],
80 };
81 }
82
83 const leftTextContainer: ViewStyle = {
84 // @ts-ignore
85 opacity: position.x.interpolate({
86 inputRange: [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2],
87 outputRange: [0, 0, 1],
88 extrapolate: 'clamp',
89 }),
90 transform: [{ rotate: '-40deg' }],
91 position: 'absolute',
92 top: 60,
93 left: 40,
94 zIndex: 2,
95 };
96 const rightTextContainer: ViewStyle = {
97 // @ts-ignore
98 opacity: position.x.interpolate({
99 inputRange: [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2],
100 outputRange: [1, 0, 0],
101 extrapolate: 'clamp',
102 }),
103 transform: [{ rotate: '40deg' }],
104 position: 'absolute',
105 top: 60,
106 right: 40,
107 zIndex: 2,
108 };
109 const text: TextStyle = {
110 borderWidth: 1,
111 borderRadius: 8,
112 borderColor: '#FF5151',
113 color: '#FF5151',
114 fontSize: 32,
115 fontWeight: '800',
116 padding: 8,
117 lineHeight: 0,
118 };
119
120 return StyleSheet.create({
121 screenContainer: {
122 flex: 1,
123 padding: 8,
124 },
125 card,
126 leftTextContainer,
127 leftText: {
128 ...text,
129 borderColor: '#4E9F3D',
130 color: '#4E9F3D',
131 },
132 rightTextContainer,
133 rightText: {
134 ...text,
135 borderColor: '#FF5151',
136 color: '#FF5151',
137 },
138 });
139}
Here, we finally put it all together by rendering the array of personList
as animated cards. We determine the current card by comparing the currentIndex
stored in state, and the index of the card in the array. We will pass down whether this card is the top card down the to the getCardStyles
function get its dynamic styles.
If the index is less than the currentIndex
, it means the card as been swiped and dismissed, so we will return null
as a result.
The counter container at the bottom will maintain how many Liked and Disliked swipes have occurred which gets updated in the PanResponder
created previously.
Lastly, to make sure that the first element in the array is showing at the top of the card deck. It is sitting at the bottom of the deck because it was the first element to get rendered. We use the .reverse()
array method on the cards to bring the first element visually to the top.
1<View style={styles.screenContainer}>
2 {personList
3 .map((person, index) => {
4 const currentCard = index === currentIndex;
5 const propsSpread = currentCard
6 ? panResponder.current.panHandlers
7 : undefined;
8 const cardStyles = getCardStyles(position.current, currentCard);
9
10 if (index < currentIndex) {
11 return null;
12 }
13 return (
14 <Animated.View
15 {...propsSpread}
16 key={person.name}
17 style={cardStyles.card}>
18 {currentCard && (
19 <Fragment>
20 <Animated.View style={cardStyles.leftTextContainer}>
21 <Text style={cardStyles.leftText}>LIKE</Text>
22 </Animated.View>
23 <Animated.View style={cardStyles.rightTextContainer}>
24 <Text style={cardStyles.rightText}>DISLIKE</Text>
25 </Animated.View>
26 </Fragment>
27 )}
28 <View style={styles.imageContainer}>
29 <SvgUri width={300} height={300} uri={person.photo} />
30 </View>
31 <Text style={styles.nameLabel}>{person.name}</Text>
32 <Text style={styles.ageLabel}>Age: {person.age}</Text>
33 </Animated.View>
34 );
35 })
36 .reverse()}
37 <View style={styles.counterContainer}>
38 <View>
39 <Text style={styles.counterText}>Liked: {liked}</Text>
40 </View>
41 <View>
42 <Text style={styles.counterText}>Disliked: {disLiked}</Text>
43 </View>
44 </View>
45 </View>
There we have it, a super simple component to handle Tinder like swipe cards.
The source code for this project can be found on here: GitHub ✌️.