by

This is part 2 in the series. Check out Part 1 if you haven’t already. In the previous part, we got started with the app, installed all required dependencies and built chart component.

What we will be building

In this part, we’re going to build the list of coins. We’ll create some presentational components to render the coins, set up Redux store and make use of API to get coin prices and price changes for the list.

The goal of the series is to build the app that looks like this.

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

Previous part code

We’ll continue building upon the code we wrote in the previous part. If you haven’t followed the previous part 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-1 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 Scrollable List part of the app.

Coin component

Let’s start with the Coin component which shows each coin symbol, name, price, and 24 hour price change in the list.

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

We’ll break it down into two components:

  • Change. Renders that little green badge that shows 24 hour price change.
  • Coin. Shows coin symbol, name, price and the price change using Change component.

Change component

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

import React, { PureComponent } from 'react';
import {
  StyleSheet,
  Text,
  View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';

const getBadgeStyles = (value: number): Array<any> => [
  styles.badge,
  value < 0 ? styles.bad : (value > 0 ? styles.good : styles.same)
];

export default class Change extends PureComponent {

  props: {
    value: ?number,
  };

  render() {
    const { value: input } = this.props;
    const value: number = input || 0; // convert to number
    const icon = value === 0 || !value
      ? <Text>{' '}</Text>
      : <Icon name={value < 0 ? 'caret-down' : 'caret-up'} style={styles.icon} />;
    return (
      <View style={getBadgeStyles(value)}>
        {icon}
        <Text style={styles.value} numberOfLines={1}>
          {Math.abs(value)}%
        </Text>
      </View>
    );
  }

}

const styles = StyleSheet.create({
  badge: {
    borderRadius: 20,
    flexDirection: 'row',
    alignItems: 'center',
    paddingLeft: 5,
    paddingRight: 5,
    minHeight: 16,
  },
  good: { backgroundColor: '#4CAF50' },
  bad: { backgroundColor: '#FF5505' },
  same: { backgroundColor: '#F5A623' },
  value: {
    color: '#FFFFFF',
    fontFamily: 'Avenir',
    fontSize: 11,
    marginTop: 0,
  },
  icon: {
    fontSize: 18,
    lineHeight: 16,
    marginRight: 2,
    color: '#FFFFFF',
    backgroundColor: 'transparent',
    textAlign: 'center',
  },
});

Coin component

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

import React, { Component } from 'react';
import {
  Dimensions,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import numeral from 'numeral';
import Change from './change';

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

export default class Coin extends Component {

  props: {
    symbol: string,
    name: string,
    price: number,
    change: number,
    active: boolean,
    onPress: Function,
  };

  static defaultProps = {
    active: false,
  };

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

  render() {
    const {
      symbol,
      name,
      price,
      change,
      active,
    } = this.props;
    return (
      <TouchableOpacity
        style={[styles.container, active ? styles.active : {}]}
        onPress={this.onPress}
      >
        <View style={styles.row}>
          <Text style={styles.text} numberOfLines={1}>
            {symbol}
          </Text>
          <View style={styles.right}>
            <Text style={styles.text} numberOfLines={1}>
              {price ? numeral(price).format('$0,0[.]0[0000]') : '—'}
            </Text>
          </View>
        </View>

        <View style={styles.row}>
          <Text style={[styles.text, styles.name]} numberOfLines={1}>
            {name}
          </Text>
          <View style={styles.right}>
            {change !== undefined && <Change value={change} />}
          </View>
        </View>
      </TouchableOpacity>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    borderRadius: 10,
    padding: 10,
    width: (width - 20) / 2, // take half of the screen width minus margin
  },
  active: {
    backgroundColor: 'rgba(255,255,255,0.05)',  // highlight selected coin
  },
  row: {
    flexDirection: 'row',
  },
  right: {
    flex: 1,
    alignSelf: 'flex-end',
    alignItems: 'flex-end',
  },
  text: {
    color: '#FFFFFF',
    fontFamily: 'Avenir',
    fontSize: 16,
    fontWeight: '500',
  },
  name: {
    color: 'rgba(255,255,255,0.5)',
    fontSize: 12,
    fontWeight: '300',
  },
});

List container

Now, let’s update List component.

  • Open list.js file within src/containers folder.
  • Add import statement for Coin component that we just created:
// existing imports
import Coin from '../components/coin/coin';
  • Add <Coin /> with some hardcoded data inside render() for now:
  render() {
    return (
      <View style={styles.container}>
        <Coin
          symbol="BTC"
          name="Bitcoin"
          price={2500}
          change={5}
          active={false}
          onPress={() => alert('Selected!')}
        />
      </View>
    );
  }

And bring up the simulator to see how Coin component looks like.

So far looks good. In the next steps let’s set up Redux actions and state to fetch coin prices through CryptoCompare API.

API helper

Before diving into Redux, let’s create a helper function to help us make API calls.

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

const API = 'https://min-api.cryptocompare.com';

const headers = {
  'Accept': 'application/json',
  'Content-Type': 'application/json',
  'X-Requested-With': 'XMLHttpRequest'
};

export const get = async (uri: string): Promise<Object> => {
  const response = await fetch(`${API}/${uri}`, {
    method: 'GET',
    headers,
  });
  return response.json();
};

Redux store

We’re going to be using Redux to store all of the coin and chart data. We’ll create two separate reducers for coins and chart and combine them into a single one using combineReducers from redux package.

Coins Redux state

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

import { get } from '../api';

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

// Type global Redux store since we're going to have multiple reducers
// and getState() returns the whole store, not just the local state
type Store = {
  +coins: State,
};

// Type the state created by reducer in this file
export type State = {
  +loading: boolean,
  +current: string,
  +list: Array<Coin>,
};

// Type coin object
type Coin = {
  symbol: string,
  name: string,
  price?: number,
  price24hAgo?: number,
  priceChange?: number,
};

// Type all actions along with the data that gets passed
type Action =
  | { type: 'LOADING_PRICES' }
  | { type: 'SELECTED_COIN', symbol: string }
  | { type: 'UPDATED_24H_AGO_PRICE', symbol: string, price: number }
  | { type: 'UPDATED_PRICES',
      response: {
        [symbol: string]: {
          ['USD' | 'EUR' | 'BTC']: number
        }
      }
    };

// Type Redux actions and functions
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 flag to show activity indicator when prices are being updated
  loading: true,
  // Preselect BTC coin
  current: 'BTC',
  // Predefine a list of coins
  list: [
    { symbol: 'BTC', name: 'Bitcoin' },
    { symbol: 'ETH', name: 'Ethereum' },
    { symbol: 'XRP', name: 'Ripple' },
    { symbol: 'LTC', name: 'Litecoin' },
    { symbol: 'ETC', name: 'Ethereum Classic' },
    { symbol: 'DASH',name: 'DigitalCash' },
    { symbol: 'XEM', name: 'NEM' },
    { symbol: 'XMR', name: 'Monero' },
  ],
};

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

// Select a coin
export const selectCoin = (symbol: string): Action => ({
  type: 'SELECTED_COIN',
  symbol,
});

// Update prices for all coins
export const updatePrices = (): ThunkAction => async (dispatch, getState) => {
  dispatch({ type: 'LOADING_PRICES' });
  const {
    coins: {
      list
    }
  } = getState();
  const symbols = coinsToCommaSeparatedSymbolList(list);
  const response = await get(`data/pricemulti?fsyms=${symbols}&tsyms=USD`);
  dispatch({
    type: 'UPDATED_PRICES',
    response,
  });
  // Update 24 hour ago prices for each coin
  list.forEach(coin =>
    dispatch(fetch24hAgoPrice(coin.symbol))
  );
};

// Fetch 24 hour ago price for a coin
export const fetch24hAgoPrice = (symbol: string): ThunkAction => async dispatch => {
  const timestamp = Math.round(new Date().getTime() / 1000);
  const timestampYesterday = timestamp - (24 * 3600);
  const response = await get(`data/histohour?fsym=${symbol}&tsym=USD&limit=1&toTs=${timestampYesterday}`);
  if (response.Data.length > 0) {
    // Get last entry's close price in Data array since the API returns two entries
    const price = response.Data.slice(-1).pop().close;
    dispatch({
      type: 'UPDATED_24H_AGO_PRICE',
      symbol,
      price,
    });
  }
};

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

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

    // Update loading state
    case 'LOADING_PRICES': {
      return {
        ...state,
        loading: true,
      };
    }

    // Update all coin prices
    case 'UPDATED_PRICES': {
      const { list } = state;
      const { response } = action;
      return {
        ...state,
        // map through each coin
        list: list.map(coin => ({
          ...coin,
          // to update the prices
          price: response[coin.symbol] ? response[coin.symbol].USD : undefined,
        })),
        loading: false,
      };
    }

    // Update 24 hour ago price for a specific coin
    case 'UPDATED_24H_AGO_PRICE': {
      const { list } = state;
      const { symbol, price } = action;
      return {
        ...state,
        // map through each coin
        list: list.map(coin => ({
          ...coin,
          // to find the one that matches
          ...(coin.symbol === symbol ? {
            // and update it's 24 hour ago price
            price24hAgo: price,
            // and the change compared to current price
            priceChange: coin.price ? getPriceChange(coin.price, price) : undefined,
          } : {
            // don't change any values if didn't match
          })
        })),
      };
    }

    // Change current coin
    case 'SELECTED_COIN': {
      const { symbol } = action;
      return {
        ...state,
        current: symbol,
      };
    }

    default: {
      return state;
    }
  }
}

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

