Web初心者でもモダンなReact, Redux, FirebaseでTo-Doを作る2 ~Redux~

Web初心者でもモダンなReact, Redux, FirebaseでTo-Doを作る2 ~Redux~:

本記事はWeb初心者でもReact, Redux, FirebaseでTo-Doシリーズ2記事目です

本シリーズの目標は、

を使ってちょっとリッチなTo-Doを作ることです。

完成版はこちらです

  1. Reactのスケルトン作り
  2. Reduxのデータフロー導入←イマここ
  3. Firebaseを使ってデータベースと認証
  4. Travis, Jestでテスト, CI環境を作る


はじめに

DJへの敬意が止まない学生の浅野光平です。

今回はReduxを前回作ったTo-Do

に組み込んでいこうと思います。流行りに乗った若い僕たちは、関数型なReduxの理解にかなりの時間を費やしました。

前回最後に貼ったRedux作者の記事で書いてあるようにReduxは、Boiler Plate(書かなければいけないテンプレート文)が増え、ファイルの数も増えます。

Stateを直接変更できず、ActionCreator, Reducerに分けて書かねばなりません。そしてStateを使いたい時はProvider, ConnectでComponentに渡し、変更したい時はデータをDispatchせねばなりません。

前回はComponentのStateをメンバ関数のsetState()を使ってできていたことをしようとするだけなのに語彙が増えて横文字だらけでいやになりますね。

DJが好きな僕は英語も好きなのでノリノリで英語のドキュメントに何日かかけて馴染みましたが、案の定他の開発メンバーはあまり理解できないし、僕も教えられるほどの体系的理解もなかなかできず「Reduxは大損害」と言う人もいたほどでした。

メリットとしてはGlobalなStateが厳格に定義されるのでコードの一貫性が生まれ、LogicとUIのコードの分離ができるので、チーム開発をする際などは確かに役にたつこともあるのでしょう。

Reduxの導入については要検討ですが、関数型の概念自体は流行っているし、やはりTutorialとしてやるなら小さいアプリなので導入してみましょう


Reduxのイメージ

Reduxは、JSアプリの状態管理フレームワークです。

FaceBook製ではないです。FaceBookのState管理問題への対処はFluxでしたが、そのフローを模倣したReduxがそれを抜きん出た人気を見せて、流行っているんだそうです。

HaskellにInspireされた関数型言語elmにInspireされているそうなので関数型です。

Reduxは三つのPrinciplesを持っています。関数型の焼き直し的な感じです。僕的にはあんまり実装につながらないし、区分も好きじゃないです。


Single Source of truth

これは、Applicationの状態は単一のStore(State)で管理しようというものです。

Reactの中ではComponent単位で定義されていたState(react公式)は、今回はAppに対して唯一のGlobalなStore(redux公式)のStateという文脈で使います。


State is read-only

Actionとかの手順を踏んで変更しようねということと下のとつながりますが、破壊しちゃだめってことだと思います。


Changes are made with pure functions

reducerを見るとわかりますが、引数はこわしちゃだめです。履歴を作ったりするために新しいものを作るだけにしておきましょう。

この記事あたりがいいねも多いからきっとわかりやすいのでしょう。(真面目に読んでいません)


0. Redux のフロー, Install


Reduxのながれ参考図



Redux-Flow.gif


こんなGIF画像があったので拾ってきました。

この絵と公式ドキュメントで誤解を産みそうな点は、

- ActionsAction Creatorという関数のあつまり(その返り値をActionとかいう)ということと

- DispatcherはReduxには存在しない(って公式にありますFluxにはあるらしいですが知りません。)ということです。DispatchがStoreクラスのメンバ関数として定義されています。


Install

さて実際に手を動かします。

まずはPackage Installから

ReduxはReact専用フレームワークではなくJSパッケージです。Reactで使う場合はReact公式のBindingであるreact-reduxを使うのが定石のようです。

npm install redux react-redux --save 
js packageは上記のようなコマンドで追加します。saveオプションをするとpackage.jsonにバージョンとpackage名が追加され、Githubでレポジトリを共有した際、多環境でも

npm i 
するだけでpackage.jsonに記入されているpackageがローカルにInstallされて環境が整うようになります


1. Reducer, Store の定義


Desigining the State Shape

まずはアプリで単一に統制されるべきStateを定義しましょう。

