Log In

React Native react-native-elements


by Yariv Katz

One of the reasons react native is the best choice for cross platform native mobile solution is the large community behind it. The large community behind react native is creating amazing high quality components we can use while developing in react native. A lot of those open source community libraries are really high quality and a lot of programming hours were invested on creating them, there are quality components that looks good on iOS as well as android. Using those libraries saves us a lot of programming time. One of the popular libraries to use with react native is react-native-elements. Think of this library like the material design library for react native, it contains common mobile components that are according to UI guidelines for android as well as iOS. In this tutorial we will make a todo application using react-native-elements.

Bootstrapping React Native

Let's start a new react native application, and install react-native-elements In your work directory type in the terminal:

> npx react-native-cli init RnTodo
> cd RnTodo
> npm install --save react-native-elements
> npm install react-native-vector-icons --save
> react-native link react-native-vector-icons
> npm start
> react-native run-ios

react-native-elements will also use the vector icons library so we installed that as well. Now that our app is running on our simulator time to start creating our todo application.

react-router-native

The Todo application will contain a few different screens. To achieve routing between different screens in our app we will install react native router. To install the router in the terminal type:

> npm install react-router-native --save

To use the router we will have to wrap our app with the NativeRouter component. Modify the App.js to this:

import React, {Component} from 'react';
import {View} from 'react-native';
import {NativeRouter} from 'react-router-native';


type Props = {};
export default class App extends Component<Props> {
    render() {
        return (
        <NativeRouter>
            <View>
            </View>
        </NativeRouter>
        );
    }
}

We are simply wrapping our app with the NativeRouter component.

Header

Our todo application will contain a Header component. The top header will contain the title of the screen, as well as a back button on the left, and an action button on the right. react-native-elements contain a Header component that we can use for that. The title in the header will be changed according the the current route we are in. The Header title and the action on the right side icon will change according to the route we are currently in. Could be a good idea to create an Header component of our own that will wrap the react-native-elements component.

We will need to know in other components, when the right header icon is clicked and perform a different action in different route. The best way will be not to deal with the action in the header rather in the routes components. So we will need to transform that the right icon is clicked to different components. We will also need to set the right icon from different components. We can use ReactiveX library and define a Subject for the icon and click event which everyone can emit a new pulse and subscribe to listen to data pulses. Install rxjs via the terminal:

> npm install rxjs --save

Now let's create a service for the header to pass icon and to pass the clicking of the header right icon. In the project dir create a folder called services and in that folder create a file called header.js with the following:

import { Subject, BehaviorSubject } from 'rxjs';

let instance = null;
export default class HeaderService {
    actionClicked$: Subject<void> = new Subject();
    icon$: BehaviorSubject<string> = new BehaviorSubject(null);

    static getInstance(): HeaderService {
        if (!instance) {
            instance = new this();
        }
        return instance;
    }
}

A Subject is a data stream with async pulses of data we can listen to. a BehaviourSubject is a data stream with initial data. We create a singleton service with a data stream passing pulses when the header icon is clicked (actionClicked$) and another data stream passing a string icon to the header. Now let's create the header component. Create a file called Header.js with the following code:

import React, {Component} from 'react';
import {View} from 'react-native';
import {Header as RNEHeader} from 'react-native-elements';
import type {Match, Location, History} from 'react-router-native';
import HeaderService from './services/header';

type Props = {
    match : Match,
    location : Location,
    history: History,
};

type State = {
    rightIcon?: string
}

export default class Header extends Component<Props, State> {
    state = {
        rightIcon: undefined
    }

    componentDidMount() {
        HeaderService.getInstance().icon$.subscribe((rightIcon: string) => {
            this.setState({
                rightIcon
            })
        });
    }

    rightPressed = () => {
        HeaderService.getInstance().actionClicked$.next();
    }

    backPressed = () => {
        const {history} = this.props;
        history.goBack();
    }

    headerByRoute = () : string  => {
        const {location} = this.props;
        switch(location.pathname) {
            case '/':
                return 'Todo List';
            default:
                return 'Todo List'
        }
    }

    render() {
        const {history} = this.props;
        return (
            <View>
                <RNEHeader
                    leftComponent={ history.entries.length > 1 ? {icon: 'chevron-left', onPress: this.backPressed } : null }
                    centerComponent={ {text: this.headerByRoute()} }
                    rightComponent={this.state.rightIcon ? {icon: this.state.rightIcon, onPress: this.rightPressed} : null }
                />
            </View>
        )
    }
}