// Convert coin array to a comma separated symbol list for the API calls
const coinsToCommaSeparatedSymbolList = (list: Array<Coin>): string => list
  .map(item => item.symbol)
  .reduce((prev, current) => prev + ',' + current);

// Get percentage change between current and previous prices
const getPriceChange = (currentPrice: number, previousPrice: number): number => {
  if (!previousPrice) {
    return 0;
  }
  return parseFloat((((currentPrice / previousPrice) - 1) * 100).toFixed(2));
};

Combining reducers

Right now we only have one reducer, but we know that we’re going to have one more for chart data. So. let’s use combineReducers to export our existing reducer and easily add a new one later.

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

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

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

export default combineReducers({
  coins,
});

Updating coins types

Since we typed the global Redux store within index.js file, we can import it and remove the existing Store type that we have within coins.js file. This way we can update global store type within index.js file once we add another reducer and all other reducers will get the updated type.

  • Open up coins.js file within src/redux folder.
  • Import Store type from index.js we just created.
import type { Store } from './index';
  • And remove existing Store type definition.

Setting up Redux store

It looks like we’re ready to hook up our Redux store into the app.

  • Open up app.js file within src folder to make a few changes.
  • Import Redux modules, initialize a store and wrap existing <View /> container in <Provider /> to make Redux store available to all children components.