公式チュートリアルではActionsの説明から入ってますが、公式Reduxでアプリを作るならReducerから決めようねと言っているのでReducerを描いてStoreを作るところからはじめます。

公式でははじめからToDoのVisibilityFilterを定義していますが、それはのちほど追加します。

ReduxのStateはKey:Valueの形のJSオブジェクト形式で定義されます。

store.getState()を使ったりするとわかるのですが、


reducer0,reducer1,....を各々が定義したreducerの名前としたとき

state = { 
  reducer0: { 
    //reducer0で扱うvalue 
  }, 
  reducer1: { 
    //reducer1で扱うvalue 
  },... 
} 
という値がアプリのStateになります(store.getState()の返り値)

StoreはReducerを定義しなければインスタンスが作れません。

前回作ったTo-DoのComponent(App.js)が持つState(React)は、todos, countでした。countは僕が定義したんですが、todosを区別するためのIndexだったのでGlobal変数でよいです。

まずはtodosだけのState(Redux store)にしましょう。

{ 
  todos: [ 
    { 
      text: 'ASSANO', 
      completed: true 
    }, 
    { 
      text: 'ASAANO', 
      completed: false 
    } 
  ] 
} 
こんな感じのStateがStoreに行くようにしたいです。

実際にコードを描いていきましょう

src/

mkdir reducers 
cd reducers 
touch index.js todos.js 
src/reducers/index.js

import { combineReducers } from 'redux' 
import todos from './todos' 
 
export default combineReducers({ 
  todos 
}) 
index.jsはJS(ES2015以降)でModuleをImportする際に、それが含まれるディレクトリ名でImportされるエントリーポイントです。

今はReducerが一つしかないですが、面倒なのでCombineしてます。

src/reducers/todos.js

const todos = (state = [], action) => { 
  switch (action.type) { 
    case 'ADD_TODO': 
      return [ 
        ...state, 
        { 
          id: action.id, 
          text: action.text, 
          completed: false 
        } 
      ] 
    case 'TOGGLE_TODO': 
      return state.map( 
        todo => 
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo 
      ) 
    default: 
      return state 
  } 
} 
 
export default todos 
reducer本体です。

store.dispatch(action)が呼ばれたときにstateに現在のアプリのState,actionにdispatchの引数が渡されます。

ここでRedux的に(関数型的に)大事なのは、現在のStateを破壊しないで、新しいオブジェクトを生成していることです。(Pure function)

これによってundo(取り消し),historyの実装が容易になるらしいです。


Storeを定義する, Provider

react-reduxを使ってComponentに接続しましょう

StoreをReactのアプリ本体のエントリーポイント(だいたいindex.js)で定義します。定義したら、store.dispatchやstateを各子Componentで使えるようにProviderで渡します。

src/index.js

import React from 'react'; 
import ReactDOM from 'react-dom'; 
import App from './components/App'; 
 
import { Provider } from 'react-redux' 
import { createStore } from 'redux' 
import rootReducer from './reducers' 
 
const store = createStore(rootReducer); 
 
ReactDOM.render( 
  <Provider store={store}> 
    <App /> 
  </Provider>, 
  document.getElementById('root') 
); 
さて次はActionを定義しましょう。


2. todosをいじる操作をAction化する


Action creatorを定義する

ここら辺がReduxの気持ち悪いところですが、ActiontypeとdataをKeyに持つJS objectです。それによってStateを変える操作を表現しています。

{ 
  type: 'DO_WHAT', 
  data 
} 
これによって、いついかなるStateの変更でも、store.dispatch(action)すればよいという抽象化されたルールが作られます。typeの種類によって、先ほど定義したswitch文によって変更の種類が分類されます。

そのActionを生成する関数をAction Creatorとして、src/actions/以下に定義していきます。

Action Creatorのarrow関数風イメージ

const actionCreator = (data) => ({ 
  type: 'DO_WHAT', 
  data 
}) 
今回のAction(Stateを変える操作)は、2種類

  • todoを追加する(ADD_TODO)
  • todoを{完了,未完}化する(TOGGLE_TODO)
具体的にActionとしてほしいオブジェクトは下のような形になりそうです。

const addAction = { 
  type: 'ADD_TODO', 
  id: nextTodoId++, 
  text: 'ASAKKO' 
} 
 
const toggleAction = { 
  type: 'TOGGLE_TODO', 
  id: 1 //対象とするtodoのIndex 
} 
 