Our Header component is importing the Header component from react-native-elements and aliasing it to the name RNEHeader. The react native elements header will get properties of what to put on the left center and right. The icon we are placing on the left is the left chevron (the default icons from react vector icons are font-awesome) and when pressing the left icon we are activating a method called backPressed. Our Header component will be inserted by the router which means we will get in the properties the properties which are passed by the router which one of them is the History object we can use to go back to the previous screen. We can also use the history to query if there are previous screens in the stack and conditionaly show the back icon if there is more than one entry. The center component displays the title of the screen based on location.pathname and we will add more to that switch as we have more screens. For the right component we are getting the icon from the state, and the state will get it from subscribing to icon$ in componentDidMount. The action of the right component will be activating the subject in the header service.

Now we need to place our header component. Modify the render method in App.js

render() {
    return (
    <NativeRouter>
        <View>
        <Route 
            component={Header}
        />
        </View>
    </NativeRouter>
    );
}

We are importing the Header component we created and use a Route selector to place it for all the routes.

Todo List

Our next job is to grab the todo list from the server and display that list. We have a todo rest server located in the following URL: https://nztodo.herokuapp.com/api/task/?format=json A single todo item from the server looks like this:

{"id":9662,"title":"w11","description":"NAAMA 22","group":"gdfgdf","when":"2019-11-01T15:02:00Z"}

For proper encapsulation and reuse of our code, it is recommended not to deal directly with the json returned from the server, rather create classes for the elements from the server. In your project directory create a folder called models and in that folder create a file called: task.js with the following code:

export default class Task {
    id: number;
    title: string;
    description: string;
    when: Date;
    group: string

    constructor(json: {[key : string] : any}) {
        this.id = json.id || 0;
        this.title = json.title || '';
        this.description = json.description || '';
        this.when = json.when ? new Date(json.when) : new Date();
        this.group = json.group || '';
    }
}

our class will get a json from the server and will know how to transfer that json to a model class.

Let's create a service that will wrap the server calls for the todo rest server. Our service will be able to query the server and get a list of todo items, create a new todo item in the server, get a single todo item and delete and item. in the services folder create a file called todo.js with the following:

import Task from '../models/task';

let instance = null;
export default class Todo {
    static getInstance(): Todo {
        if (!instance) {
            instance = new this();
        }
        return instance;
    }

    getTasks = (): Promise<Task[]> => {
        return fetch('https://nztodo.herokuapp.com/api/task/?format=json')
            .then((res) => res.json())
            .then((items: any[]) => items.map((item) => new Task(item)));
    }
}

Our service is a singleton with a method to query our todo server and return the items mapped as our class. We will first create a new file called TodoList.js with the following:

import React, {Component} from 'react';
import { View, FlatList } from 'react-native';
import { List, ListItem } from 'react-native-elements';
import Task from './models/task';
import TodoService from './services/todo';
import HeaderService from './services/header';

type Props = {
    history: History
}

type State = {
    tasks: Task[]
}

export default class TodoList extends Component<Props, State> {
    state = {
        tasks: []
    }

    rightIconPressed = () => {
        const {history} = this.props;
        history.push('/add-task');
    }

    async componentDidMount() {
        HeaderService.getInstance().actionClicked$.subscribe(this.rightIconPressed)
        HeaderService.getInstance().icon$.next('plus-one');
        const tasks: Task[] = await TodoService.getInstance().getTasks()
        this.setState({
            tasks
        });
    }

    gotoTask = (taskId: number) => {
        const {history} = this.props;
        history.push(`/task/${taskId}`);
    }

    renderRow = ({item}: Task) => {
        return (
            <ListItem 
                title={item.title}
                subtitle={item.description}
                onPress={() => this.gotoTask(item.id)}
            />
        )
    }

    render() {
        return (
            <View>
                <List>
                    <FlatList 
                        data={this.state.tasks}
                        keyExtractor={item => item.id.toString()}
                        renderItem={this.renderRow}
                    />
                </List>
            </View>
        )
    }
}

This component will use the react-native-elements List and ListItem to create a nice looking list for android and iOS. We are also using the FlatList to optimize the rendering of elements to only the ones that are currently viewed. in the componentDidMount we are grabbing the list from the server and populating that list in the state. we are also setting the icon to be plus icon and subscribing the when the plus icon is pressed we will navigate to the screen to add a new task.

