by

What we will be building

During this series we’ll be working on the app that has a list of tags and allows adding, removing and reordering tags by dragging and dropping them.

In this part, we’ll focus on setting up the presentational components to render some tags in a list. And we’ll also add the ability to remove tags from the list by tapping on them.

Here’s how the app will look like.

For your reference, the final code for the app we’re building can be found in this GitHub repo.

Creating new project

Let’s get starting by initializing a new project using create-react-native-app.

If you’re not familiar with create-react-native-app check out Building React Native Apps on any Platform without Xcode or Android Studio.

Open Terminal and run these commands to initialize a new project.

create-react-native-app DragAndDropTags;
cd DragAndDropTags;

Run npm install -g create-react-native-app in terminal if you don’t have create-react-native-app installed.

Now there are three options how to run the app during the development process.

  • To run in iOS simulator execute:
npm run ios
  • To run in Android simulator launch your virtual device first and then execute:
npm run android
  • To start the packager and run the app on your mobile device first execute:
npm start
  • Then launch Expo app (iOS, Android) on your mobile device and scan the barcode outputted in the terminal.

Once you’re done, you should see this screen either in the simulator or on your phone.

Installing dependencies

We’re going to need to install one dependency for the project.

Execute the following command in Terminal to install it:

npm install --save react-native-vector-icons

Installing Flow

Flow is a static type checker for JavaScript. We’ll be using it to type hint React component, Redux actions, reducers, etc. Flow improves code readability, finds errors instantly, and allows you to refactor code more confidently, which is especially important as your app grows bigger.

  • Open .flowconfig in project root folder and scroll down to the bottom to find out Flow version. Let’s say it’s 0.49.1.
  • Open Terminal and execute the following putting Flow version after [email protected]:
npm install --save-dev [email protected]
  • Open package.json file and add "flow": "flow" to the bottom of scripts section after test.
  "scripts": {
    "start": "react-native-scripts start",
    "eject": "react-native-scripts eject",
    "android": "react-native-scripts android",
    "ios": "react-native-scripts ios",
    "test": "node node_modules/jest/bin/jest.js --watch",
    "flow": "flow"
  },
  • Open Terminal and run npm run flow. You should see No errors! message.
npm run flow

Starting coding

First of all, let’s create a new folder for all of our source files, so all code is contained in one location for easier navigation around different components we’re about to create.

  • Create a new folder called src within the project root folder. We’ll be creating all new source files and folders within this folder.

App component

Let’s start off by scraping the boilerplate code and setting up a basic component with some text and styles instead.

  • Open App.js file within the project root folder and replace all of the boilerplate code with the following.
// @flow

import React, { PureComponent } from 'react';
import {
  StatusBar,
  StyleSheet,
  Text,
  View,
} from 'react-native';

export default class Main extends PureComponent {

  render() {
    return (
      <View style={styles.container}>
        <StatusBar hidden={true} />

        <View style={styles.header}>
          <Text style={[styles.text, styles.title]}>
            Let's drag and drop some tags!
          </Text>
          <Text style={styles.text}>
            Drag and drop tags to reorder, tap to remove or press Add New to add new tags.
          </Text>
        </View>

      </View>
    );
  }
  
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#2196F3',
  },
  header: {
    marginHorizontal: 20,
    marginVertical: 50,
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  text: {
    color: '#FFFFFF',
    fontFamily: 'Avenir',
    fontSize: 16,
    textAlign: 'center',
  },
});

Tags components basics

Next, let’s start working on Tags component highlighted in the screenshot below. It’s going to be our core component that will handle dragging and dropping, adding and removing tags.

In the first steps, we’re going to build basic versions of each component that can only render tags and do nothing else. And after that we’ll be adding more features step by step.

The structure

Since there’s going to be a lot of drag and drop gesture handling logic and also rendering the list and the tags itself, let’s break it all down into three separate components.

  1. Tags. Handles drag and drop gestures using React Native’s PanResponder and each tag position measured by Tag component.
  2. TagsArea. Renders each tag passed from Tags component using Tag component.
  3. Tag. Renders each tag. This is a simple presentational component, that doesn’t have any logic, except for measuring its position on the screen when re-rendered and passing it up to the parent component TagsArea.

