all articles

welogger app development 1 - first try of react-native

2017-11-11 @sunderls

welogger react-native

I planned to develop welogger.com as a web app, but after trying doing it, I gave up, why?

  1. Current iOS standalone mode results reloading on opening, it is very bothering.
  2. Loading is really slow
  3. iOS started to support service worker, but there is a long way to go
  4. File input responds slowly when tapped. We cannnot do much after selecting photos unless we do it by ourselves. We can modify binary data but it is complicated.
  5. webview scrolling leads to problems somtimes
  6. we cannot control keyboards.
  7. confirmation dialog pops everytime location is requested
  8. cost much to teach users using it

So I decided to create welogger in a real native app, with react-native.

After trying it in one day, I got something like this

at the beginning

First we look at react-native, seem simple:

npm install -g create-react-native-app
create-react-native-app welogger
cd welogger
npm start

Open it in real device or simulator, modify App.js then the app is auto reloaded.

css adjustment

You need to get used to it, it is the same to ordinary CSS but not 100%. The most important one is flexbos, other css rules could be searched when you need it.

This is the style for top page.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    flexDirection: 'column',
    justifyContent: 'space-between',
  },
  main: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    flexDirection: 'column',
    justifyContent: 'center'
  },
  title: {
    textAlign: 'center',
    color: '#2196f3',
    fontSize: 30,
    lineHeight: 60,
    fontFamily: 'Avenir Next',
    fontWeight: 'bold'
  },
  intro: {
    textAlign: 'center',
    lineHeight: 25,
    fontSize: 16,
    color: '#555'
  },
  bottom: {
    backgroundColor: '#fff',
    justifyContent: 'center',
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingLeft: 40,
    paddingRight: 40,
    paddingBottom: 50
  }
});

react-navigation nesting

The official suggestion is react-navigation, so I tried it:

  1. Home -> display different page based on login status, request user to login when necessary
  2. Login -> show login page with modal transition.
  3. Profile -> this is common push in transition, slide in profile page from right

transition is different, so tried navigator nesting.

The outer one is modal StackNavigator, in it thre is StackNavigator.

App.js

const App = StackNavigator({
  Home: { screen: Home },
  Profile: { screen: Profile }
});

export default StackNavigator({
  App: { screen: App },
  Login: { screen: Login },
}, {
  mode: 'modal',
  headerMode: 'none'
});

