kindle本「ReactでReduxを使ってみよう」のまとめ。
点数
80点
感想
理解することはできたが、少々わかりづらい点が多かった。
感想としては、reduxはとにかく面倒くさい、特にredux-thunkを使うとソースが複雑になってしまう、という印象を受けた。
コンポーネントが増えるとstateの管理が大変になるので、大規模アプリではreduxを使っていきたい。
基礎
Reduxとは
ReduxはJavaScriptの状態管理ライブラリである。
状態管理ライブラリは、単純なアプリでは使わない方がよい。
多数のコンポーネント間で状態を共有したい場合にだけ使うべき。
- 状態を変更したいコンポーネントはRedux Storeにdispatchを行う。
- 状態を取得したいコンポーネントはRedux Storeにsubscribeでハンドラ登録を行う
Reactと相性が良いため、セットで紹介されていることが多いが、Redux自体は独立したものなので単体で利用することも可能。
用語
- State
Reduxアプリの状態。 - Action
Stateの変更に関する情報を持ったオブジェクト。
typeプロパティは必須。
payload(データ)をプロパティとして持つことができる。 - Action Creator
Actionを返す関数。 - Store
Stateを保持しているオブジェクト。
Reduxアプリに1つだけ存在する。
createStore(reducer)を使って作られる。
以下のメソッドを持っている。
dispatch(action):State変更
getState():Stateの取得
subscribe(listener):State変更時のコールバック関数の登録 - Reducer
Actionから新しいStoreを作成して返す関数。
Basic Example
import { createStore } from 'redux'
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
};
let store = createStore(counter);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INCREMENT' }); // 1
store.dispatch({ type: 'INCREMENT' }); // 2
store.dispatch({ type: 'DECREMENT' }); // 1
- State:dispatchメソッドの戻り値
- Action:{ type: ‘INCREMENT’ }と{ type: ‘DECREMENT’ }
- Action Creator:上記の例では使われていない
- Store:変数store
- Reducer:counter関数
(第1引数が保持したいデータ。この例ではintだが、通常はオブジェクトにする)
処理の流れ
- createStoreメソッドでStoreを作成。
(ccounter関数が1度実行されstateが0になる) - store.subscribe()で状態変更時のハンドラを登録。
- store.dispatch()で状態変更。引数がAction。
この例ではAction Creatorは使われていない。
Action Creatorを使う場合は、以下のようになる。
const increment = () => {
return { type: 'INCREMENT' };
};
const decrement = () => {
return { type: 'DECREMENT' };
};
store.dispatch(increment()) // 1
store.dispatch(decrement()) // 2
store.dispatch(increment()) // 1
3原則
Redux実装時に守るべき3つのルールがある。 https://redux.js.org/introduction/three-principles
- Stateは1つのStore内にオブジェクトツリーとして保持する
cartとproductsの状態が必要な場合、state.cart, state.productsのようになる。 - Stateは読み取り専用でなければならない
変更時はStoreのdispatchメソッドにActionを引数として渡す。 - Stateの変更は純粋関数(pure function)によって行われる
(=Reducerは純粋関数出なければならない)
純粋関数とは
- inputが同じであれば常に同じ結果を返す。
- 副作用を生み出さない。
(コンソールやディスクへの出力がない、参照渡しによる呼び出し元への影響がない)
例えば、引数argで配列を受け取る関数の場合、arg.push('hoge'); return arg;
としている場合は非純粋関数、return [...arg, 'hoge'];
としている場合は非純粋関数。
Redux Devtools
chromeやfirefoxの拡張機能。
有効にするには、createStore関数に第2引数を渡す必要がある。
const store = createStore(
counterReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
// または
import { createStore, compose } from 'redux'
const store = createStore(counterReducer, compose(window.devToolsExtension && window.devToolsExtension()));
redux-thunkなどのミドルウェアを使用する場合
import { createStore, applyMiddleware, compose } from 'redux';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk)));
Node.jsで実行
Node.jsでReduxを使ってみる
npm install --save redux
const { createStore } = require('redux');
※Node.js環境ではimportではなくrequireを使用する- 下記コードをbasic1.jsとして保存
node basic1.js
で実行
const { createStore } = require('redux');
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
};
let store = createStore(counterReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INCREMENT' }); // 1
store.dispatch({ type: 'INCREMENT' }); // 2
store.dispatch({ type: 'INCREMENT' }); // 3
store.dispatch({ type: 'DECREMENT' }); // 2
Stateをオブジェクトにしてみる
- 下記コードをbasic2.jsとして保存
node basic2.js
で実行
const {createStore} = require('redux');
const userReducer = (state = {name: '', age: 0}, action) => {
switch (action.type) {
case 'SET_NAME':
return {...state, name: action.name};
case 'SET_AGE':
return {...state, age: action.age};
default:
return state
}
};
let store = createStore(userReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({type: 'SET_NAME', name: 'hoge'}); // { name: 'hoge', age: 0 }
store.dispatch({type: 'SET_AGE', age: 10}); // { name: 'hoge', age: 10 }
Reducerは、新しいオブジェクトにスプレッド構文を使用して純粋関数にしている。
Reactで実行
ReactでReduxを使ってみる
公式サイトにサンプルあり。
https://react-redux.js.org/introduction/quick-start
npm install --save-dev create-react-app
npx create-react-app basic1app
cd basic1app; npm install redux react-redux
react-redux:ReduxのStateの取得やActionのdispatchを、Reactコンポーネントからpropsオブジェクトを使って行うことができる。cd ./src; mkdir reducers; touch ./reducers/counterReducer.js
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
};
export default counterReducer;
mkdir actions; touch ./actions/counterActions.js
export const increment = () => {
return { type: 'INCREMENT' };
};
export const decrement = () => {
return { type: 'DECREMENT' };
};
- index.jsを修正
// 〜省略〜
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import counterReducer from './reducers/counterReducer';
const store = createStore(
counterReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
ProviderでAppコンポーネントを囲む。
store属性にcreateStore関数で生成したStoreを渡す。<Provider store={store}><App /></Provider>,
- App.jsを修正
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions/counterActions';
class App extends Component {
render() {
return (
<div>
<p>{this.props.count}回クリックされました。</p>
<button onClick={this.props.increment}>+</button>
<button onClick={this.props.decrement}>-</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state
}
};
const mapDispatchToProps = (dispatch) => {
return {
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
connect関数によりStateとdispatch関数をpropsにマッピングさせる。
・mapStateToProps:stateを受け取ってpropsにセットする関数
・mapDispatchToProps:dispatch関数を受け取ってpropsにセットする関数
※関数からreturnされたオブジェクトのキー名がprops名となる。export default connect(mapStateToProps, mapDispatchToProps)(App);
mapStateToProps, mapDispatchToPropsの片方しか必要のないコンポーネントでは、connectは以下のようになる。export default connect(mapStateToProps)(App);
export default connect(null, mapDispatchToProps)(App);
npm start
で実行
Stateをオブジェクトにしてみる
npx create-react-app basic2app
cd basic2app; npm install redux react-redux
cd ./src; mkdir reducers; touch ./reducers/userReducer.js
const userReducer = (state = {name: '', age: 0}, action) => {
switch (action.type) {
case 'SET_NAME':
return {...state, name: action.name};
case 'SET_AGE':
return {...state, age: action.age};
default:
return state
}
};
export default userReducer;
mkdir actions; touch ./actions/userActions.js
export const setName = (name) => {
return { type: 'SET_NAME', name }; // name: nameの省略
};
export const setAge = (age) => {
return { type: 'SET_AGE', age}; // ageはage: ageの省略
};
- index.jsを修正
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import userReducer from './reducers/userReducer';
const store = createStore(
userReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
- App.jsを修正
import { setName, setAge } from './actions/userActions';
class App extends Component {
handleSetNameClick = () => {
this.props.setName('hoge');
}
handleSetAgeClick = () => {
this.props.setAge(10);
}
render() {
return (
<div>
<p>名前:{this.props.name}、年齢:{this.props.age}</p>
<button onClick={this.handleSetNameClick}>Set Name</button>
<button onClick={this.handleSetAgeClick}>Set Age</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
name: state.name,
age: state.age
}
};
const mapDispatchToProps = (dispatch) => {
return {
setName: (name) => dispatch(setName(name)),
setAge: (age) => dispatch(setAge(age))
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
npm start
で実行