Expo+Redux(+firebase)でログインフォーム② 〜Reduxの導入〜


はじめに

Expo+Redux(+firebase)でログインフォーム① 〜概要・Expoの準備〜の続きです。

今回の記事では、Reduxを使って、まずは簡単なカウンターを作っていきます。
次々回の記事で、ログイン画面と画面遷移を実装していく予定です。

現在のディレクトリ構成

root/
 ├ App.js
 ├ package.json

srcディレクトリの作成

rootに色々と詰め込んでいくとわかりにくくなるので、srcディレクトリを作ってAppを移しましょう
Expoはデフォルトでroot/App.jsを読み込むようになっているので、これを変更するために、node_modules/expo/AppEntry.jsを編集します

/node_modules/expo/AppEntry.js
-- import App from '../../App';
++ import App from '../../src/App';

そしてroot/App.jsを削除し、root/src/App.tsxを書いていきます。

Reduxを導入

まずはApp.tsxにreduxを入れていきます
とりあえず最小の構成で、1ファイルにまとめています

src/App.tsx
import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import { createStore, combineReducers } from 'redux'
import { Provider, connect } from 'react-redux'

// reducer
function counter(state, action) {
  if (action.type === 'undefined') {
    return null
  }

  switch(action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return null
  }
}

// store
const store = createStore(combineReducers({ count: counter }))

// component
function Counter({ count, dispatch }) {
  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>{count}</Text>
      <Button
        title='Increment'
        onPress={() => dispatch({ type: 'INCREMENT' })}
      />
      <Button
        title='DECREMENT'
        onPress={() => dispatch({ type: 'DECREMENT' })}
      />
    </View>
  )
}

// container
const CounterContainer = connect(state => ({ count: state.count }))(Counter)

export default function App() {
  return (
    <Provider store={store}>
      <CounterContainer />
    </Provider>
  )
}

// スタイル
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
  paragraph: {
    margin: 24,
    fontSize: 18,
    fontWeight: 'bold',
    textAlign: 'center',
  },
})

Reducer

App.tsx
// reducer
function counter(state, action) {
  if (action.type === 'undefined') {
    return null
  }

  switch(action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return null
  }
}

actionと現在のstateを受け取って、新しいstateを返す関数です。
actionがない時や、swich文のdefaultでnullを返すようにしないとエラーになるので、注意が必要です。

Store

root/App.tsx
// store
const store = createStore(combineReducers({ count: counter }))

Reducerが今後増えることを踏まえて、combineReducerでまとめておきます。

Component/Container

App.tsx
// component
function Counter({ count, dispatch }) {
  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>{count}</Text>
      <Button
        title='Increment'
        onPress={() => dispatch({ type: 'INCREMENT' })}
      />
      <Button
        title='DECREMENT'
        onPress={() => dispatch({ type: 'DECREMENT' })}
      />
    </View>
  )
}

// container
const CounterContainer = connect(state => ({ count: state.count }))(Counter)

画面に表示する部分です。
ボタンを押すと、typeがINCREMENT/DECREMENTのactionがdispatchされます。
ボタンを押した時のコールバック関数は、本来はContainer側に書くべきだとは思いますが、コードが少しややこしくなるので、それは後にしようと思います。

App

App.tsx
export default function App() {
  return (
    <Provider store={store}>
      <CounterContainer />
    </Provider>
  )
}

ReactNativeは、で囲ってstoreを渡してやれば、Reduxと接続することができます。

ここまでで実行するとこんな感じ

次回

今回は最低構成のreduxを組んでみたので、次回はDucksパターンを意識してファイルを分けていきたいと思います。
その後、ReactNativeDebuggerも使えるようにしていきます。
Expo+Redux(+firebase)でログインフォーム③ 〜ファイル整理・Debugger〜