Attention to the header. If we do nothing, there are two headers in home and profiel. We can close the header in Home, but we need it in profile. We can close the header in the out-most navigator by `headerMode:'none', but by this we need to create Cancel in Login. There must be a better solution, but for now we do it in the simplest fashion.

session with JWT

Welogger web version uses jwt, I used the same logic in app, so basically I don't need to do much.

  1. save jwt in AsyncStorage, this is the same as localStorage
  2. add token from AsyncStorage for each API request
  3. if there is token in API response's header, update it in AsyncStorage
  4. generate token when logging in, update AsyncStorage

Api.js

I used fetch api to do requesting.

api.js

import { t } from './Locale';
import { AsyncStorage } from 'react-native';

const API_BASE = 'xxxxx';

function handle(response) {
    const header = response.headers.get('Authorization');
    if (header) {
        const token = header.split(/\s+/)[1];
        AsyncStorage.setItem('token', token);
    }

    return response.json().then(result => {
        if (!response.ok) {
            if (typeof result.error.message === 'string') {
                alert(t(result.error.message));
            } else if (typeof result.error.message === 'object') {
                alert(Object.keys(result.error.message).map(key => {
                    let msg = result.error.message[key];
                     return Array.isArray(msg) ? msg.map(t).join('\n') : t(msg);
                }).join('\n'));
            }
            throw new Error(result.error.message || result.error); 
        }
        return result;
    });
}

const Api = {
    async get(path, params){
        const token = await AsyncStorage.getItem('token');
        return fetch(API_BASE + path, {
            mode: 'cors', 
            method: 'GET', 
            headers: {
                'Authorization': 'Bearer ' + token,
            }
        }).then(handle)
    },

    async post(path, params){
        const token = await AsyncStorage.getItem('token');
        return fetch(API_BASE + path, {
            mode: 'cors', 
            method: 'POST', 
            body: JSON.stringify(params),
            headers: {
            'Authorization': 'Bearer ' + token,
            'Content-Type': 'application/json',
            Accept: 'application/js'
            }
        }).then(handle)
    },

    delete(path, params){
        return fetch(API_BASE + path, {
            mode: 'cors', 
            method: 'DELETE', 
            body: JSON.stringify(params),
            headers: {
            'Authorization': 'Bearer ' + localStorage.getItem('token'),
            'Content-Type': 'application/json',
            Accept: 'application/js'
            }
        }).then(handle)
    }
}

export default Api;

Login

Login screen is simple, put two input & call the token genrating API and it's done.

Login.js

import API from '../lib/Api';
import { AsyncStorage } from 'react-native';

export default class Login extends React.Component {
    static navigationOptions = {
        title: 'login',
    }

    constructor(props) {
        super(props);

        this.state = {
            email: ''
        };
    }

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

    login = async () => {
        API.post('/auth', {
            email: this.state.email,
            password: this.state.password
        }).then(async res => {
            await AsyncStorage.setItem('token', res.token);
            this.props.navigation.goBack();
        });
    }

    render() {
        return (
        <View style={styles.container}>
            <View style={styles.main}>
            <TextInput
                style={styles.input}
                onChangeText={(text) => this.setState({email: text})}
                placeholder="email"
                value={this.state.email}
            />
            <TextInput
                style={styles.input}
                onChangeText={(text) => this.setState({password: text})}
                placeholder="password"
                secureTextEntry={true}
                value={this.state.password}
            />
            <Button title="LOGIN" onPress={this.login} />
            <Button title="cancel" onPress={this.cancel} />
            </View>
        </View>
        );
    }
}

Home Screen

Home screen will display loading indicator according to API requesting , and show the necessrary info by checking login state.

Home.js

export default class Home extends Screen {
  constructor(props) {
    super(props);

    this.state = {
      isLoading: true
    }
  }

  // no header in home
  static navigationOptions = {
      header: null
  }

  // go to login page
  gotoLogin = () => {
    this.props.navigation.navigate('Login');
  }

  // go to profile page, pass name
  gotoProfile = () => {
    this.props.navigation.navigate('Profile', { name: this.state.user.name})
  }

  // delete token when log out
  logout = async () => {
    await AsyncStorage.removeItem('token');
    this.setState({
      user: null
    });
  }

  componentDidMount() {
    super.componentDidMount();

    this.getData().then(this.update)
  }

  getData() {
    return API.get('/init').catch(e => {
      this.setState({
        user: null
      });
    });
  }

  update = (data) => {
    this.setState(Object.assign({}, {
      isLoading: false
    }, data));
  }

  // when come back from login page
  // this hook is created by myself, will explain later
  componentDidFocus() {
    this.getData().then(this.update)
  }

  render() {
    const state = this.state;
    return (
      <View style={styles.container}>
        <View style={styles.main}>{
          state.isLoading ? <Text style={styles.intro}>loading</Text>
            : state.user ? <Button title={state.user.name} onPress={this.gotoProfile} />
            : <Text style={styles.intro}>welcome</Text>
        }
        </View>

        <View style={styles.bottom}>
          <Button title="SIGNUP" onPress={this.signup}/>
          <Button title="LOGOUT" onPress={this.logout}/>
          <Button title="LOGIN" onPress={this.gotoLogin}/>
        </View>
      </View>
    );
  }
}

profile screen

we can get data trom this.props.navigation.state.params

Profile.js

export default class Profile extends React.Component {
    static navigationOptions = {
        title: 'Profile',
    }

    constructor(props) {
        super(props);

        this.state = {
            name: ''
        };
    }

    render() {
        return (
        <View style={styles.container}>
            <View style={styles.main}>
            <Text> This is a profile page for {this.props.navigation.state.params.name}</Text>
            </View>
        </View>
        );
    }
}

componentDidFocus

when logge in , users is redirected to the previous screen, but how to do that? I didn't find a good way, looks like people are disucssing about it. Peronally, the status of "front-most", "rare-most" information is necessary.componentDidTop() and componentDidHide() seems promising, which I explained it in LINE Manga:smooth transition with Page Stack. react-navigation supports onTransitioStart and onTransitionEnd, maybe I could get data here, and use emitter to notify.

In Screen.js, screen.focus is listend. If id is the same, it means it is focused again, then componentDidFocus could be triggered.

Screen.js

export default class Screen extends React.Component {
    constructor(props) {
        super(props);
    }

    componentDidMount() {
        emitter.addListener('screen.focus', (data) => {
            console.log('screen.focus', data.key, `scene_${this.props.navigation.state.key}`);
            if (data.key === `scene_${this.props.navigation.state.key}`) {
                this.isFocusing = true;
                if (this.componentDidFocus) {
                    this.componentDidFocus();
                }
            }
        })
    }
}

emit screen.focus in navigator.

App.js

export default StackNavigator({
  App: { screen: App },
  Login: { screen: Login },
}, {
  mode: 'modal',
  headerMode: 'none',
  onTransitionStart: (target, current) => {
    if (target.index < current.index) {

      emitter.emit('screen.focus', {
        key: target.scene.key.replace(/(.*-)(\d+)/, '$1' + target.index)
      });
    }
  }
});

This looks really bad, but it works. Better options will be found later.

at last

The very simple welogger app is done. From now I need to do:

  1. i18n
  2. features on pages
  3. geolocation
  4. camera
  5. package