Dynamically Add / Remove Component with Animation Tutorial in React native

Hello guys, After a bit long period I come back with very interesting tutorial. If you guys remember, Some time ago, I have posted a tutorial “React Native dynamically Add View Component on Button Click with Animation Tutorial“. As the name indicates, in this tutorial I demonstrated, how we can add a View component on button click with nice animation. Many of my visitors asked about how we can add delete item functionality with nice smooth animation. So then I decided to write a complete new blog and through this tutorial, I am going to explain how we can Dynamically Add Remove Component with Animation, describing all functionalities(Add / Remove) and ya thanks for reading my blogs.

Note: In this tutorial, I am using Add More icon image. You can download this Add More icon image and create a folder named assets/images under your react native project directory to access this icon in your app.

In brief, what I am doing in Dynamically Add / Remove component with Animation tutorial?

  • We have a floating button, which allows you to add a View  component dynamically with nice smooth animation.
  • And each item have a Remove button which allows you to remove the selected item with animation. You can see the final output in the below GIF image.

dynamically-add-remove-element-using-animation

So, let’s get started…

Step – 1: Import all required components from react , react-native  packages and get the device width through react native Dimensions  API.

import React, { Component } from 'react';
import {
  View,
  Text,
  Platform,
  StyleSheet,
  TouchableOpacity,
  Animated,
  ScrollView,
  Image,
  Dimensions,
  LayoutAnimation,
  UIManager
} from 'react-native';

const width = Dimensions.get('window').width;

Implement Code for Item (Child) Component:

Step – 2: Implement constructor method.

constructor() {
  super();
  this.animatedValue = new Animated.Value(0);

  if( Platform.OS === 'android' ) {
    UIManager.setLayoutAnimationEnabledExperimental(true);
  }
}

Explanation:

  • I am using react native LayoutAnimation  API to animate the remaining ScrollView  items after removing the selected item and LayoutAnimation  is not working by default on Android. So, we have to enable it for Android devices. That’s why I am using UIManager.setLayoutAnimationEnabledExperimental(true); statement.

Step – 3: Implement shouldComponentUpdate method.

shouldComponentUpdate(nextProps, nextState) {
  if(nextProps.item.id !== this.props.item.id) {
    return true;
  }
  return false;
}

Explanation:

  • Basically, shouldComponentUpdate  is a lifecycle method of react and used to improve the performance of the app.
  • shouldComponentUpdate  method returns a boolean  either true  or false . If it returns true then component re-renders otherwise react skips the re-rendering process.
  • In our case, every time when we add a new component then rendering occurs for all existing items. Suppose, we have 9 items in ScrollView  then after adding 10th item, Item  component re-renders for all existing 9 items and this is wasteful re-rendering and harms our app’s performance. So, to prevent the wasteful re-rendering shouldComponentUpdate  method is used.

Step – 4: Implement componentDidMount method.

componentDidMount() {
  Animated.timing(
    this.animatedValue,
    {
      toValue: 0.5,
      duration: 500,
      useNativeDriver: true
    }
  ).start(() => {
    this.props.afterAnimationComplete();
  });
}

Explanation:

  • This is the place where our animation will work for adding a component and after completing the animation I am calling afterAnimationComplete  method which is received as prop from parent component.

Step – 5: Implement removeItem method.

removeItem = () => {
  Animated.timing(
    this.animatedValue,
    {
      toValue: 1,
      duration: 500,
      useNativeDriver: true
    }
  ).start(() => {
    this.props.removeItem(this.props.item.id);
  });
}

Explanation:

  • This method is responsible to execute the removing animation and again I am calling removeItem  method to actually remove the selected item from ScrollView  and this method is also received as prop from parent component.

Step – 6: Implement render method.

render() {
  const translateAnimation = this.animatedValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [-width, 0, width]
  });

  const opacityAnimation = this.animatedValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [0, 1, 0]
  });

  return (
    <Animated.View style={[
      styles.viewHolder, {
        transform: [{ translateX: translateAnimation}],
        opacity: opacityAnimation
      }]}
    >
      <Text
        style={styles.text}>
          Row {this.props.item.text}
      </Text>
      <TouchableOpacity
        style={styles.removeBtn}
        onPress={this.removeItem}
      >
        <Image
          source={require('./assets/images/add.png')}
          style={styles.removeIcon}
        />
      </TouchableOpacity>
    </Animated.View>
  );
}