ではそれぞれのdataを受け取ってこんな感じのオブジェクトを返す関数を定義しましょう。

ディレクトリ構造としてはreducerに似せますが、コードは短いのでエントリーポイントに書き切ってしまいましょう。

mkdir actions 
cd actions 
touch index.js 
src/actions/index.js

let nextTodoId = 0 
export const addTodo = text => ({ 
  type: 'ADD_TODO', 
  id: nextTodoId++, 
  text 
}) 
export const toggleTodo = id => ({ 
  type: 'TOGGLE_TODO', 
  id 
}) 
さあAction Creatorが定義できたので、あとは各Componentでstore.dispatch(actioncreator(data))の形でコードを書き換えればようやくReduxのTo-Doの形になります。やはり一苦労ですね。


Container, connect

Reduxのうまみの一つは、「UIとデータロジックの分離」です。実際のチーム開発だと、UI書く側と裏のロジックを書く側にわかれることが多いので、その作業の区分をディレクトリ構造で表現することができます。

Redux作者Danさんの区分をうすっぺらく区分すると


  • Component == How things look == StyleとかUIロジック

  • Container == How things work == Dataとかのロジック
という感じです。

実際にcontainersを追加していきます。

(AddTodoは公式では違う構成で作られていますが今回は他のComponentと似せて同名のContainerを作ります。)

src/

mkdir containers 
cd containers 
touch VisibleTodoList.js AddTodo.js 
src/componentsで定義したReact ComponentのPropsに、Dispatchの作用を持つ関数必要なRedux storeのStateを渡させるコードをreact-reduxのConnectを使って描いていきます。

src/containers/VisibleTodoList.js

(VisibleFilterないから不整合ですが)

import { connect } from 'react-redux' 
import { toggleTodo } from '../actions' 
import TodoList from '../components/TodoList' 
 
const mapStateToProps = state => ({ 
  todos: state.todos 
}) 
 
const mapDispatchToProps = dispatch => ({ 
  toggleTodo: id => dispatch(toggleTodo(id)) 
})​ 
export default connect( 
  mapStateToProps, 
  mapDispatchToProps 
)(TodoList) 
 
src/containers/addTodo.js

import { connect } from 'react-redux'; 
import { addTodo } from '../actions'; 
import AddTodo from '../components/AddTodo'; 
 
 
const mapDispatchToProps = dispatch => ({ 
  addTodo: text => dispatch(addTodo(text)) 
})​ 
export default connect( 
  null, 
  mapDispatchToProps 
)(AddTodo) 
 
mapDispatchToProps, mapStateToPropsという二つの関数を定義してconnectの引数に渡します。

そうすると、src/index.jsで定義したstoreのstate、dispatchが↑の2つの関数に代入され、Component側のpropsには、2つの関数の返り値がpropsとして渡されます。

最後にsrc/components/App.jsで定義していたReactのStateと関数を無くして、ComponentのImport先を変更しましょう。

src/components/App.js

import React from 'react' 
import AddTodo from '../containers/AddTodo' 
import VisibleTodoList from '../containers/VisibleTodoList' 
 
 
class App extends React.Component { 
  render(){ 
    return ( 
      <div> 
        <VisibleTodoList /> 
        <AddTodo /> 
      </div> 
    ) 
  } 
} 
export default App 
 
これでやっと前回と同じ動きをします(ちょっと寂しい)


3. VisibilityFilterを追加する

さて機能拡張しましょう。下のGIFがガバガバムーブですが、ご愛嬌ください。



画面収録 2018-12-08 12.54.10.gif


全てのTodo, 完了したTodo, 未完なTodo,をそれぞれフィルターして表示するためのStateを追加します。(SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE)

{visibilityFilters: 'SHOW_XXXX'} 
まずはreducer,

src/reducers/index.js

import { combineReducers } from 'redux' 
import todos from './todos' 
import visibilityFilter from './visibilityFilter' 
 
export default combineReducers({ 
  todos, 
  visibilityFilter 
}) 
 
src/reducers/visibilityFilter.js

const visibilityFilter = (state = 'SHOW_ALL', action) => { 
  switch (action.type) { 
    case 'SET_VISIBILITY_FILTER': 
      return action.filter 
    default: 
      return state 
  } 
} 
 
