Build Offline Capable React Native Apps

Basics: Catching and Displaying Network Errors

App/screens/CreateItem.js

import React from 'react';
import { ScrollView, View, Alert } from 'react-native';

import { TextField } from '../components/Form';
import { Button } from '../components/Button';
import { geoFetch } from '../util/api';

class CreateItem extends React.Component {
  state = {
    title: null,
    description: null,
    latitude: null,
    longitude: null,
    loading: false,
  };

  onCurrentLocationPress = () => {
    navigator.geolocation.getCurrentPosition(res => {
      if (res && res.coords) {
        this.setState({
          latitude: res.coords.latitude.toString(),
          longitude: res.coords.longitude.toString(),
        });
      }
    });
  };

  onSavePress = () => {
    const { title, description, latitude, longitude } = this.state;
    this.setState({ loading: true }, () => {
      geoFetch(`/`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ title, description, latitude, longitude }),
      })
        .then(() => {
          this.props.navigation.popToTop();
        })
        .catch(error => {
          console.log('create item error', error);
          Alert.alert('Sorry, something went wrong.', error.message);
        })
        .finally(() => {
          this.setState({ loading: false });
        });
    });
  };

  render() {
    return (
      <ScrollView contentContainerStyle={{ paddingVertical: 20 }}>
        <TextField
          label="Title"
          placeholder="I am what I am..."
          value={this.state.title}
          onChangeText={title => this.setState({ title })}
        />
        <TextField
          label="Description"
          placeholder="This is a description..."
          value={this.state.description}
          onChangeText={description => this.setState({ description })}
        />
        <TextField
          label="Latitude"
          placeholder="37.3861"
          keyboardType="decimal-pad"
          value={this.state.latitude}
          onChangeText={latitude => this.setState({ latitude })}
        />
        <TextField
          label="Longitude"
          placeholder="-122.0839"
          keyboardType="decimal-pad"
          value={this.state.longitude}
          onChangeText={longitude => this.setState({ longitude })}
        />
        <View style={{ alignItems: 'center' }}>
          <Button
            text="Use Current Location"
            style={{ marginBottom: 20 }}
            onPress={this.onCurrentLocationPress}
          />
          <Button
            text="Save"
            onPress={this.onSavePress}
            loading={this.state.loading}
          />
        </View>
      </ScrollView>
    );
  }
}

export default CreateItem;

App/screens/Details.js

import React from 'react';
import {
  View,
  StyleSheet,
  SafeAreaView,
  Text,
  ScrollView,
  Dimensions,
  InteractionManager,
  Alert,
} from 'react-native';
import MapView, { Marker } from 'react-native-maps';

import { Button } from '../components/Button';
import { geoFetch } from '../util/api';

const screen = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  section: {
    backgroundColor: '#fff',
    borderTopWidth: 1,
    borderTopColor: '#E4E4E4',
    borderBottomWidth: 1,
    borderBottomColor: '#E4E4E4',
    marginVertical: 20,
    padding: 14,
    alignItems: 'center',
  },
  titleText: {
    fontWeight: '600',
    fontSize: 18,
    color: '#4A4A4A',
    textAlign: 'center',
    marginBottom: 10,
  },
  text: {
    fontSize: 16,
    color: '#4A4A4A',
    marginBottom: 20,
  },
  map: {
    width: screen.width,
    height: Math.round(screen.height * 0.25),
    borderTopWidth: 1,
    borderTopColor: '#E4E4E4',
    borderBottomWidth: 1,
    borderBottomColor: '#E4E4E4',
    backgroundColor: '#fff',
  },
});

class Details extends React.Component {
  state = {
    loading: false,
    updatedItem: null,
    showMap: false,
  };

  componentDidMount() {
    InteractionManager.runAfterInteractions(() => {
      this.setState({ showMap: true });
    });
  }

  handleLogPress = _id => {
    this.setState({ loading: true }, () => {
      geoFetch(`/log-find?_id=${_id}`, { method: 'PUT' })
        .then(res => {
          this.setState({ updatedItem: res.result });
        })
        .catch(error => {
          console.log('log press error', error);
          Alert.alert('Sorry, something went wrong.', error.message);
        })
        .finally(() => {
          this.setState({ loading: false });
        });
    });
  };

  render() {
    const item = this.state.updatedItem
      ? this.state.updatedItem
      : this.props.navigation.getParam('item', {});

    return (
      <SafeAreaView style={styles.container}>
        <ScrollView>
          {this.state.showMap ? (
            <MapView
              style={styles.map}
              region={{
                latitude: item.latitude,
                longitude: item.longitude,
                latitudeDelta: 0.0922,
                longitudeDelta: 0.0421,
              }}
              zoomEnabled={false}
              scrollEnabled={false}
            >
              <Marker
                coordinate={{
                  latitude: item.latitude,
                  longitude: item.longitude,
                }}
              />
            </MapView>
          ) : (
            <View style={styles.map} />
          )}
          <View style={styles.section}>
            <Text style={styles.titleText}>{item.title}</Text>
            <Text style={styles.text}>{item.description}</Text>
            <Text style={styles.text}>
              {`Found ${item.foundCount || 0} times.`}
            </Text>
            <Button
              text="Log"
              onPress={() => this.handleLogPress(item._id)}
              loading={this.state.loading}
            />
          </View>
        </ScrollView>
      </SafeAreaView>
    );
  }
}

export default Details;

App/screens/List.js

import React from 'react';
import { ActivityIndicator, Alert } from 'react-native';

import { List, ListItem } from '../components/List';
import { geoFetch } from '../util/api';

class ListScreen extends React.Component {
  state = {
    loading: true,
    list: [],
    refreshing: false,
  };

  componentDidMount() {
    this.getData();
  }

  getData = () =>
    geoFetch('/list')
      .then(response => {
        this.setState({
          loading: false,
          refreshing: false,
          list: response.result,
        });
      })
      .catch(error => {
        console.log('list error', error);
        Alert.alert(
          'Sorry, something went wrong. Please try again',
          error.message,
          [
            {
              text: 'Try Again',
              onPress: this.getData,
            },
          ]
        );
      });

  handleRefresh = () => {
    this.setState({ refreshing: true });
    this.getData();
  };

  render() {
    if (this.state.loading) {
      return <ActivityIndicator size="large" />;
    }

    return (
      <List
        data={this.state.list}
        renderItem={({ item, index }) => (
          <ListItem
            title={item.title}
            isOdd={index % 2}
            onPress={() => this.props.navigation.navigate('Details', { item })}
          />
        )}
        onRefresh={this.handleRefresh}
        refreshing={this.state.refreshing}
      />
    );
  }
}

export default ListScreen;

Want to track your progress? Create an account with React Native School!

Continue

Join the email list to be notified of all new lessons and classes!