Wizardry

Deconstructing React Native Apps - Metro tranvia mendoza - Part 1: Features

January 12, 2018

Julian

Written by Julian, a Javascript spellslinger.

Cast a Twitter follow

Cover

This is an analysis of how I built a React Native app. The first part, which you are reading right now, I will deconstruct its features; the second part will analyse how it manages state, and the last part will focus on animations.

The app we’ll analyze is one I made to answer a simple question, “how much time is left until the next light rail comes?“.

Everyone who lives in Mendoza, my city, needs this information every time they take the light rail. There is an official schedule in pdf, but it is really hard to read its time tables on a mobile phone. It would be convenient to access this information from the home screen of a phone, but it is not always possible to create a direct access of a pdf. I wanted a simpler way to know when does the next train comes, so I built this app.

You can see the final result in here:

You can also check out the final code in github. If you’re ever in Mendoza, you can download the app in here.

Overview

To get to know all components involved, we’ll list the app’s requirements and explain how each component contributes to that functionality.

In part twowe will analyze how domain specific state and view state are organized inside the app.

In the third part we will take a look at the animations that improve the user experience.

Requirements

This is the list of things the app does:

Show minutes remaining until next train
Show a list of all times
Show current station name
Switch current station
Change current time period

I think the best way to explain the responsibilities of each component is by describing how each component contributes to each feature.

Show minutes remaining until next train

<App /> stores current time in its state and sets up an interval on componentDidMount that updates it every minute. In order to keep minutes left synchronized with the real time, it does not start the interval immediately, it starts it when the next minute starts.

class App extends Component {
  state = {
    ...
    currentTime: new Date(),
    ...
  };
  componentDidMount() {
    ...
    // When the next minute starts
    this.timerTimeout = setTimeout(() => {
      this.setState({ currentTime: new Date() });
      // Update time every one minute
      this.timerInterval = setInterval(
        () => this.setState({ currentTime: new Date() }),
        60 * 1000
      );
    }, millisecondsUntilNextMinute);
  }
  ...
}

<App /> gets next train time from times.json by finding the first element that is greater than the current time

class App extends Component {
  ...
  render() {
    const leftStation =
      stations[stations.indexOf(this.state.currentStation) - 1];
      ...
    const nextLeftTrainTime =
      leftStation &&
      times[this.state.timePeriod][
        this.state.currentStation
      ].gutierrezMendozaDirection.find(time => {
        const [hours, minutes] = time.split(":");
        return new Date().setHours(hours, minutes) > this.state.currentTime;
      });
      ...
  }
}

<App /> calculates minutes remaining and passes it as prop to <TrainScene />

class App extends Component {
  ...
  render() {
    ...
    return (
      <View style={styles.container}>
        <TrainScene
          minutesLeft={
            nextLeftTrainTime &&
            `${differenceInMinutes(
              new Date().setHours(
                nextLeftTrainTime.split(":")[0],
                nextLeftTrainTime.split(":")[1]
              ),
              this.state.currentTime
            )}'`
          }
        />
        ...
    );
  }
}

Show a list of all times

<App /> holds current station in its state, and it passes it to <TrainScene />. currentStation is a string which gets initialized as "Pedro Molina".

<TrainScene /> gets times of current station from times.json and displays them in a <ScrollView />.

class TrainScene extends Component {
  ...
  render() {
    ...
    <ScrollView
      ref={scrollView => {
        this.scrollView = scrollView;
      }}
      style={{ flex: 1 }}
      contentContainerStyle={{
        alignItems: "center",
        justifyContent: "center"
      }}
    >
      {this.state.timeList.map(time => (
        <Text
          key={time}
          style={{
            height: 30,
            color: time === this.props.nextTrainTime ? red : black
          }}
        >
          {time}
        </Text>
      ))}
    </ScrollView>
    ...
  }
}

As you can see in the previous snippet, it maps over state.timeList, not over props.timeList. It stores them in its state in order to only show them after the transition animation finishes. We’ll see how to do that when we talk about animations in the app.

Show current station name

<App /> holds current station in its state and passes it as a prop to <Sign />. <Sign /> just displays current station.

class App extends Component {
  state = {
    currentStation: "PEDRO MOLINA",
    ...
  };
  ...
  render() {
    return (
      ...
      <Sign currentStop={this.state.currentStation}>
      ...
    )
  }
}

Switch current station

<App /> Has setNextStation and setPreviousStation functions. They fetch current station from stations array, modify currentStation in their state, and also store that value in AsyncStorage. It finally passes those functions to <Sign /> as onLeftTap and onRightTap.

const stations = Object.keys(times.weekdays);

...

class App extends Component {
  ...
  setNextStation = gestureState => {
    const currentStationIndex = stations.indexOf(this.state.currentStation);
    const isFirst = currentStationIndex === 0;
    if (isFirst) {
      return;
    }
    const currentStation = stations[currentStationIndex - 1];
    this.setState({
      currentStation
    });
    AsyncStorage.setItem(
      "METROTRANVIA_MENDOZA_CURRENT_STATION",
      currentStation
    );
  };
  setPreviousStation = gestureState => {
    ...
  }
  ...
  render() {
    return (
      <Sign
          onLeftTap={this.setNextStation}
          onRightTap={this.setPreviousStation}
      />
    )
  }
}

Change current time period

There are three possible time periods: Monday to Friday, Saturdays and Sundays. The user can switch between them by tapping the box with the current time period.

<App /> Holds current time period in its state. <Sign /> Receives a function that sets time period in state as its onTimePeriodChange prop. It fires that property using a <TouchableWithoutFeedback /> component because we manage the feedback using animations, we don’t want the native feedback in here.

class App extends Component {
  state = {
    ...
    timePeriod: "weekdays"
  };
  ...
  render() {
    return (
      <View style={styles.container}>
        ...
        <Sign
          timePeriod={this.state.timePeriod}
          onTimePeriodChange={timePeriod => this.setState({ timePeriod })}
        />
        ...
    );
  }
}
class Sign extends Component {
  ...
  render() {
    return (
      <View style={styles.sign}>
        <TouchableWithoutFeedback
          onPress={() =>
            this.props.onTimePeriodChange(
              toggleTimePeriod(this.state.timePeriod)
            )
          }
        >
          <Animated.View
            style={{...}}
          >
            <Animated.Text style={{...}}>
              {getTimePeriodName(this.state.timePeriod)}
            </Animated.Text>
          </Animated.View>
        </TouchableWithoutFeedback>
        ...
      </View>
    );
  }
}

Conclusion

We learned about the app’s requirements, and how each component collaborates in order to achieve every feature.

Now that you know how everything works under the hood, you should try the final version again. Play with the sandbox, modify values, change colors, switch components. Have fun breaking stuff! That’s the best way to learn.

You can also check out the final code in github. If you’re ever in Mendoza, you can download the app in here.

Next part will be a deeper dive into how the app manages its state, which it separates between domain specific state and view specific state.