世界一シンプルなReact-native + Reduxチュートリアル②

シンプルなTodoアプリのstate管理をreduxへと移します。
reduxではまりがちなArrayを使ったいいサンプルだと思います。
なぜならreduxではmutativeなメソッドであるpush, spliceなどの配列操作のメソッドは非推奨です。
ES6のスプレッド演算子かsliceメソッドを使いましょう。

それでは以下のtodoアプリをgit cloneしてください。
https://github.com/brunolsantos/React-Native-ToDoApp
run-iosして動くことを確認しましょう。
以下のような画面が現れます。
スクリーンショット 2018-04-17 午前0.01.45.png

さあそれではこのシンプルなアプリにreduxを入れていきましょう。
まずはrootフォルダで
yarn add redux react-redux
reduxとreactを一緒に使うにはreact-reduxが必要です。

必要なフォルダ、ファイルを作りましょう。
以下のようにフォルダとファイルを作り直してください。
スクリーンショット 2018-04-15 午後9.58.38.png

1. rootReducerを作ります。

rootReducerはreduxのAPIのcombineReducersで全てのreducerを一つにまとめ扱いやすくしたreducerです。

// src/rootReducer
import { combineReducers } from "redux";
import HomeReducer from "./Home/reducer";

export default combineReducers({
  home: HomeReducer
  // mapStateToPropsでstate.home.fooなどと参照できる
});

2. storeを作ります。

state専用データベースのようなものです。実態は巨大なjsonです。一度、console.logして確かめてみましょう。ここにstateを集約することでスパゲッティ
を防いでいます。

// src/store.js
import { createStore } from "redux";
import rootReducer from "./rootReducer";

export const store = createStore(rootReducer);

3. Providerで配下のコンポーネントにstoreを渡します。

JSXで囲んであげましょう。

// src/App.js
import React, { Component } from "react";
import { Provider } from "react-redux";
import { store } from "./store";
import Home from "./Home/homeContainer";

const App = () => (
  <Provider store={store}>
    <Home />
  </Provider>
);

export default App;

以上で、どのアプリでも必要なreduxのセットアップを書き終えました。

次にactionを作っていきましょう。actionとはreducerで扱うデータをjsonにしたものです。reducerは純粋な関数、actionは関数に渡すオブジェクト/引数と考えてokです。
const ADD_NOTE = "ADD_NOTE"の部分はredux的に必要です。
諦めて書きましょう。

4. actionを作成する。

// src/Home/actions.js
export const ADD_NOTE = "ADD_NOTE";
export const DELETE_NOTE = "DELETE_NOTE";

export const addNote = payload => ({
  type: "ADD_NOTE",
  payload: { noteText: payload }
});

export const deleteNote = payload => ({
  type: "DELETE_NOTE",
  payload
});

5. reducerを作成する。

ここで作成したreducerをrootReducerに食わせます。)reducerは純粋な関数です。
stateを加工してstoreにて保存します。
switch文でactionの名前ごとに処理を分けます。
reducer自体に名前は必要ないので省略していますが、名前をつけることも出来ます。

// src/Home/reducer.js
const INITIAL_STATE = {
  noteArray: []
};

export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case "ADD_NOTE":
      return {
        noteArray: [...state.noteArray, action.payload]
      };
    case "DELETE_NOTE":
      return {
        ...state,
        noteArray: [...state.noteArray, action.payload]
      };
    default:
      return state;
  }
};

7. containerを作成する。containerとはreduxからstateを受け取るコンポーネントです。connect関数を利用してstoreからstateを受け取ります。

簡潔に言うと、
mapStateToProps = グローバルstate
mapDispatchToProps = dispatchする関数
です。

// src/Home/homeContainer.js
import React, { Component } from "react";
import {
  View,
  Text,
  StyleSheet,
  TextInput,
  ScrollView,
  TouchableOpacity
} from "react-native";
import { connect } from "react-redux";
import { addNote, deleteNote } from "./actions";

import Note from "./components/note";