Let’s start building these components backward one by one, starting with Tag, then TagsArea and finally, Tags.

Defining the types

But before we start working on the components let’s create a file with a couple of types that we’re going to use to type hint our components.

  • Create a new file called types.js within src folder.
// Tag object type
export type TagObject = {
  title: string,                // tag title
  tlX?: number,                 // top left x coordinate
  tlY?: number,                 // top left y coordinate
  brX?: number,                 // bottom right x coordinate
  brY?: number,                 // bottom right y coordinate
  isBeingDragged?: boolean,     // whether the tag is currently being dragged or not
};

// PanResponder's gesture state type
export type GestureState = {
  dx: number,                   // accumulated distance of the gesture since the touch started
  dy: number,                   // accumulated distance of the gesture since the touch started
  moveX: number,                // the latest screen coordinates of the recently-moved touch
  moveY: number,                // the latest screen coordinates of the recently-moved touch
  numberActiveTouches: number,  // Number of touches currently on screen
  stateID: number,              // ID of the gestureState- persisted as long as there at least one touch on screen
  vx: number,                   // current velocity of the gesture
  vy: number,                   // current velocity of the gesture
  x0: number,                   // the screen coordinates of the responder grant
  y0: number,                   // the screen coordinates of the responder grant
};

Tag component

Now that we have the types defined let’s create a presentational Tag component for rendering each tag. This is a pretty simple component that receives a tag object that has a title to render, and two callback functions. First one is onPress which is called when a user taps on a tag, and the second one is onRender which is called whenever a tag is rendered. The component passes tag coordinates on the screen, and its width and height to onRender callback function, so the parent component knows where each tag is located on the screen at any given moment.

  • Create a new folder called components within src folder to store all component files within that folder.
  • Create a new file called Tag.js within src/components folder.
// @flow

import React, { PureComponent } from 'react';
import {
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import type { NativeMethodsMixinType } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes';
import type { TagObject } from '../types';

type Props = {
  tag: TagObject,
  // Called when user taps on a tag
  onPress: (tag: TagObject) => void,
  // Called after a tag is rendered
  onRender: (tag: TagObject, screenX: number, screenY: number, width: number, height: number) => void,
};

export default class Tag extends PureComponent {

  props: Props;

  container: ?NativeMethodsMixinType;

  // Append styles.tagBeingDragged style if tag is being dragged
  getTagStyle = (): {} => ({
    ...styles.tag,
    ...(this.props.tag.isBeingDragged ? styles.tagBeingDragged : {}),
  });

  // Call view container's measure function to measure tag position on the screen
  onLayout = (): void => {
    this.container && this.container.measure(this.onMeasure);
  };

  // Pass tag coordinates up to the parent component
  onMeasure = (x: number,
               y: number,
               width: number,
               height: number,
               screenX: number,
               screenY: number): void => {
    this.props.onRender(this.props.tag, screenX, screenY, width, height);
  };

  // Handle tag taps
  onPress = (): void => {
    this.props.onPress(this.props.tag);
  };

  render() {
    const { tag: { title } } = this.props;
    return (
      <View
        ref={el => this.container = el}
        style={styles.container}
        onLayout={this.onLayout}
      >
        <TouchableOpacity
          style={this.getTagStyle()}
          onPress={this.onPress}
        >
          <Icon name="ios-close-circle-outline" size={16} color="#FFF" />
          <Text>{' '}</Text>
          <Text style={styles.title}>{title}</Text>
        </TouchableOpacity>
      </View>
    );
  }

}

const styles = {
  container: {
    marginBottom: 8,
    marginRight: 6,
  },
  tag: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'rgba(255, 255, 255, .33)',
    borderColor: 'rgba(255, 255, 255, .25)',
    borderRadius: 20,
    borderWidth: 1,
    paddingHorizontal: 10,
    paddingVertical: 3,
  },
  tagBeingDragged: {
    backgroundColor: 'rgba(255, 255, 255, .01)',
    borderStyle: 'dashed',
  },
  title: {
    color: '#FFFFFF',
    fontFamily: 'Avenir',
    fontSize: 15,
    fontWeight: 'normal',
  },
};

TagsArea component

