React:UI に特化した JavaScript フロントエンドライブラリ#
Redux#
redux は、アプリ全体のコンポーネントの共有状態とデータを集中管理するための状態管理ライブラリです。
store#
import { createStore } from 'redux'
const init = {
value:'defaultValue'
}
const store = createStore((state=init,action) => {
const { type, data } = action
switch(type){
case 'add':
return {...state,num:data}
default:
return state
}
})
export default store
アクションをディスパッチする#
import store from './store'
const actionCreator = (data) => {
return {
type:'add',
data
}
}
btnClick = () => {
const action = actionCreator(233)
store.dispatch(action)
}
store の値を取得する#
import store from './store'
class Home extends React.component{
componentDidMount(() => {
this.unSubscribe = store.subscribe(() => {
// storeの状態変化を監視してビューを更新
this.setState({})
})
})
componentWillUnmount(() => {
// 監視を解除
this.unSubscribe()
})
render(){
return (
<>
<h2> { store.getState().num }</h2>
</>
)
}
}
react-redux#
react-redux は、コンポーネント内でアクションをより簡単にディスパッチし、store 内のデータを取得できるようにし、データの更新が自動的にコンポーネントを再レンダリングします。
- Provider は、最外層のルートコンポーネントを包み、その子コンポーネントが状態を共有できるようにするコンポーネントです。
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
);
- 関数型コンポーネントでアクションを送信し、store 内の値を取得する
import React, { memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
export default memo(function Text() {
const dispatch = useDispatch();
const num = useSelector(state => state.num); // 値を取得
const btnClick = () => {
// アクションを送信
dispatch({
type: 'add',
num: 10,
});
};
return (
<div>
<h2></h2>
<button onClick={e => btnClick()}>アクションを送信</button>
</div>
);
});
-
クラスコンポーネントで store を取得し、アクションを発生させる ==> connect
connect は、コンポーネントと store を関連付けてデータを共有できるようにします。
import React from 'react'
import { connect } from 'react-redux'
class Home extends React.component {
const btnClick = () => {
this.props.sendAction()
}
<>
<button onClick={this.btnClick}>ディスパッチ</button>
</>
}
export connect(
// パラメータ1:reduxのstate; パラメータ2:コンポーネント自身のprops
// 戻り値:オブジェクト、これをコンポーネントのpropsに渡します。
(state,ownProps)=>{ // 受け取る
},
// パラメータ1:dispatch関数、アクションをディスパッチするためのもの;パラメータ2:自身のprops
// 戻り値:オブジェクト、これをHomeコンポーネントのpropsに渡します。
(dispatch,ownProps)=>{ // 送信
return {
sendAction:() => {
// dispatchを利用してアクションを送信
dispatch({
type:'add',
num:1
})
}
}
}
)(Home)
コードを改善する#
- store
import { createStore } from 'redux';
const init = {
value: 'デフォルト値',
num: 0,
};
function reducer(state = init, action) {
const { type, data } = action;
switch (type) {
case 'add':
return { ...state, num: data };
case 'sub':
return { ...state, num: data };
default:
return state;
}
}
export default createStore(reducer);
- ルートコンポーネント
import React, { PureComponent } from 'react';
import { Provider } from 'react-redux';
import About from './pages/About';
import Home from './pages/Home';
import store from './store';
export default class App extends PureComponent {
render() {
return (
<Provider store={store}>
<Home ownProps={'ヒヒヒ'} />
<hr />
<About />
</Provider>
);
}
}
- クラスコンポーネント
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
class Home extends PureComponent {
btnClick = () => {
this.props.addAction();
};
render() {
return (
<div>
<h2>Home:クラスコンポーネント</h2>
<button onClick={this.btnClick}>+</button>
<h2>現在のカウント:{this.props.num}</h2>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return state;
};
const mapDispatchToProps = (dispatch, ownProps) => {
let num = 0;
return {
addAction: () => {
dispatch({
type: 'add',
data: ++num,
});
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
- 関数型コンポーネント
import React, { memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
export default memo(function About() {
const dispatch = useDispatch();
const num = useSelector(state => state.num);
const btnClick = () => {
let temNum = num;
dispatch({
type: 'sub',
data: --temNum,
});
};
return (
<div>
<h2>About:関数型コンポーネント</h2>
<button onClick={btnClick}>-</button>
<h2>現在のカウント{num}</h2>
</div>
);
});
combineReducers(reducers)#
- reducers/todos.js
export default function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text]);
default:
return state;
}
}
- reducers/counter.js
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
- reducers/index.js
import { combineReducers } from 'redux';
import todos from './todos';
import counter from './counter';
export default combineReducers({
todos,
counter,
});
- App.js
import { createStore } from 'redux';
import reducer from './reducers/index';
let store = createStore(reducer);
console.log(store.getState());
// {
// counter: 0,
// todos: []
// }
store.dispatch({
type: 'ADD_TODO',
text: 'Reduxを使用する',
});
console.log(store.getState());
// {
// counter: 0,
// todos: [ 'Reduxを使用する' ]
// }
Mobx#
store#
import { action, computed, runInAction, makeAutoObservable, flow } from 'mobx';
export class CounterStore {
// データを定義
count = 0;
list = [1, 2, 3, 4, 5];
asyncData = 0;
flowData = 'default';
constructor() {
// データをリアクティブにする
makeAutoObservable(this, {
// マークすることも、しないこともできます
incrementCount: action,
decrementCount: action,
changeFilterList: action,
asyncAdd: action,
asyncFlow: action.bound,
filterList: computed,
});
}
// データを変更するためのアクション関数を定義
incrementCount = () => {
this.count++;
};
decrementCount = () => {
this.count--;
};
changeFilterList = () => {
this.list.push(...[5, 4, 3, 2, 1]);
};
// 非同期アクション(runInActionでラップ)
asyncAdd = async () => {
await Promise.resolve();
runInAction(() => {
this.incrementCount();
this.asyncData++;
});
};
// 非同期アクション(flowを使用)
asyncFlow = flow(function* () {
yield console.log(this); // thisをバインドする必要があります
const data = yield Promise.resolve('Flow');
this.flowData = data;
});
// computedを定義
get filterList() {
return this.list.map(item => item * 2);
}
}
ルートストア#
import { CounterStore } from './counter';
class Store {
constructor() {
this.counter = new CounterStore();
}
}
const store = new Store();
export { store };
store の共有#
コンテキストを使用
import React, { createContext } from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { store } from './store';
export const Context = createContext();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Context.Provider value={store}>
<App />
</Context.Provider>
);
コンポーネント内での使用#
import React, { memo, useContext } from 'react';
import { observer } from 'mobx-react-lite';
import { Context } from '../..';
const Counter = () => {
const {
counter: {
count,
changeFilterList,
asyncData,
flowData,
decrementCount,
filterList,
incrementCount,
asyncAdd,
asyncFlow,
},
} = useContext(Context);
return (
<div>
<button onClick={incrementCount}>+</button>
<span>{count}</span>
<button onClick={decrementCount}>-</button>
{filterList.map((i, n) => {
return <li key={n}>{i}</li>;
})}
<button onClick={changeFilterList}>配列を追加</button>
<hr />
<span>{asyncData}</span>
<button onClick={asyncAdd}>非同期runInAction</button>
<hr />
<span>{flowData}</span>
<button onClick={asyncFlow}>非同期flow</button>
</div>
);
};
export default memo(observer(Counter));