A customizable and lazy-loading-ready Timeline component for react native

react-native-just-timeline

React Native's customizable, RTL-supported, elegant, lazy-loading-ready Timeline component.

React Native Just Timeline is a package intended to provide an elegant, highly flexible and customizable, performant solution to render a sequence of events, taking in mind the support for RTL orientation and the package was developed with that concern in mind.
So, this package has the following as its major concerns:

  1. Support for RTL
  2. Clean design
  3. Flexibility to customize that design
  4. Perfomance, by providing the ability to lazy-load
  5. Lightweight

Installation

The installation for the Timeline itself is pretty straightforward, just:

npm install react-native-just-timeline

However, react-native-vector-icons is internally used to provided the icons, so you'll have to make sure it's correctly installed in your project and properly linked, or otherwise, an error would pretty much occur, e.g. Unable to resolve 'FontAwesome'.

Usage

Basic

Actually, to get the Timeline up and running, all you need to do is to call the component Timeline and provide it with a data array of objects. Every object represents a timeline event or row. The basic data shape to get the default Timeline functionality and styling is:

const BasicTimeline = () => {
  const data = [
    // First row in the Timeline
    {
      title: {
        content: 'Event One Title',
      },
      description: {
        content: 'Event One Description',
      },
      time: {
        content: moment().format('ll'),
      },
    },

    // Second row in the Timeline
    {
      title: {
        content: 'Event Two Title',
      },
      description: {
        content: 'Event Two Description',
      },
      time: {
        content: moment().format('ll'),
      },
    },

    // You got the idea..
    {
      title: {
        content: 'Event Three Title',
      },
      description: {
        content: 'Event Three Description',
      },
      time: {
        content: moment().format('ll'),
      },
      icon: {
        content: 'pencil',
      },
    }
  ];

  return (
    <View style={styles.container}>
      <Timeline data={data} />
    </View>
  );
};

This code gets you the default basic Timeline, like below:

The title, description, time, and icon are objects because we can provide each of them with another property that is adjacent to content for styling style, and it accepts normal styling properties and is used to override the default styling of each of them, e.g.:

  const data = [
    {
      title: {
        content: 'Event One Title',
        style: {
          color: '#fff',
          backgroundColor: '#222',
        }
      },
      description: {
        content: 'Event One Description',
        style: {
          color: 'green',
        }
      },
      time: {
        content: moment().format('ll'),
        style: styles.time,
      },
    },
    ...
  ];

Check the /Examples/Example1 and /Examples/Example2 for clearer overlook and more tweaks ;)

Render props

