/ react native

Building a Card Swiper in React Native

This past month, a friend and I have been working on releasing React Screens. We plan on making two screens, complete with various UI elements that you can use some of, or the whole screen, in your own React Native apps.

Along with this, I plan on writing a tutorial about how we make some of the screens that we release. This means you can follow along and get almost half of the screens we release for free. If you like what we are doing and want to support us, preorder the first set of screens so that we can keep doing this each month!

If you are only interested in seeing it in action, you can check out the GitHub repo. There's a QR code you can scan with expo to demo it.

Getting started

There's a few really important requirements for this deck swiper (and all of the components we release).

  • It must support any React component as a deck item
  • Completely customizable, no component api lock-in

These two problems are evident across a huge amount of open source components out there. For instance, take a look at this one. You have to learn more props and methods than React itself has! Okay, that may be a stretch, but it's true. There's way too many things this component exposes as props.

Keep it simple

This is the least complex way we came up with:

<CardContainer
  onTossLeft={card => console.log(card, 'tossed left')}
  onTossRight={card => console.log(card, 'tossed right')}
  actionsBar={toss => <Actions toss={toss} />}
/>

There's only three things to understand!

  • onTossLeft/Right are called when you toss the card. The value given to you is whatever props you give to your card component.
  • actionsBar is a component that can trigger a toss left or right, ex: toss('left'). This lets you have buttons in the bottom, or wherever you want.
  • Children are your individual cards! We offer a simple one, but it's way too easy to make your own as you will see shortly.

That's about it! Because the tossing logic is abstracted for you, we don't care what the actual cards are. They can be anything.

If you would like to see how easy it is as an end user, skip to Using the Swiper.

CardContainer

Okay, let's go over how this CardContainer is made. Before we get into the code, I want to mention that there's one small performance thing we haven't optimized yet. We are rendering all of the children in a stack, this could be better optimized like a list view in React Native. This way not all of them would be rendered at once.

Due to the amount of code I'll be explaining, I will post majority of the code, with a link to github for the full file.

constructor({ children }) {
  super();
  this.state = { stack: children };
}

onToss(callback) {
  let stack = [...this.state.stack];
  this.setState(
    {
      stack: stack.filter((item, index) => index !== stack.length - 1),
      toss: false,
    },
    callback
  );
}

componentWillReceiveProps({ children }) {
  if (children !== this.props.children) this.setState({ stack: children });
}

render() {
  let { stack, toss } = this.state;
  let { onTossLeft, onTossRight, actionsBar } = this.props;

  return (
    <View style={{ flex: 1 }}>
      {React.Children.map(stack, (child, index) => (
        <Animator
          toss={index === stack.length - 1 ? toss : false}
          onTossRight={() => this.onToss(() => onTossRight(child.props))}
          onTossLeft={() => this.onToss(() => onTossLeft(child.props))}
        >
          {child}
        </Animator>
      ))}
      {actionsBar(toss => this.setState({ toss }))}
    </View>
  );
}

View on GitHub

The first thing we are doing is bringing props into state. This is generally an anti-pattern in React. I feel that this is a good solution for what this component has to do. If anyone can think of a better way to accomplish anything we are doing, I'd love the feedback in the comments or on Twitter. Just a reminder that we are always learning and I'd never consider myself an expert :)

onToss

This function is called from our Animator any time a toss actually occurs. The Animator wraps your card components to make them tossable. More on that later. When this function is called, it removes the card at the end of the card stack. Because the top-most card is the only one that can be tossed, we know to simply remove from the end of the array on any toss.

componentWillReceiveProps

If you decide to pass in a new set of cards, we will update the card container with the new stack.

render

Now this part may seem a little complicated, and I agree that it is. Figuring out how to link up the actionsBar with the current card stack was hard to keep simple, but I think this logic accomplishes that. When you toss from the actionsBar, it updates the state to toss, and passes in the value to the top-most card on the screen. The animated container will check for that prop, and animate a toss, and call the onTossX prop on the card container! Let's get into the Animator now!

Animator

The CardContainer maps through the children and puts an Animator around it so that it can be tossed. Here's what it looks like:

