by

The series

Welcome to the final part of the series. Here is the list of the previous parts:

  1. Part 1. Setting up a new project and dependencies. Building presentational chart component.
  2. Part 2. Building the list of coins and fetching the prices using API.
  3. Part 3. Final part. Using real data for the chart and date ranges and adding new coins.

What we will be building

In this final part, we’re going to upgrade chart component, so it uses real historical data using API and shows data for different date ranges. We’re also going to create a new component that shows a list of all available coins and allows adding new coins to the main list.

By the end of this tutorial, you’ll have a fully functional app on the screenshot.

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

Previous part code

We’ll continue building upon the code we wrote in the previous part. If you haven’t been following up and you don’t have the code you can check it out from GitHub. Open Terminal App and execute these commands:

git clone --branch part-2 https://github.com/rationalappdev/react-native-charts-tutorial.git;
cd react-native-charts-tutorial;
npm install;

Design layout breakdown

Let’s take a look at the design layout breakdown from the previous part again to remind us into what components we decided to break the app down.

In this part, we’re going to be working on Chart, Date Ranges and Add Coin Button components.

Setting up Redux for Chart container

Let’s start off by setting up actions and reducer for fetching historical prices for the chart using API, and managing date ranges.

Adding actions and reducer

  • Create a new file called chart.js within src/redux folder.
// @flow

import { get } from '../api';
import type { Store } from './index';

// -----------------------------------------------------------------------------
// Constants
// -----------------------------------------------------------------------------

export const RANGE_1D = '1D';
export const RANGE_1W = '1W';
export const RANGE_1M = '1M';
export const RANGE_3M = '3M';
export const RANGE_6M = '6M';
export const RANGE_1Y = '1Y';
export const RANGE_MAX = 'MAX';
export const RANGES = [RANGE_1D, RANGE_1W, RANGE_1M, RANGE_3M, RANGE_6M, RANGE_1Y, RANGE_MAX];

// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------

export type Range = typeof RANGE_1D
  | typeof RANGE_1W
  | typeof RANGE_1M
  | typeof RANGE_3M
  | typeof RANGE_6M
  | typeof RANGE_1Y
  | typeof RANGE_MAX;

export type State = {
  +loading: boolean,      // activity indicator flag
  +range: Range,          // current date range
  +prices: Array<number>, // historical prices
};

type Action =
  | { type: 'LOADING_CHART_PRICES' }
  | { type: 'SELECTED_CHART_RANGE', range: Range }
  | {
  type: 'UPDATED_CHART_PRICES',
  response: {
    Data: Array<{
      close: number,
      high: number,
      low: number,
      open: number,
      time: number,
      volumefrom: number,
      volumeto: number,
    }>,
    TimeFrom: number,
    TimeTo: number,
  }
}
  ;

type GetState = () => Store;
type PromiseAction = Promise<Action>;
type ThunkAction = (dispatch: Dispatch, getState: GetState) => any;
type Dispatch = (action: Action | ThunkAction | PromiseAction | Array<Action>) => any;

// -----------------------------------------------------------------------------
// Initial state
// -----------------------------------------------------------------------------

const initialState: State = {
  loading: true,  // show activity indicator on first load
  range: '1D',    // default to one day range
  prices: [],     // no price data initially
};

// -----------------------------------------------------------------------------
// Actions
// -----------------------------------------------------------------------------

// Change current date range
export const selectRange = (range: Range): Action => ({
  type: 'SELECTED_CHART_RANGE',
  range
});

// Fetch prices using API and dispatch the data to reducer
export const updateChartPrices = (): ThunkAction => async (dispatch, getState) => {
  dispatch({ type: 'LOADING_CHART_PRICES' });
  const {
    coins: { current },
    chart: { range },
  } = getState();
  const response = await get(buildAPIQuery(current, range));

  dispatch({
    type: 'UPDATED_CHART_PRICES',
    response,
  });
};