Next, let’s create TagsArea component which is mostly just a layer between Tags and Tag components. It receives tags array and a few callback functions and loops through tags array to render each tag using Tag component that we created in the previous step. And it also adds Add new button after all of the tags.

  • Create a new file called TagsArea.js within src/components folder.
// @flow

import React, { PureComponent } from 'react';
import {
  Text,
  View
} from 'react-native';
import Tag from './Tag';
import type { TagObject } from '../types';

type Props = {
  tags: TagObject[],
  // Called when user taps 'Add new' button
  onPressAddNew: () => void,
  // Passes these two callbacks down to Tag component
  onPress: (tag: TagObject) => void,
  onRenderTag: (tag: TagObject, screenX: number, screenY: number, width: number, height: number) => void,
};

export default class TagsArea extends PureComponent {

  props: Props;

  render() {
    const {
      tags,
      onPress,
      onPressAddNew,
      onRenderTag,
    } = this.props;

    return (
      <View style={styles.container}>

        {tags.map(tag =>
          <Tag
            key={tag.title}
            tag={tag}
            onPress={onPress}
            onRender={onRenderTag}
          />
        )}

        <Text
          style={styles.add}
          onPress={onPressAddNew}
        >
          Add new
        </Text>

      </View
      >
    );
  }

}

const styles = {
  container: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    borderColor: 'rgba(255,255,255,0.5)',
    borderRadius: 5,
    borderWidth: 2,
    paddingBottom: 10,
    paddingHorizontal: 15,
    paddingTop: 15,
  },
  add: {
    backgroundColor: 'transparent',
    color: '#FFFFFF',
    paddingHorizontal: 5,
    paddingVertical: 5,
    textDecorationLine: 'underline',
  },
};

Tags component

Now, let’s create a basic version of Tags component that receives an array of tags which looks like ['tag', 'another-one', 'etc.'] via tags props, converts it to an array of objects to be able to store additional tag props in the state, and passes it down to TagsArea to render those.

  • Create a new file called Tags.js within src/components folder.
// @flow

import React, { PureComponent } from 'react';
import {
  StyleSheet,
  View
} from 'react-native';
import TagsArea from './TagsArea';
import type { TagObject } from '../types';

type Props = {
  // Array of tag titles
  tags: string[],
  // Passes onPressAddNewTag callback down to TagsArea component
  onPressAddNewTag: () => void,
};

type State = {
  tags: TagObject[],
};

export default class Tags extends PureComponent {

  props: Props;

  state: State = {
    // Convert passed array of tag titles to array of objects of TagObject type,
    // so ['tag', 'another'] becomes [{ title: 'tag' }, { title: 'another' }]
    tags: [...new Set(this.props.tags)]       // remove duplicates
      .map((title: string) => ({ title })),   // convert to objects
  };

  render() {
    const { tags } = this.state;
    return (
      <View
        style={styles.container}
      >

        <TagsArea
          tags={tags}
          onPress={() => {}} // do nothing for now
          onRenderTag={() => {}} // do nothing for now
          onPressAddNew={this.props.onPressAddNewTag}
        />

      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingHorizontal: 15,
  },
});

Updating App.js

It’s time to see those components we just created in action. Let’s update App.js to define some tags and render them using Tags component.

  • Open App.js file within the project root folder and add a few lines of code. The lines you need to add are highlighted.