class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {
      noteText: ""
    };
  }

  // arrayの中には、keyを保存せず、子コンポーネントに渡すときに
  // ゼロからmapでキーを生成している。
  render() {
    let notes = this.props.noteArray.map((val, key) => {
      return (
        <Note
          key={key}
          keyval={key}
          val={val}
          deleteMethod={() => this.deleteNote(key)}
        />
      );
    });
    return (
      <View style={styles.container}>
        <View style={styles.header}>
          <Text style={styles.headerText}>- NOTER -</Text>
        </View>
        <ScrollView style={styles.scrollContainer}>{notes}</ScrollView>

        <View style={styles.footer}>
          <TextInput
            style={styles.textInput}
            placeholder=">note"
            onChangeText={noteText => this.setState({ noteText })}
            value={this.state.noteText}
            placeholderTextColor="white"
            underlineColorAndroid="transparent"
          />
        </View>
        <TouchableOpacity
          onPress={this.addNote.bind(this)}
          style={styles.addButton}
        >
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>
    );
  }

  addNote() {
    if (this.state.noteText) {
      this.props.onAddNote(this.state.noteText);
      this.setState({ noteText: "" });
    }
  }

  deleteNote(key) {
    this.state.noteArray.splice(key, 1);
    this.setState({ noteArray: this.state.noteArray });
  }
}

const mapStateToProps = state => {
  return {
    noteArray: state.home.noteArray
  };
};

const mapDispatchToProps = dispatch => {
  return {
    onAddNote: text => dispatch(addNote(text)),
    onDeleteNote: () => dispatch(deleteNote())
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Home);

const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  header: {
    backgroundColor: "#E91E63",
    alignItems: "center",
    justifyContent: "center",
    borderBottomWidth: 10,
    borderBottomColor: "#ddd"
  },
  headerText: {
    color: "white",
    fontSize: 18,
    padding: 26
  },
  scrollContainer: {
    flex: 1,
    marginBottom: 100
  },
  footer: {
    position: "absolute",
    bottom: 0,
    left: 0,
    right: 0,
    zIndex: 10
  },
  textInput: {
    alignSelf: "stretch",
    color: "#fff",
    padding: 20,
    backgroundColor: "#252525",
    borderTopWidth: 2,
    borderTopColor: "#ededed"
  },
  addButton: {
    position: "absolute",
    zIndex: 11,
    right: 20,
    bottom: 90,
    backgroundColor: "#E91E63",
    width: 70,
    height: 70,
    borderRadius: 35,
    alignItems: "center",
    justifyContent: "center",
    elevation: 8
  },
  addButtonText: {
    color: "#fff",
    fontSize: 24
  }
});

9. note.jsをcomponentsの中に移動する。

// src/components/nots.js
import React, { Component } from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";

export default class Note extends Component {
  render() {
    return (
      <View key={this.props.keyval} style={styles.note}>
        <Text style={styles.noteText}>{this.props.val.noteText}</Text>

        <TouchableOpacity
          onPress={this.props.deleteMethod}
          style={styles.noteDelete}
        >
          <Text style={styles.noteDeleteText}>D</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  note: {
    position: "relative",
    padding: 20,
    paddingRight: 100,
    borderBottomWidth: 2,
    borderBottomColor: "#ededed"
  },
  noteText: {
    paddingLeft: 20,
    borderLeftWidth: 10,
    borderLeftColor: "#E91E63"
  },
  noteDelete: {
    position: "absolute",
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#2980b9",
    padding: 10,
    top: 10,
    bottom: 10,
    right: 10
  },
  noteDeleteText: {
    color: "white"
  }
});

最後に

4つもファイルが増えました。めんどくさいですね。
本当にredux必要ですか?
mobXなら30分で基本的な使い方マスターできますよ。
ただ、大規模になればなるほど、疎結合であり、スパゲッティになりにくいreduxの強みが生きてきます。
とてもよくスケールするので、他人の書いたコードを気にせずに自分のコードを書けるのでストレスが貯まりません。

reduxのことは以来になってもReactNativeのことは嫌いにならないでください。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.