// Build API query based on symbol of interest and current date range
const buildAPIQuery = (symbol: string, range: Range): string => {

  let endpoint = 'histohour';
  let aggregate = 1;
  let limit = 24;

  switch (range) {
    case RANGE_1D:
      endpoint = 'histohour';
      aggregate = 1;
      limit = 24;
      break;
    case RANGE_1W:
      endpoint = 'histoday';
      aggregate = 1;
      limit = 7;
      break;
    case RANGE_1M:
      endpoint = 'histoday';
      aggregate = 1;
      limit = 30;
      break;
    case RANGE_3M:
      endpoint = 'histoday';
      aggregate = 3;
      limit = 30;
      break;
    case RANGE_6M:
      endpoint = 'histoday';
      aggregate = 6;
      limit = 30;
      break;
    case RANGE_1Y:
      endpoint = 'histoday';
      aggregate = 12;
      limit = 30;
      break;
    case RANGE_MAX:
      endpoint = 'histoday';
      aggregate = 200;
      limit = 2000; // maximum allowed limit
      break;
  }

  return `data/${endpoint}?fsym=${symbol}&tsym=USD&aggregate=${aggregate}&limit=${limit}`;
};

// -----------------------------------------------------------------------------
// Reducer
// -----------------------------------------------------------------------------

export default function reducer(state: State = initialState, action: Action) {
  switch (action.type) {

    case 'LOADING_CHART_PRICES': {
      return {
        ...state,
        loading: true,
      };
    }

    case 'UPDATED_CHART_PRICES': {
      const { response: { Data } } = action;
      return {
        ...state,
        loading: false,
        prices: !!Data ? Data.map(item => item.close) : [] // use closing prices
      };
    }

    case 'SELECTED_CHART_RANGE': {
      const { range } = action;
      return {
        ...state,
        range,
      };
    }

    default: {
      return state;
    }
  }
}
  • Open index.js file within src/redux folder and replace its content with the following to combine chart reducer we just created with coins reducer.
// @flow

import { combineReducers } from 'redux';
// import reducers
import chart from './chart';
import coins from './coins';
// import types
import type { State as Chart } from './chart';
import type { State as Coins } from './coins';

export type Store = {
  +chart: Chart,
  +coins: Coins,
};

export default combineReducers({
  chart,
  coins,
});

Updating Chart container

Next, let’s make use of those actions and reducer we just created and update Chart container to use real data instead of the fake data that we currently have.

  • Open chart.js file within src/containers folder and replace its content with the following.
import React, { Component } from 'react';
import {
  ActivityIndicator,
  StyleSheet,
  View
} from 'react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { updateChartPrices } from '../redux/chart';
import Line from '../components/chart/line';

@connect(
  (state) => {
    const {
      coins: {
        current: symbol,
      },
      chart: {
        range,
        prices,
        loading,
      },
    } = state;
    return {
      symbol,
      range,
      prices,
      loading,
    };
  },
  (dispatch) => bindActionCreators({ updateChartPrices }, dispatch)
)
export default class Chart extends Component {

  state = {
    viewHeight: 0,
    prices: [],
  };

  componentWillMount() {
    this.props.updateChartPrices();
  }

  componentWillReceiveProps(nextProps) {
    // Update chart data if current symbol or range were changed
    if (nextProps.symbol !== this.props.symbol
      || nextProps.range !== this.props.range) {
      this.props.updateChartPrices();
    }
    this.setState({ prices: nextProps.prices });
  }

  render() {
    const {
      loading,
      prices,
    } = this.props;

    return (
      <View style={styles.container}>
        {loading && <View pointerEvents="box-none" style={styles.loading}>
          <ActivityIndicator size="large" />
        </View>}
        {prices.length > 0 && <Line
          values={this.state.prices}
        />}
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 38, // take 38% of the screen height
    backgroundColor: '#FFFFFF',
  },
  loading: {
    ...StyleSheet.absoluteFillObject, // overlay the chart
    alignItems: 'center',             // center horizontally
    justifyContent: 'center',         // center vertically
    zIndex: 1,                        // show in front of the chart
  },
});

Checking out the progress

Let’s pull up the simulator to see how the chart is being updated with real historical prices when we switch between different coins.

Adding Range component

Next, let’s build date range switcher.

  • Create a new folder called range within src/components folder.

Building presentational components

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

import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  TouchableOpacity,
} from 'react-native';

export default class Range extends Component {

  props: {
    name: string,
    active: boolean,
    onPress: (range: string) => void,
  };

  onPress = () => {
    const {
      name,
      onPress
    } = this.props;
    onPress(name);
  };

  render() {
    const {
      name,
      active,
    } = this.props;
    return (
      <TouchableOpacity style={styles.container} onPress={this.onPress}>
        <Text style={[styles.text, active ? styles.active : {}]}>{name}</Text>
      </TouchableOpacity>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    padding: 15,
  },
  text: {
    color: 'rgba(255,255,255,0.5)',
    fontFamily: 'Avenir',
    fontSize: 12,
  },
  active: {
    color: '#FFFFFF',
  },
});
  • Create a new file called switcher.js within src/components/range folder.