// ... existing imports
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';

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

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <View style={styles.container}>
          <Chart />
          <Ranges />
          <List />
        </View>
      </Provider>
    );
  }
}

// ... existing styles

Connecting List container to Redux store

Next, let’s connect our List container to the Redux store to show the coins from the store in a scrollable list using ScrollView instead of the one that we hardcoded. And we also want to dispatch updatePrices() action when the container is loaded to load the prices initially and when the list is being pulled down to update the prices.

  • Open up list.js file within src/containers folder and update its code with the following.
import React, { Component } from 'react';
import {
  RefreshControl,
  ScrollView,
  StyleSheet,
  View
} from 'react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { updatePrices, selectCoin } from '../redux/coins';
import Coin from '../components/coin/coin';

@connect(
  (state) => {
    const {
      // pull some data out of coins reducer
      coins: {
        current,  // currently selected coin
        list,     // list of all coins
        loading,  // whether prices are being updated
      }
    } = state;
    return {
      current,
      list,
      loading,
    };
  },
  (dispatch) => bindActionCreators({ updatePrices, selectCoin }, dispatch)
)
export default class List extends Component {

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

  render() {
    const {
      current,
      list,
      loading,
      selectCoin,
      updatePrices,
    } = this.props;
    return (
      <View style={styles.container}>
        <ScrollView
          contentContainerStyle={styles.list}
          // hide all scroll indicators
          showsHorizontalScrollIndicator={false}
          showsVerticalScrollIndicator={false}
          // enable pull-to-refresh
          refreshControl={
            <RefreshControl
              refreshing={loading}     // show activity indicator while updating prices
              onRefresh={updatePrices} // update prices when list pulled
              tintColor="#FFFFFF"
            />
          }
        >
          {list.map((coin, index) => {
            const {
              symbol,
              name,
              price,
              priceChange,
            } = coin;
            return <Coin
              symbol={symbol}
              name={name}
              price={price}
              change={priceChange}
              active={current === symbol}
              onPress={selectCoin}
              key={index}
            />;
          })}
        </ScrollView>
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 62,                   // take 62% of the screen height
    backgroundColor: '#673AB7',
  },
  list: {
    flexDirection: 'row',       // arrange coins in rows
    flexWrap: 'wrap',           // allow multiple rows
    paddingHorizontal: 10,
  },
});

And bring up the simulator to see how it looks.

Debugging Redux

Let’s try React Native Debugger for debugging Redux. It shows all Redux actions in the order they being dispatched and how each of them changes the state. You can even go back in time and see how the app looked like before certain actions were dispatched.

To enable debugger, launch React Native Debugger, press D in the simulator and select Debug JS Remotely.

Check out the previous post in case you missed it and didn’t install React Native Debugger.

Wrapping up

And that’s it for the part 2. Stay tuned for the next, final part in the series where we finish building a full featured app.

Recommended Reading

Spread the Word