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を作ることです。
完成版はこちらです
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は、JSアプリの状態管理フレームワークです。
FaceBook製ではないです。FaceBookのState管理問題への対処はFluxでしたが、そのフローを模倣したReduxがそれを抜きん出た人気を見せて、流行っているんだそうです。
HaskellにInspireされた関数型言語elmにInspireされているそうなので関数型です。
Reduxは三つのPrinciplesを持っています。関数型の焼き直し的な感じです。僕的にはあんまり実装につながらないし、区分も好きじゃないです。
これは、Applicationの状態は単一のStore(State)で管理しようというものです。
Reactの中ではComponent単位で定義されていたState(react公式)は、今回はAppに対して唯一のGlobalなStore(redux公式)のStateという文脈で使います。
Actionとかの手順を踏んで変更しようねということと下のとつながりますが、破壊しちゃだめってことだと思います。
reducerを見るとわかりますが、引数はこわしちゃだめです。履歴を作ったりするために新しいものを作るだけにしておきましょう。
この記事あたりがいいねも多いからきっとわかりやすいのでしょう。(真面目に読んでいません)
こんなGIF画像があったので拾ってきました。
この絵と公式ドキュメントで誤解を産みそうな点は、
- ActionsはAction Creatorという関数のあつまり(その返り値をActionとかいう)ということと
- DispatcherはReduxには存在しない(って公式にありますFluxにはあるらしいですが知りません。)ということです。DispatchがStoreクラスのメンバ関数として定義されています。
さて実際に手を動かします。
まずはPackage Installから
ReduxはReact専用フレームワークではなくJSパッケージです。Reactで使う場合はReact公式のBindingであるreact-reduxを使うのが定石のようです。
js packageは上記のようなコマンドで追加します。saveオプションをするとpackage.jsonにバージョンとpackage名が追加され、Githubでレポジトリを共有した際、多環境でも
するだけでpackage.jsonに記入されているpackageがローカルにInstallされて環境が整うようになります
まずはアプリで単一に統制されるべきStateを定義しましょう。
公式チュートリアルではActionsの説明から入ってますが、公式がReduxでアプリを作るならReducerから決めようねと言っているのでReducerを描いてStoreを作るところからはじめます。
公式でははじめからToDoのVisibilityFilterを定義していますが、それはのちほど追加します。
ReduxのStateはKey:Valueの形のJSオブジェクト形式で定義されます。
store.getState()を使ったりするとわかるのですが、
reducer0,reducer1,....を各々が定義したreducerの名前としたとき
という値がアプリのStateになります(store.getState()の返り値)
StoreはReducerを定義しなければインスタンスが作れません。
前回作ったTo-DoのComponent(App.js)が持つState(React)は、todos, countでした。countは僕が定義したんですが、todosを区別するためのIndexだったのでGlobal変数でよいです。
まずはtodosだけのState(Redux store)にしましょう。
こんな感じのStateがStoreに行くようにしたいです。
実際にコードを描いていきましょう
src/
src/reducers/index.js
index.jsはJS(ES2015以降)でModuleをImportする際に、それが含まれるディレクトリ名でImportされるエントリーポイントです。
今はReducerが一つしかないですが、面倒なのでCombineしてます。
src/reducers/todos.js
reducer本体です。
store.dispatch(action)が呼ばれたときにstateに現在のアプリのState,actionにdispatchの引数が渡されます。
ここでRedux的に(関数型的に)大事なのは、現在のStateを破壊しないで、新しいオブジェクトを生成していることです。(Pure function)
これによってundo(取り消し),historyの実装が容易になるらしいです。
react-reduxを使ってComponentに接続しましょう
StoreをReactのアプリ本体のエントリーポイント(だいたいindex.js)で定義します。定義したら、store.dispatchやstateを各子Componentで使えるようにProviderで渡します。
src/index.js
さて次はActionを定義しましょう。
ここら辺がReduxの気持ち悪いところですが、ActionはtypeとdataをKeyに持つJS objectです。それによってStateを変える操作を表現しています。
これによって、いついかなるStateの変更でも、store.dispatch(action)すればよいという抽象化されたルールが作られます。typeの種類によって、先ほど定義したswitch文によって変更の種類が分類されます。
そのActionを生成する関数をAction Creatorとして、src/actions/以下に定義していきます。
Action Creatorのarrow関数風イメージ
今回のAction(Stateを変える操作)は、2種類
ではそれぞれのdataを受け取ってこんな感じのオブジェクトを返す関数を定義しましょう。
ディレクトリ構造としてはreducerに似せますが、コードは短いのでエントリーポイントに書き切ってしまいましょう。
src/actions/index.js
さあAction Creatorが定義できたので、あとは各Componentでstore.dispatch(actioncreator(data))の形でコードを書き換えればようやくReduxのTo-Doの形になります。やはり一苦労ですね。
Reduxのうまみの一つは、「UIとデータロジックの分離」です。実際のチーム開発だと、UI書く側と裏のロジックを書く側にわかれることが多いので、その作業の区分をディレクトリ構造で表現することができます。
Redux作者Danさんの区分をうすっぺらく区分すると
実際にcontainersを追加していきます。
(AddTodoは公式では違う構成で作られていますが今回は他のComponentと似せて同名のContainerを作ります。)
src/
src/componentsで定義したReact ComponentのPropsに、Dispatchの作用を持つ関数、必要なRedux storeのStateを渡させるコードをreact-reduxのConnectを使って描いていきます。
src/containers/VisibleTodoList.js
(VisibleFilterないから不整合ですが)
src/containers/addTodo.js
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
これでやっと前回と同じ動きをします(ちょっと寂しい)
さて機能拡張しましょう。下のGIFがガバガバムーブですが、ご愛嬌ください。
全てのTodo, 完了したTodo, 未完なTodo,をそれぞれフィルターして表示するためのStateを追加します。(SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE)
まずはreducer,
src/reducers/index.js
src/reducers/visibilityFilter.js
デフォルト値は'SHOW_ALL',
次にフィルターを変えるActionを追加してとUIを定義します。
公式の構造通りにしますが、Link(components)作る→FilterLink(containers)としてConnectする→Footer(components)で並べるという混乱しやすい順番なので注意してください。
そして最後にVisibleTodoListに渡すtodosをFilterするのを忘れないでください(公式に準拠しないツケ)
src/components/Link.js
src/containers/FilterLink.js
src/components/Footer.js
src/containers/VisibleTodoList.js
src/components/App.js
記事かいてて人間Gitしててめちゃくちゃつかれた。
公式のコピペを多用しました。Mac環境で公式のコピペをすると改行コードの違いか、謎の空文字が入っているのでCompile errorがおきます。↑のコードにも混在しているかもしれませんので注意してください。
ほんとに小さいアプリにたいしてだとRedux導入はつらい。
苦労をすると、面倒なことが必要なこと・良いことに思えてしまうのでものごとのよしあしを見抜くのが難しくなります。これも慣れてしまえばシンプルにかけるとはいえ。僕にはわからない。
KISS(Keep It Simple, Stupid, etc)でいきましょう。
次はそんな面倒なことを抜きにして、DB使いたい!アプリ公開したい!サーバーレス!という兄貴達に喜ばれているFirebaseと連携します。
本記事はWeb初心者でもReact, Redux, FirebaseでTo-Doシリーズ2記事目です
本シリーズの目標は、
を使ってちょっとリッチなTo-Doを作ることです。
完成版はこちらです
- Reactのスケルトン作り
- Reduxのデータフロー導入←イマここ
- Firebaseを使ってデータベースと認証
- 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のながれ参考図
こんなGIF画像があったので拾ってきました。
この絵と公式ドキュメントで誤解を産みそうな点は、
- ActionsはAction Creatorという関数のあつまり(その返り値をActionとかいう)ということと
- DispatcherはReduxには存在しない(って公式にありますFluxにはあるらしいですが知りません。)ということです。DispatchがStoreクラスのメンバ関数として定義されています。
Install
さて実際に手を動かします。まずはPackage Installから
ReduxはReact専用フレームワークではなくJSパッケージです。Reactで使う場合はReact公式のBindingであるreact-reduxを使うのが定石のようです。
npm install redux react-redux --save
npm i
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 },... }
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 } ] }
実際にコードを描いていきましょう
src/
mkdir reducers cd reducers touch index.js todos.js
import { combineReducers } from 'redux' import todos from './todos' export default combineReducers({ todos })
今は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
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') );
2. todosをいじる操作をAction化する
Action creatorを定義する
ここら辺がReduxの気持ち悪いところですが、ActionはtypeとdataをKeyに持つJS objectです。それによってStateを変える操作を表現しています。{ type: 'DO_WHAT', data }
そのActionを生成する関数をAction Creatorとして、src/actions/以下に定義していきます。
Action Creatorのarrow関数風イメージ
const actionCreator = (data) => ({ type: 'DO_WHAT', data })
- todoを追加する(ADD_TODO)
- todoを{完了,未完}化する(TOGGLE_TODO)
const addAction = { type: 'ADD_TODO', id: nextTodoId++, text: 'ASAKKO' } const toggleAction = { type: 'TOGGLE_TODO', id: 1 //対象とするtodoのIndex }
ディレクトリ構造としてはreducerに似せますが、コードは短いのでエントリーポイントに書き切ってしまいましょう。
mkdir actions cd actions touch index.js
let nextTodoId = 0 export const addTodo = text => ({ type: 'ADD_TODO', id: nextTodoId++, text }) export const toggleTodo = id => ({ type: 'TOGGLE_TODO', id })
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/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)
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)
そうすると、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がガバガバムーブですが、ご愛嬌ください。全てのTodo, 完了したTodo, 未完なTodo,をそれぞれフィルターして表示するためのStateを追加します。(SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE)
{visibilityFilters: 'SHOW_XXXX'}
src/reducers/index.js
import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos, visibilityFilter })
const visibilityFilter = (state = 'SHOW_ALL', action) => { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter default: return state } } export default visibilityFilter
次にフィルターを変える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がオブジェクト式で入ってる
//... export const setVisibilityFilter = filter => ({ type: 'SET_VISIBILITY_FILTER', filter })
import React from 'react' const Link = ({ active, children, onClick }) => ( <button onClick={onClick} disabled={active} style={{ marginLeft: '4px' }} > {children} </button> ) export default Link
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)
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
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)
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
公式のコピペを多用しました。Mac環境で公式のコピペをすると改行コードの違いか、謎の空文字が入っているのでCompile errorがおきます。↑のコードにも混在しているかもしれませんので注意してください。
次に
ほんとに小さいアプリにたいしてだとRedux導入はつらい。苦労をすると、面倒なことが必要なこと・良いことに思えてしまうのでものごとのよしあしを見抜くのが難しくなります。これも慣れてしまえばシンプルにかけるとはいえ。僕にはわからない。
KISS(Keep It Simple, Stupid, etc)でいきましょう。
次はそんな面倒なことを抜きにして、DB使いたい!アプリ公開したい!サーバーレス!という兄貴達に喜ばれているFirebaseと連携します。
コメント
コメントを投稿