import React, { Component } from 'react';
import {
  StyleSheet,
  View
} from 'react-native';
import Range from './range';

export default class Switcher extends Component {

  render() {
    const {
      ranges,
      current,
      onSelectRange,
    } = this.props;
    return (
      <View style={styles.container}>
        {ranges.map((name, index) =>
          <Range
            name={name}
            active={current === name}
            onPress={onSelectRange}
            key={index}
          />)}
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
});

Updating Ranges container

Now, let’s update Ranges container.

  • Open ranges.js file within src/containers folder and replace its content with the following.
import React, { Component } from 'react';
import {
  StyleSheet,
  View
} from 'react-native';

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { selectRange } from '../redux/chart';
import Switcher from '../components/range/switcher';
import { RANGES } from '../redux/chart';

@connect(
  (state) => {
    const {
      chart: {
        range,
      },
    } = state;
    return {
      range,
    };
  },
  (dispatch) => bindActionCreators({ selectRange }, dispatch)
)
export default class Ranges extends Component {

  render() {
    const {
      range,
      selectRange,
    } = this.props;
    return (
      <View style={styles.container}>
        <Switcher
          ranges={RANGES}
          current={range}
          onSelectRange={selectRange}
        />
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#673AB7',
  },
});

Checking out the progress

Let’s pull up the simulator to see how the chart is being updated when we switch between different date ranges.

Adding new coins

Next, we want to add a button at the bottom of the list, which when pressed opens a modal screen with a list of all coins available for adding to the main list. The list will look somewhat like this.

Installing React Navigation

First of all, we’re going to need to add a new dependency to implement navigation between different screens. We haven’t though of this before, which is fine. You can’t plan everything 100% in advance.

npm install --save react-navigation

Setting up navigation

We’re going to reorganize the app a little bit since there’s going to be two screens: list for the current screen with the chart and list, and add for the new screen with a list of all available coins.

  • Create a new folder called screens within src folder.

Adding List screen

Now, we’re going to move most of app.js into a new file called list.js within src/screens folder. Except for <Provider /> since we want both screens to have access to Redux store.

  • Create a new file called list.js within src/screens folder.
// @flow

import React, { Component } from 'react';
import {
  StyleSheet,
  View
} from 'react-native';
import Chart from '../containers/chart';
import Ranges from '../containers/ranges';
import List from '../containers/list';
import type { Navigation } from '../app';

export default class extends Component {

  props: {
    navigation: Navigation,
  };

  onAddCoin = () => {
    const { navigation } = this.props;
    navigation.navigate('add');
  };

  render() {
    return (
      <View style={styles.container}>
        <Chart />
        <Ranges />
        <List onAddCoin={this.onAddCoin} />
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,        // take up the whole screen
    paddingTop: 20, // put content below status bar
    backgroundColor: 'white',
  },
});

The main difference here is navigation prop which is an object that being passed to every screen by react-navigation. It has navigate(screen: string) and goBack() functions that we’re going to use. navigate changes the current screen to the one that you pass as a parameter and goBack goes back to the previous screen.

Adding Add Coin screen

Next, let’s add a screen that will be showing a list of all available coins. We don’t have a list container set up yet, so we’ll just leave a todo note inside render() to add it later.

  • Create a new file called add.js within src/screens folder.
// @flow

import React, { Component } from 'react';
import {
  StyleSheet,
  TouchableOpacity,
  View,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import type { Navigation } from '../app';

export default class extends Component {

  props: {
    navigation: Navigation,
  };

  onClose = () => {
    const { navigation } = this.props;
    navigation.goBack();
  };

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity style={styles.button} onPress={this.onClose}>
          <Icon
            name="md-close"
            size={30}
            color="#FFFFFF"
          />
        </TouchableOpacity>
        {/* TODO: Add list here */}
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 20, // put content below status bar
    // backgroundColor: 'white',
    backgroundColor: '#673AB7',
  },
  button: {
    margin: 10,
  },
});

Updating app.js

And finally, let’s update app.js to set up navigation between the two screens that we just added.

  • Open app.js file within src folder and replace its content with the following.
import React, { Component } from 'react';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import reducer from './redux';
import List from './screens/list';
import Add from './screens/add';
import { StackNavigator } from 'react-navigation';