Explanation:

  • translateAnimation  is responsible for moving the component with animation with opacity by using opacityAnimation .
  • First, when we add a new component then element’s translateX  position is equal to the -width  and component animates from -width  to 0  as you can see in the translateAnimation ‘s outputRange  and when user clicks on the remove icon on any component then component animates from 0  to width  (as you can see in the translateAnimation ‘s outputRange) and opacityAnimation  also works along with translateAnimation .

Implement Code for App (Parent) Component:

Step – 7: Implement constructor method.

constructor() {
  super();
  this.state = { valueArray: [], disabled: false }
  this.addNewEle = false;
  this.index = 0;
}

Step – 8: Implement afterAnimationComplete method.

afterAnimationComplete = () => {
  this.index += 1;
  this.setState({ disabled: false });
}

Explanation:

  • Remember, this method is called by Item  component after completing the adding component animation.
  • In this method, I am going to set false the disabled state, which re-enables the add component (+) button and allows to add new component.

Step – 9: Implement addMore method.

addMore = () => {
  this.addNewEle = true;
  const newlyAddedValue = { id: "id_" + this.index, text: this.index + 1 };

  this.setState({
    disabled: true,
    valueArray: [...this.state.valueArray, newlyAddedValue]
  });
}

Explanation:

  • This method is responsible to add a new object ( will define new component ) into array and disable the add button while running adding component animation.

Step – 10: Implement remove method.

remove(id) {
  this.addNewEle = false;
  const newArray = [...this.state.valueArray];
  newArray.splice(newArray.findIndex(ele => ele.id === id), 1);

  this.setState(() => {
    return {
      valueArray: newArray
    }
  }, () => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
  });
}

Explanation:

  • In this method, I am removing the selected item from array and applying the LayoutAnimation  to re-arrange the remaining components with nice animation.

Step – 11: Implement render method.

render() {
  return (
    <View style={styles.container}>
      <ScrollView
        ref={scrollView => this.scrollView = scrollView}
        onContentSizeChange={()=> {        
          this.addNewEle && this.scrollView.scrollToEnd();
        }}
      >
        <View style={{ flex: 1, padding: 4 }}>
          {this.state.valueArray.map(ele => {
            return (
              <Item
                key={ele.id}
                item={ele}
                removeItem={(id) => this.remove(id)}
                afterAnimationComplete={this.afterAnimationComplete}
              />
            )
          })}
        </View>
      </ScrollView>

      <TouchableOpacity
        activeOpacity={0.8}
        style={styles.btn}
        disabled={this.state.disabled}
        onPress={this.addMore}
      >
        <Image source = { require('./assets/images/add.png') } style = { styles.btnImage }/>
      </TouchableOpacity>
    </View>
  );
}

Step – 12: Apply custom styles for all required components.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#eee',
    justifyContent: 'center',
    paddingTop: (Platform.OS == 'ios') ? 20 : 0
  },

  viewHolder: {
    paddingVertical: 15,
    backgroundColor: '#B00020',
    justifyContent: 'center',
    alignItems: 'flex-start',
    margin: 4,
    paddingLeft: 15,
    borderRadius: 10
  },

  text: {
    color: 'white',
    fontSize: 25,
    paddingRight: 17
  },

  btn: {
    position: 'absolute',
    right: 25,
    bottom: 25,
    borderRadius: 30,
    width: 60,
    height: 60,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,0.7)',
    padding: 15
  },

  btnImage: {
    resizeMode: 'contain',
    width: '100%',
    tintColor: 'white'
  },

  removeBtn: {
    position: 'absolute',
    right: 13,
    width: 25,
    height: 25,
    borderRadius: 15,
    padding: 7,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white'
  },

  removeIcon: {
    width: '100%',
    transform: [{ rotate: '45deg' }],
    resizeMode: 'contain'
  }
});

Complete Source Code:

import React, { Component } from 'react';
import {
  View,
  Text,
  Platform,
  StyleSheet,
  TouchableOpacity,
  Animated,
  ScrollView,
  Image,
  Dimensions,
  LayoutAnimation,
  UIManager
} from 'react-native';

const width = Dimensions.get('window').width;