Modify the App.js to include the component we created. We will include that component in the root route of the app.

import React, {Component} from 'react';
import {View} from 'react-native';
import {NativeRouter, Route, Switch} from 'react-router-native';
import Header from './Header';
import TodoList from './TodoList';

type Props = {};
export default class App extends Component<Props> {
    render() {
        return (
        <NativeRouter>
            <View>
            <Route 
                component={Header}
            />

            <Switch>
                <Route path="/" exact component={TodoList} />
            </Switch>

            
            </View>
        </NativeRouter>
        );
    }
}

We are using the Switch route selector to select a component for each page in our app. and we are placing the TodoList component for the main route.

add-task

On the TodoList component, we are directing the user when he click the header right icon to the /add-task route. In this route we want to display a page where the user can create a new todo task. React native elements has form components we can use to make our form more appealing. We would also want to place a date picker to choose the time of the todo task. react-native has a date picker but it is a different component for android and iOS, there is a more popular date picker component we can use called: react-native-datepicker Let's install the library of this component. In your terminal type:

> npm install react-native-datepicker --save

We will also need to modify our todo service and add a method to that service to send a post request to the server to create a new task. In the services folder, modify the file todo.js

import Task from '../models/task';

let instance = null;
export default class Todo {
    url = 'https://nztodo.herokuapp.com/api/task/';

    static getInstance(): Todo {
        if (!instance) {
            instance = new this();
        }
        return instance;
    }

    getTasks = (): Promise<Task[]> => {
        return fetch(`${this.url}?format=json`)
            .then((res) => res.json())
            .then((items: any[]) => items.map((item) => new Task(item)));
    }

    createTask = (task: Task): Promise<Task> => {
        return fetch(this.url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(task)
        })
        .then((res) => res.json())
        .then((json: any) => new Task(json));
    }
}

We added the method createTask which will send a post request for the server to create a new task, and return a promise with the created task.

Now let's create the page component of the add task screen. In the project directory, create the file AddTask.js with the following:

import React, {Component} from 'react';
import { FormLabel, FormInput, FormValidationMessage, Button } from 'react-native-elements';
import { View } from 'react-native';
import DatePicker from 'react-native-datepicker';
import TodoService from './services/todo';
import Task from './models/task';
import {History} from 'react-router-native';

type Props = {
    history: History
}

type State = {
    title: string,
    description: string,
    group: string,
    when: string,
    isSubmitted: boolean,
    isLoading: boolean,
    error: string | null
}

export default class AddTask extends Component<Props, State> {
    state = {
        title: '',
        description: '',
        group: '',
        when: (new Date()).toISOString(),
        isSubmitted: false,
        isLoading: false,
        error: null
    }

    constructor(props: Props) {
        super(props);
        this.submitTask = this.submitTask.bind(this);
    }

    submitTask: () => Promise<any>;
    async submitTask() {
        this.setState({
            isSubmitted: true,
            isLoading: true
        });
        try {
            const task: Task = await TodoService.getInstance().createTask(new Task(this.state));
            const {history} = this.props;
            history.replace(`/task/${task.id}`);
        } catch(error) {
            this.setState({error: error.message});
        }
    }

    render() {
        return (
            <View>
                <FormLabel>Title</FormLabel>
                <FormInput 
                    maxLength={100}
                    onChangeText={(text) => this.setState({title: text})}
                />
                {
                    this.state.isSubmitted && this.state.title === '' ?
                        <FormValidationMessage>This field is required</FormValidationMessage> :
                        null
                }
                <FormLabel>Description</FormLabel>
                <FormInput 
                    multiline
                    onChangeText={(text) => this.setState({description: text})}
                />
                <FormLabel>Group</FormLabel>
                <FormInput 
                    maxLength={100}
                    onChangeText={(text) => this.setState({group: text})}
                />
                {
                    this.state.isSubmitted && this.state.group === '' ?
                        <FormValidationMessage>This field is required</FormValidationMessage> :
                        null
                }
                <FormLabel>When</FormLabel>
                <DatePicker 
                    style={ {width: '100%', paddingRight: 20, paddingLeft: 20, marginBottom: 20, marginTop: 10} }
                    mode="datetime"
                    date={this.state.when}
                    onDateChange={(date: string) => this.setState({when: date})}
                />
                <Button 
                    loading={this.state.isLoading}
                    title="Create"
                    onPress={this.submitTask}
                />
                {
                    this.state.error ?
                        <FormValidationMessage>{this.state.error}</FormValidationMessage> :
                        null
                }
            </View>
        )
    }
}