// Type navigation object that's being passed to every screen by react-navigation
export type Navigation = {
  navigate: (screen: string, params?: Object) => void,
  goBack: () => void,
  state: {
    routeName: string,
    params?: Object,
  }
};

const store = createStore(
  reducer,
  composeWithDevTools(applyMiddleware(thunk)),
);

// Configure navigation
const Screens = StackNavigator({
  list: { screen: List }, // list with the chart
  add: { screen: Add },   // add new coin screen
}, {
  mode: 'modal',      // Add screen slides up from the bottom
  headerMode: 'none', // disable headers
});

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Screens />
      </Provider>
    );
  }
}

Adding the button

Now, let’s add that button and make it go to the modal screen.

Creating presentational component

First, let’s create a presentational component that renders the button itself.

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

import React, { Component } from 'react';
import {
  Dimensions,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';

// Get screen width
const { width } = Dimensions.get('window');

export default class Add extends Component {

  props: {
    onPress: () => void,
  };

  render() {
    const { onPress } = this.props;
    return (
      <View style={styles.container}>
        <TouchableOpacity style={styles.button} onPress={onPress}>
          <Icon name={'md-add-circle'} style={styles.icon} />
          <Text>{' '}</Text>
          <Text style={styles.text}>Add coin</Text>
        </TouchableOpacity>
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center', // center horizontally
    width: width,         // take the screen width
    marginTop: 15,
  },
  button: {
    flexDirection: 'row', // arrange icon and text in a row
    alignItems: 'center', // center vertically
    padding: 15,
  },
  icon: {
    fontSize: 24,
    color: '#FFFFFF',
  },
  text: {
    color: '#FFFFFF',
    fontFamily: 'Avenir',
    fontSize: 16,
  },
});

Updating List container

Next, let’s update List container and add the button we created in the previous step.

  • Open list.js file within src/containers folder to make a few changes.
  • Import Add button component we created in the previous step.
// ... existing imports
import Add from '../components/coin/add';
  • Pull onAddCoin that we’re passing to the container from List screen out of this.props.
  // ...
  render() {
    const {
      current,
      list,
      loading,
      selectCoin,
      updatePrices,
      onAddCoin, // add this
    } = this.props;
  // ...
  • Add <Add onPress={onAddCoin} /> JSX before closing </ScrollView> to render the button.
		      // ...
		  })}
          <Add onPress={onAddCoin} /> // add this
        </ScrollView>
        // ....

Checking out the navigation

Let’s pull up the simulator and check out our modal. It looks like the navigation is working. The next step is to add that list of all available coins.

Adding Redux action

Let’s modify coins Redux config a little bit to allow adding new coins to the list.

  • Open coins.js file within src/redux folder to make a few changes.
  • Add new action type ADDED_COIN.
// ...

type Action =
  | { type: 'LOADING_PRICES' }
  | { type: 'SELECTED_COIN', symbol: string }
  | { type: 'ADDED_COIN', symbol: string, name: string } // add this
  | { type: 'UPDATED_24H_AGO_PRICE', symbol: string, price: number }
  | { type: 'UPDATED_PRICES',
      response: {
        [symbol: string]: {
          ['USD' | 'EUR' | 'BTC']: number
        }
      }
    };

// ...
  • Add new action called addCoin before existing selectCoin action.
// ...

// Add a coin
export const addCoin = (symbol: string, name: string): ThunkAction => async dispatch => {
  dispatch({
    type: 'ADDED_COIN',
    symbol,
    name,
  });
  dispatch(updatePrices());
};

// ...
  • Add new reducer handler for ADDED_COIN action type before SELECTED_COIN inside reducer() function.
    // ...

    // Add new coin
    case 'ADDED_COIN': {
      const { symbol, name } = action;

      // Coin is already in the list
      if (state.list.find(coin => coin.symbol === symbol)) {
        return state;
      }

      return {
        ...state,
        list: [
          ...state.list,
          { symbol, name },
        ],
      };
    }

    // ...

Adding Row component

Before starting with the container, let’s build a presentational component that renders each row in the list.

This is a pretty strait-forward component that shows coin symbol and name, and calls onPress function passing coin symbol and name as arguments. We’re going to pass this function from Add container that we’ll build in the next step.

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

import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  TouchableHighlight,
  View
} from 'react-native';

export default class Row extends Component {

  props: {
    symbol: string,
    name: string,
    onPress: Function,
  };