componentWillMount() {
 let { onTossRight, onTossLeft } = this.props;
 this.pan = new Animated.ValueXY();

 this.cardPanResponder = PanResponder.create({
   onStartShouldSetPanResponder: () => true,
   onPanResponderMove: Animated.event([
     null,
     { dx: this.pan.x, dy: this.pan.y },
   ]),
   onPanResponderRelease: (e, { dx }) => {
     const absDx = Math.abs(dx);
     const direction = absDx / dx;

     if (absDx > 120) {
       Animated.decay(this.pan, {
         velocity: { x: 3 * direction, y: 0 },
         deceleration: 0.995,
       }).start(dx > 0 ? onTossRight : onTossLeft);
     } else {
       Animated.spring(this.pan, {
         toValue: { x: 0, y: 0 },
         friction: 4.5,
       }).start();
     }
   },
 });
}

componentWillReceiveProps({ toss, onTossRight, onTossLeft }) {
 if (toss && !this.props.toss) {
   if (toss === 'left') {
     return Animated.timing(this.pan, {
       toValue: { x: 3 * (-180), y: 0 },
       duration: 400,
     }).start(onTossLeft);
   }

   return Animated.timing(this.pan, {
     toValue: { x: 3 * 180, y: 0 },
     duration: 400,
   }).start(onTossRight);
 }
}

render() {
 let { children, style } = this.props;
 const rotateCard = this.pan.x.interpolate({
   inputRange: [-200, 0, 200],
   outputRange: ['10deg', '0deg', '-10deg'],
 });
 const animatedStyle = {
   transform: [
     { translateX: this.pan.x },
     { translateY: this.pan.y },
     { rotate: rotateCard },
   ],
 };

 return (
   <Animated.View
     {...this.cardPanResponder.panHandlers}
     style={[styles.card, animatedStyle, style]}
   >
     {children}
   </Animated.View>
 );
}

View on GitHub

I will admit, I do not have much experience with Animated in React Native, this project has helped me understand it a lot better. My friend Dominic did the majority of this component. Let's go over each method like we did earlier!

componentWillMount

We register the Animated stuff here. If you toss the card, we call the correct methods. You can read up on Animated for a better idea as to how all of this works.

componentWillReceiveProps

Remember earlier when we passed in toss to our Animator if the actions bar was pressed? This is where that is handled. It throws the card away in the correct direction based on the actions bar!

Using the Swiper

Now that we covered the things that make up this deck swiper, let's go over how nice and simple the end-user api is! This first one is the Card component. Here's how our example card looks:

Card

export default ({ image, title, subTitle }) => (
  <View style={styles.container}>
    <Image style={styles.image} source={{ uri: image }}>
      <View style={styles.textContainer}>
        <Text style={styles.name}>{title}</Text>
        <Text style={styles.subTitle}>{subTitle}</Text>
      </View>
    </Image>
  </View>
);

As you can see, it's just a stateless component! It can look however you want, and the Animator will handle the rest. This means that we don't need new props on the actual swiper to handle modifying the design. The hard parts are completely abstracted. You can make your own card. It's really that simple.

ActionsBar

Our example actions bar looks like this:

export default ({ toss }) => {
  return (
    <View style={styles.actionBar}>
      <View style={{ bottom: 25 }}>
        <TouchableOpacity
          style={styles.buttonDislikeC}
          onPress={() => toss('left')}
        >
          <Text style={styles.buttonTextDislike}>Dislike</Text>
        </TouchableOpacity>
      </View>

      <View style={{ bottom: 25 }}>
        <TouchableOpacity
          style={styles.buttonLikeC}
          onPress={() => toss('right')}
        >
          <Text style={styles.buttonTextLike}>Like</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

Another stateless component that even a person brand new to React can understand. Call toss from wherever you would like, style it however you like! The styling here will be cleaned up for launch and wasn't the main focus at the time.

All together now

When you use our deck swiper, it will look like this:


<CardContainer
  onTossLeft={card => console.log(card, 'tossed left')}
  onTossRight={card => console.log(card, 'tossed right')}
  actionsBar={toss => <Actions toss={toss} />}
>
  <Card
   image="http://blah.com/avatar.jpg"
   title="Bob, 20"
   subTitle="Engineer"
 />
</CardContainer>

I can honestly say that the end user api is as simple and customizable as it gets. I'm happy with how this came out and would love to hear feedback from everyone. The only things I see needing optimizations other than the rendering performance is how the actions bar is hooked into the card stack. Even with the couple known issues, I feel that it's very solid in its current state.

Please think about subscribing to React Screens for all of the high quality views we will be creating. Not all of them will be open sourced of course!

Let me know below or on Twitter (@zachcodes) what you thought of this deck swiper. Without @Dom_Gozza I wouldn't have thought to help create this screen. I had no idea a good solution didn't already exist.

It's my hope with the community's help we can make this the best card swiper for React Native.