We are creating our form with react-native-elements. The date picker object we are using the react-native-datepicker library. our submitTask method use our service to create a new task. we also added validation to our component. If the create new task is success we are directing the user to the task page he just created. Notice also that since the submitTask method is async await method, we can't use the lambda syntax of class methods so we have to bind that method in the constructor, in order to avoid flow error when binding the method we have to place a declaration of that method as well. Let's connect this component to the App.js based on the route we are in. Modify App.js and add the following to the Switch statement in that component:

<Switch>
    <Route path="/" exact component={TodoList} />
    <Route path="/add-task" component={AddTask} />
</Switch>

Of course you will have to also add the import for the AddTask component at the top of the file. Now our route Switch will choose between the TodoList and the AddTask page.

Task page

We will now create the page to display a single task page, this will be the page we go when selecting an item in our list, or when creating a new task we will be directed to the task page we just created. From that page we can either delete a task, or update the current task. Let's first add the proper methods in the service todo.js to get a single task. In the services folder, modify the file todo.js to the following:

import Task from '../models/task';

let instance = null;
export default class Todo {
    url = 'https://nztodo.herokuapp.com/api/task/';

    static getInstance(): Todo {
        if (!instance) {
            instance = new this();
        }
        return instance;
    }

    getTasks = (): Promise<Task[]> => {
        return fetch(`${this.url}?format=json`)
            .then((res) => res.json())
            .then((items: any[]) => items.map((item) => new Task(item)));
    }

    createTask = (task: Task): Promise<Task> => {
        return fetch(this.url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(task)
        })
        .then((res) => res.json())
        .then((json: any) => new Task(json));
    }

    getTask = (id: number | string): Promise<Task> => {
        return fetch(`${this.url}${id}/?format=json`)
            .then(res => res.json())
            .then(json => new Task(json));
    }
}

We added a method to delete a task and a method to update a task. Now let's add another component to display details about a task. Create a new file called TaskDetails.js with the following code:

import React, {Component} from 'react';
import Task from './models/task';
import { Card, ListItem } from 'react-native-elements';
import { View, Text } from 'react-native';
import type {Match} from 'react-router-native';
import Todo from './services/todo';
import HeaderService from './services/header';

type Props = {
    match: Match
};

type State = {
    task: Task | null
};

export default class TaskDetails extends Component<Props, State> {
    state = {
        task: null
    }

    async componentDidMount() {
        const {match}: Match = this.props;
        const {id}: string = match.params
        const task: Task = await Todo.getInstance().getTask(id);
        this.setState({
            task
        });
    }

    renderRow = (key: string, value: string): React.Element => {
        return (
            <View style={ {justifyContent: 'space-between', flexDirection: 'row'} }>
                <View style={{width: 80}}>
                    <Text style={{fontWeight: 'bold'}}>{key}</Text>
                </View>
                <View style={{}}>
                    <Text>{value}</Text>
                </View>
            </View>
        );
    }

    render() {
        const {task} = this.state;
        if (!task) return null;
        return (
            <Card title={task.title}>
                <ListItem 
                    hideChevron
                    title={this.renderRow('Title', task.title)}
                />
                <ListItem 
                    hideChevron
                    title={this.renderRow('Description', task.description)}
                />
                <ListItem 
                    hideChevron
                    title={this.renderRow('Group', task.group)}
                />
                <ListItem 
                    hideChevron
                    title={this.renderRow('When', task.when.toDateString())}
                />
            </Card>
        );
    }
}

after the component is mounting we are grabbing the task from the server. We are using the Card from react-native-elements to display the data, and we are rendering the list items title to display list with category in bold and the value.

Now let's connect our component to the proper route in App.js. In App.js in the Switch statement add the following:

<Switch>
    <Route path="/" exact component={TodoList} />
    <Route path="/add-task" component={AddTask} />
    <Route path="/task/:id" component={TaskDetails} />
</Switch>

Test your app, and you should see all the screens are working properly.

Summary

In this article we covered the popular components we have in the library react-native-elements. We can quickly build a professional looking cross platform mobile app for iOS and Android using awesome community components. We also combined router in our app and communicated between the components using rxjs wchich works perfectly on react app and react native as well.