Render props is always a great React.js/React Native pattern that embraces reusability and customizability.
If you felt like you are kind of limited with the previous structure of { content: "", style: {} } and need more control, you can send a function that returns a React Native component, whatsoever it is. And, we can do that on each part of the row individually without having to follow the same pattern on all of them, i.e. in the following code, we can mix up the render props in title with basic { content: "", style: {} } on the rest.
**You get the default style of title, description, icon, and time as a prop to the function```

const RenderPropsUsage = () => {
  const data = [
  // First row in Timeline
  {
    // Here we send a function that returns a component instead of object
    title: ({ styles }) => (
      <View>
        <Text style={{fontSize: 10, color: '#999', marginBottom: 7}}>
          {moment().format('lll')}
        </Text>
        <Text style={[styles, {marginBottom: 0, color: '#d2584b'}]}>
          Item Deleted Event
        </Text>
      </View>
    ),
    description: {
      content: 'Item Deleted Event Description',
    },
    time: {
      content: moment().format('ll'),
      style: {
        paddingTop: 8,
      },
    },
    icon: {
      content: 'trash',
      style: {
        width: 35,
        height: 35,
        backgroundColor: '#d2584b',
        color: '#FFF',
        borderColor: '#FFF',
        fontSize: 16,
        paddingTop: 6,
        borderRadius: 18,
      },
    },
  },
  
  // Second row in Timeline
  {
    title: ({styles}) => (
      <View>
        <Text style={{fontSize: 10, color: '#999', marginBottom: 7}}>
          {moment().format('lll')}
        </Text>
        <Text style={[styles, {marginBottom: 0, color: '#00b48b'}]}>
          Item Added Event
        </Text>
      </View>
    ),
    description: {
      content: 'Item Added Event Description',
    },
    time: {
      content: moment().format('ll'),
      style: {
        paddingTop: 8,
      },
    },
    icon: {
      content: 'check',
      style: {
        width: 35,
        height: 35,
        backgroundColor: '#00b48b',
        color: '#FFF',
        borderColor: '#FFF',
        fontSize: 16,
        paddingTop: 6,
        borderRadius: 18,
      },
    },
  }];
  
  return (
    <View style={styles.container}>
      <Timeline data={data} />
    </View>
  );
};

This code gets you something like:

Check the /Examples/Example3 and /Examples/Example4 for more.

Clickable Rows

Well, you may face that case in which you need to perform an action on a certain event/row in the Timeline when pressing. This functionality is available, you control it on each event/row's object individually by sending a pressAction function which gets executed onPress.

const Example5 = () => {
  const [isModalOpen, setModalStatus] = useState(false);
  const data = [
    // First row in Timeline
    {
      pressAction: () => setModalStatus(true), // Gets triggered on row's press
      title: {
        content: 'Bakr',
      },
      description: {
        content: 'Comment goes right here',
      },
      time: {
        content: moment().format('ll'),
        style: {
          paddingTop: 8,
        },
      },
      icon: () => (
        <View>
          <Image
            source={require('../assets/images/1.jpg')}
            style={{
              width: 45,
              height: 45,
              borderRadius: 25,
              borderWidth: 3,
              borderColor: '#fff',
            }}
          />
        </View>
      ),
    },
    
    // Second row in Timelind
    {
      pressAction: () => setModalStatus(true), // Gets triggered on row's press
      title: {
        content: 'Nancy',
      },
      description: {
        content: 'Comment goes right here',
      },
      time: {
        content: moment().format('ll'),
        style: {
          paddingTop: 8,
        },
      },
      icon: () => (
        <View>
          <Image
            source={require('../assets/images/2.jpg')}
            style={{
              width: 35,
              height: 35,
              borderRadius: 20,
              borderWidth: 3,
              borderColor: '#FFF',
            }}
          />
        </View>
      ),
    }
  ];
  
  return (
    <View style={styles.exampleContainer}>
    
      {/* Toggle this modal on event/row's press */}
      <CustomDialog
        isVisible={isModalOpen}
        dismissAction={() => setModalStatus(false)}>
        <PopupContent pressAction={() => setModalStatus(false)} />
      </CustomDialog>
      
      
      <Timeline data={data} />
    </View>
  );
};

This code gets you something like:

Check the /Examples/Example5 for more.

Lazy-loading, Infinite Scrolling

Internally, the package utilizes React Native's FlatList to manage the list of rows in a performant way. That brings us to an important hint: you can send any props that you usually send to FlatList to JustTimeline. However, some props are renamed, refer to the props table to know about that

So, with FlatList in mind, we can create a lazy-loadable Timeline using the exact same functionality as we might do in a normal FlatList, using onEndReached function.
onEndReached will be a function that creates an async call and then appends the new data to our state.
TimelineFooter a prop that accepts a React Native component to display at the end/bottom of the Timeline, we use it to render a loading spinner at the end of the Timeline while fetching the data.

const LazyLoadableTimeline = () => {
  const [{loading, data}, setState] = useReducer(
    (base, next) => ({...base, ...next}),
    {
      loading: false,
      data: defaultData, // The already-existing data that were, for instance, fetched on component mounting
    },
  );

  return (
    <View style={styles.container}>
      <Timeline
        data={data}
        TimelineFooter={() => (loading ? <ActivityIndicator /> : null)} // Loader component
        onEndReached={() => {
          // When the user reaches the end of the Timeline, set a loader
          setState({loading: true});
          
          // Your asyncronous behavious, e.g. AJAX call
          setTimeout(() => {
            // Append the new data to the state's array
            setState({
              data: data.concat([
                {
                  title: {
                    content: 'New Event 1',
                  },
                  description: {
                    content: 'New Event 1 description',
                  },
                  time: {
                    content: moment().format('ll'),
                  },
                },
                {
                  title: {
                    content: 'New Event 2',
                  },
                  description: {
                    content: 'New Event 2 description',
                  },
                  time: {
                    content: moment().format('ll'),
                  },
                }
              ]),
              
              // Turn off the loader
              loading: false,
            });
          }, 2500);
        }}
      />
    </View>
  );
};

The previous code gets you the following behavior:

Check the /Examples/Example6 for more.

Props

Prop Description Type Default
data Contains the events/rows data as an array of object. Each object represents an event Array Array
eventStyle Style object used to override the row's container's styles Object Object
timeContainerStyle Style object used to override the time's container block's styles Object Object
iconContainerStyle Style object used to override the icon's container block's styles Object Object
lineStyle Style object used to override the vertical line style Object Object
contentContainerStyle Style object used to override the content's container block's styles, i.e. that contains title and description Object Object
TimelineFooter A React Native component to be rendered at the end of the Timeline React Native Component Undefined
TimelineHeader A React Native component to be rendered at the heading of the Timeline React Native Component Undefined
onEndReached A function that gets executed when user reaches the end of the Timeline Function Undefined
onEndReachedThreshold Defines the distance in pixels far from the Timeline's bottom at which the onEndReached function gets called Number 0

GitHub