  onPress = () => {
    const {
      symbol,
      name,
      onPress,
    } = this.props;
    onPress(symbol, name);
  };

  render() {
    const {
      symbol,
      name,
    } = this.props;
    return (
      <TouchableHighlight
        onPress={this.onPress}
        underlayColor="#7E57C2"
        style={styles.container}
      >
        <View>
          <Text style={styles.text} numberOfLines={1}>
            {symbol}
          </Text>
          <Text style={[styles.text, styles.name]} numberOfLines={1}>
            {name}
          </Text>
        </View>
      </TouchableHighlight>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    borderBottomColor: 'rgba(255,255,255,0.25)',
    borderBottomWidth: StyleSheet.hairlineWidth,
    padding: 10,
  },
  text: {
    color: '#FFFFFF',
    fontFamily: 'Avenir',
    fontSize: 16,
    fontWeight: '500',
  },
  name: {
    color: 'rgba(255,255,255,0.5)',
    fontSize: 12,
    fontWeight: '300',
  },
});

Building Add container

Almost there! The last thing we need to build is the container that queries the API to get the list of all available coins and renders those as a list using Row component we added in the previous step.

  • Create a new file called add.js within src/containers folder.
// @flow

import React, { Component } from 'react';
import {
  ListView,
  RefreshControl
} from 'react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { addCoin } from '../redux/coins';
import Row from '../components/coin/row';

// Type API response shape
type Response = {
  Data: {
    [symbol: string]: {
      CoinName: string,
      Name: string,
    },
  },
};

// Type coin object
type Coin = {
  symbol: string,
  name: string,
};

@connect(
  (state) => ({}),
  (dispatch) => bindActionCreators({ addCoin }, dispatch)
)
export default class Add extends Component {

  props: {
    onAddedCoin: () => void,
  };

  state = {
    // Used to show activity indicator when the data is being loaded
    loading: true,
    // Holds the data for ListView
    dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 })
  };

  // Load list data when component is about to get mounted
  componentWillMount() {
    this.loadListData();
  };

  // Load the data, transform it, and update the list
  loadListData = async () => {
    const response = await this.fetchCoinList();
    const coins = this.transformAPIResponse(response);
    this.updateDataSource(coins);
  };

  // Query API and return raw response
  fetchCoinList = async (): Promise<Response> => {
    const response = await fetch('https://www.cryptocompare.com/api/data/coinlist');
    return response.json();
  };

  // Transform API response into Array<{ symbol, name }> format
  transformAPIResponse = (response: Response): Array<Coin> => {
    const coins = response.Data;
    return Object.keys(coins).map(symbol => ({
      symbol: coins[symbol].Name,
      name: coins[symbol].CoinName,
    }));
  };

  // Update the state with coin data
  updateDataSource(coins: Array<Coin>) {
    this.setState({
      dataSource: this.state.dataSource.cloneWithRows(coins),
      loading: false,
    });
  }

  // Render each row using Row component
  renderRow = (coin: Coin) => {
    const { symbol, name } = coin;
    return <Row
      symbol={symbol}
      name={name}
      onPress={this.onAddCoin}
    />
  };

  // Handle row presses
  onAddCoin = (symbol: string, name: string) => {
    const { addCoin, onAddedCoin } = this.props;
    addCoin(symbol, name); // redux action
    onAddedCoin(); // closes the modal
  };

  render() {
    const { loading } = this.state;
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
        refreshControl={
          <RefreshControl
            refreshing={loading}
            onRefresh={this.loadListData}
            tintColor="#FFFFFF"
          />
        }
      />
    );
  }

}

Updating Add Coin screen

  • Open add.js file within src/screens folder to make a couple of changes.
  • Import Add container that we created in the previous step.
// ... existing imports
import Add from '../containers/add'; 
  • Replace todo note that we left earlier inside render() with <Add onAddedCoin={this.onClose} />
// ...
        </TouchableOpacity>
        <Add onAddedCoin={this.onClose} /> // add this instead of todo
// ...

Checking out the result

Let’s pull up the simulator to see how it all works together. When you tap on a coin in the list, it’s getting added to the main list, and the prices are being updated.

Wrapping up

And that’s it for the series. I hope you’ve enjoyed the process and have learned a lot! Don’t forget to subscribe to get notified about new tutorials. And if you have any questions or ideas for new tutorials, just leave a comment below the post.

Spread the Word
  • Thomas Reyskens

    Very nice artice!