import React, { PureComponent } from 'react';
import {
  StatusBar,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import Tags from './src/components/Tags';

const TAGS = [
  '#love',
  '#instagood',
  '#photooftheday',
  '#beautiful',
  '#fashion',
  '#happy',
  '#tbt',
  '#cute',
  '#followme',
  '#like4like',
  '#follow',
  '#followme',
  '#picoftheday',
  '#me',
  '#selfie',
  '#summer',
  '#instadaily',
  '#photooftheday',
  '#friends',
  '#girl',
  '#fun',
  '#style',
  '#instalike',
  '#food',
  '#family',
  '#tagsforlikes',
  '#igers',
];

export default class Main extends PureComponent {

  render() {
    return (
      <View style={styles.container}>
        <StatusBar hidden={true} />

        <View style={styles.header}>
          <Text style={[styles.text, styles.title]}>
            Let's drag and drop some tags!
          </Text>
          <Text style={styles.text}>
            Drag and drop tags to reorder, tap to remove or press Add New to add new tags.
          </Text>
        </View>

        <Tags
          tags={TAGS}
          onPressAddNewTag={() => {}} // do nothing for now
        />

      </View>
    );
  }

}

And there you go. Now we have a nice list with a bunch of tags. BTW those aren’t just some random tags. Those are the most popular tags on one of the social media platforms. I’m sure you’ve guessed what that platform is by looking at the tags.

Now that we’re done with the basic version let’s continue adding features and add an ability to remove tags from the list.

Removing tags

Let’s update Tags component, so when you tap on a tag, it gets removed from the list.

  • Open Tags.js file within src/components folder to make a couple of changes.

Step 1. Setting up animation

First of all, let’s set up animation so when a tag gets removed from the list the rest of them that follow the removed tag is animated nicely when moving to take the place of the removed tag. That’s when LayoutAnimation comes in handy.

Here’s what we’re going to need to do to achieve that.

  • Import LayoutAnimation and configure it in componentWillUpdate to animate any layout changes.
  • Add new prop animationDuration for setting animation duration with the default value of 250.
import React, { PureComponent } from 'react';
import {
  LayoutAnimation,
  StyleSheet,
  View
} from 'react-native';
import TagsArea from './TagsArea';
import type { TagObject } from '../types';

type Props = {
  // Array of tag titles
  tags: string[],
  // Tag swapping animation duration in ms
  animationDuration: number,
  // Passes onPressAddNewTag callback down to TagsArea component
  onPressAddNewTag: () => void,
};

type State = {
  tags: TagObject[],
};

export default class Tags extends PureComponent {

  props: Props;

  static defaultProps = {
    animationDuration: 250
  };

  state: State = {
    // Convert passed array of tag titles to array of objects of TagObject type,
    // so ['tag', 'another'] becomes [{ title: 'tag' }, { title: 'another' }]
    tags: [...new Set(this.props.tags)]       // remove duplicates
      .map((title: string) => ({ title })),   // convert to objects
  };

  // Animate layout changes when dragging or removing a tag
  componentWillUpdate() {
    LayoutAnimation.configureNext({
      ...LayoutAnimation.Presets.easeInEaseOut,
      duration: this.props.animationDuration
    });
  }
 
  render() {...}

In order to get this to work on Android you need to set the following flags via UIManager: UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);

Step 2. Removing tags from the state

And finally, let’s add a function that removes tags from the state and pass that function to TagsArea component, so it could pass it down to Tag component which will call it whenever you tap on a tag.

  • Add removeTag function which finds a given tag and removes it from the state.
  componentWillUpdate() {...}

  // Remove tag
  removeTag = (tag: TagObject): void => {
    this.setState((state: State) => {
      const index = state.tags.findIndex(({ title }) => title === tag.title);
      return {
        tags: [
          // Remove the tag
          ...state.tags.slice(0, index),
          ...state.tags.slice(index + 1),
        ]
      }
    });
  };

  render() {...}
  • Pass this.removeTag function we just added to onPress prop of TagsArea component instead of a blank placeholder function that we currently have.
  render() {
    const { tags } = this.state;
    return (
      <View
        style={styles.container}
      >

        <TagsArea
          tags={tags}
          onPress={this.removeTag}
          onRenderTag={() => {}} // do nothing for now
          onPressAddNew={this.props.onPressAddNewTag}
        />

      </View>
    );
  }

Now, try tapping on tags to remove them and see how nicely they disappear thank to LayoutAnimation. That was easy to set up, wasn’t it?

And another great thing about LayoutAnimation is that it’s going to animate tag movements when we add tag reordering feature without any additional code. That’s the beauty of the React Native way.

Stay tuned for the next part

You’ve done a lot so far! We already have an app that renders tags in a beautiful list and allows removing them.

In the next and final part, we’ll implement reordering tags using drag and drop gesture and add a modal with a text input for adding new tags.

Subscribe below to get an email when the next part comes out!

Recommended Reading

Spread the Word
  • deadcoder0904

    Your tutorials are the best on React Native on the internet. Thanks 👍

    • Vincent Valentine

      That’s all I wanna say!