class Item extends Component {
  constructor() {
    super();
    this.animatedValue = new Animated.Value(0);

    if( Platform.OS === 'android' ) {
      UIManager.setLayoutAnimationEnabledExperimental(true);
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    if(nextProps.item.id !== this.props.item.id) {
      return true;
    }
    return false;
  }

  componentDidMount() {
    Animated.timing(
      this.animatedValue,
      {
        toValue: 0.5,
        duration: 500,
        useNativeDriver: true
      }
    ).start(() => {
      this.props.afterAnimationComplete();
    });
  }

  removeItem = () => {
    Animated.timing(
      this.animatedValue,
      {
        toValue: 1,
        duration: 500,
        useNativeDriver: true
      }
    ).start(() => {
      this.props.removeItem(this.props.item.id);
    });
  }

  render() {
    const translateAnimation = this.animatedValue.interpolate({
      inputRange: [0, 0.5, 1],
      outputRange: [-width, 0, width]
    });

    const opacityAnimation = this.animatedValue.interpolate({
      inputRange: [0, 0.5, 1],
      outputRange: [0, 1, 0]
    });

    return (
      <Animated.View style={[
        styles.viewHolder, {
          transform: [{ translateX: translateAnimation}],
          opacity: opacityAnimation
        }]}
      >
        <Text
          style={styles.text}>
            Row {this.props.item.text}
        </Text>
        <TouchableOpacity
          style={styles.removeBtn}
          onPress={this.removeItem}
        >
          <Image
            source={require('./assets/images/add.png')}
            style={styles.removeIcon}
          />
        </TouchableOpacity>
      </Animated.View>
    );
  }
}

export default class App extends Component {
  constructor() {
    super();
    this.state = { valueArray: [], disabled: false }
    this.addNewEle = false;
    this.index = 0;
  }

  afterAnimationComplete = () => {
    this.index += 1;
    this.setState({ disabled: false });
  }

  addMore = () => {
    this.addNewEle = true;
    const newlyAddedValue = { id: "id_" + this.index, text: this.index + 1 };

    this.setState({
      disabled: true,
      valueArray: [...this.state.valueArray, newlyAddedValue]
    });
  }

  remove(id) {
    this.addNewEle = false;
    const newArray = [...this.state.valueArray];
    newArray.splice(newArray.findIndex(ele => ele.id === id), 1);

    this.setState(() => {
      return {
        valueArray: newArray
      }
    }, () => {
      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <ScrollView
          ref={scrollView => this.scrollView = scrollView}
          onContentSizeChange={()=> {        
            this.addNewEle && this.scrollView.scrollToEnd();
          }}
        >
          <View style={{ flex: 1, padding: 4 }}>
            {this.state.valueArray.map(ele => {
              return (
                <Item
                  key={ele.id}
                  item={ele}
                  removeItem={(id) => this.remove(id)}
                  afterAnimationComplete={this.afterAnimationComplete}
                />
              )
            })}
          </View>
        </ScrollView>

        <TouchableOpacity
          activeOpacity={0.8}
          style={styles.btn}
          disabled={this.state.disabled}
          onPress={this.addMore}
        >
          <Image source = { require('./assets/images/add.png') } style = { styles.btnImage }/>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#eee',
    justifyContent: 'center',
    paddingTop: (Platform.OS == 'ios') ? 20 : 0
  },

  viewHolder: {
    paddingVertical: 15,
    backgroundColor: '#B00020',
    justifyContent: 'center',
    alignItems: 'flex-start',
    margin: 4,
    paddingLeft: 15,
    borderRadius: 10
  },

  text: {
    color: 'white',
    fontSize: 25,
    paddingRight: 17
  },

  btn: {
    position: 'absolute',
    right: 25,
    bottom: 25,
    borderRadius: 30,
    width: 60,
    height: 60,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,0.7)',
    padding: 15
  },

  btnImage: {
    resizeMode: 'contain',
    width: '100%',
    tintColor: 'white'
  },

  removeBtn: {
    position: 'absolute',
    right: 13,
    width: 25,
    height: 25,
    borderRadius: 15,
    padding: 7,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white'
  },

  removeIcon: {
    width: '100%',
    transform: [{ rotate: '45deg' }],
    resizeMode: 'contain'
  }
});

Enjoy guys and Happy Coding…

7 Comments

  1. how to genric button add delet button like as a pluse button also add minus button genric
    in left side click to remove last item view in row

  2. hello sir you are posted very good tutorials on react native for begineers,i appreciates you very much.
    sir iam searching over the internet but none did project on FETCHING MARKERS FROM DATABASE USING MYSQL+PHP ,

    i have source code and php connections and mysql databse

  3. Getting error like undefined is not an object(evaluating ‘nextProps.item.id’)

  4. Thank you very much I finished my college assignment from this site.

Leave a Reply

Your email address will not be published. Required fields are marked *