export default visibilityFilter 
デフォルト値は'SHOW_ALL',

次にフィルターを変えるActionを追加してとUIを定義します。

公式の構造通りにしますが、Link(components)作る→FilterLink(containers)としてConnectする→Footer(components)で並べるという混乱しやすい順番なので注意してください。

そして最後にVisibleTodoListに渡すtodosをFilterするのを忘れないでください(公式に準拠しないツケ)

  • active: ButtonがActiveかどうか、そのフィルターが適応されていればdeactivateされる
  • children: React componentsのタグで囲んだ中身がpropsで入るときの名前
  • ownProps: connectの引数にいれる関数の第二引数、ComponentにReact本来の渡されるpropsがオブジェクト式で入ってる
src/actions/index.js

//... 
 
export const setVisibilityFilter = filter => ({ 
  type: 'SET_VISIBILITY_FILTER', 
  filter 
}) 
src/components/Link.js

import React from 'react' 
 
const Link = ({ active, children, onClick }) => ( 
  <button 
    onClick={onClick} 
    disabled={active} 
    style={{ 
      marginLeft: '4px' 
    }} 
  > 
    {children} 
  </button> 
) 
export default Link 
src/containers/FilterLink.js

import { connect } from 'react-redux' 
import { setVisibilityFilter } from '../actions' 
import Link from '../components/Link' 
 
const mapStateToProps = (state, ownProps) => ({ 
  active: ownProps.filter === state.visibilityFilter 
}) 
const mapDispatchToProps = (dispatch, ownProps) => ({ 
  onClick: () => dispatch(setVisibilityFilter(ownProps.filter)) 
}) 
 
export default connect( 
  mapStateToProps, 
  mapDispatchToProps 
)(Link) 
 
src/components/Footer.js

import React from 'react' 
import FilterLink from '../containers/FilterLink' 
const Footer = () => ( 
  <div> 
    <span>Show: </span> 
    <FilterLink filter={'SHOW_ALL'}>All</FilterLink> 
    <FilterLink filter={'SHOW_ACTIVE'}>Active</FilterLink> 
    <FilterLink filter={'SHOW_COMPLETED'}>Completed</FilterLink> 
  </div> 
) 
 
export default Footer 
 
src/containers/VisibleTodoList.js

import { connect } from 'react-redux' 
import { toggleTodo } from '../actions' 
import TodoList from '../components/TodoList' 
 
const getVisibleTodos = (todos, filter) => { 
  switch (filter) { 
    case 'SHOW_ALL': 
      return todos 
    case 'SHOW_COMPLETED': 
      return todos.filter(t => t.completed) 
    case 'SHOW_ACTIVE': 
      return todos.filter(t => !t.completed) 
    default: 
      throw new Error('Unknown filter: ' + filter) 
  } 
} 
 
const mapStateToProps = state => ({ 
  todos: getVisibleTodos(state.todos, state.visibilityFilter) 
}) 
 
 
const mapDispatchToProps = dispatch => ({ 
  toggleTodo: id => dispatch(toggleTodo(id)) 
}) 
 
export default connect( 
  mapStateToProps, 
  mapDispatchToProps 
)(TodoList) 
 
src/components/App.js

import React from 'react' 
import AddTodo from '../containers/AddTodo' 
import VisibleTodoList from '../containers/VisibleTodoList' 
import Footer from './Footer' 
 
class App extends React.Component { 
  render(){ 
    return ( 
      <div> 
        <VisibleTodoList /> 
        <AddTodo /> 
        <Footer /> 
      </div> 
    ) 
  } 
} 
export default App 
記事かいてて人間Gitしててめちゃくちゃつかれた。

公式のコピペを多用しました。Mac環境で公式のコピペをすると改行コードの違いか、謎の空文字が入っているのでCompile errorがおきます。↑のコードにも混在しているかもしれませんので注意してください。


次に

ほんとに小さいアプリにたいしてだとRedux導入はつらい。

苦労をすると、面倒なことが必要なこと・良いことに思えてしまうのでものごとのよしあしを見抜くのが難しくなります。これも慣れてしまえばシンプルにかけるとはいえ。僕にはわからない。

KISS(Keep It Simple, Stupid, etc)でいきましょう。

次はそんな面倒なことを抜きにして、DB使いたい!アプリ公開したい!サーバーレス!という兄貴達に喜ばれているFirebaseと連携します。

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)