学びの記録『りあクト!』第11章 Redux でグローバルな状態を扱う
useSelector
, useDispatch
を用いたReduxの書き方
ディレクトリ構造
container, presentationalディレクトリで分けて、atomic design風に分ける。
. ├── App.css ├── App.tsx ├── components │ ├── container │ │ ├── molecules │ │ │ └── ColorfulBeads.tsx │ │ └── organisms │ │ └── CounterBoard.tsx │ └── presentational │ ├── molecules │ │ ├── ColorfulBeads.css │ │ └── ColorfulBeads.tsx │ └── organisms │ ├── CounterBoard.css │ └── CounterBoard.tsx ├── index.tsx └── redux ├── actions.ts └── reducer.ts
コード
index.tsx
import React from 'react' import ReactDOM from 'react-dom' import { createStore } from 'redux' import { Provider } from 'react-redux' import { counterReducer, initialState } from './redux/reducer' import { App } from './App' import 'semantic-ui-css/semantic.css' const store = createStore(counterReducer, initialState) ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'), )
App.tsx
import React from 'react' import { EnhancedCounterBoard as CounterBoard } from './components/container/organisms/CounterBoard' import { EnhancedColorfulBeads as ColorfulBeads } from './components/container/molecules/ColorfulBeads' import './App.css' export const App: React.FC = () => ( <div className="container"> <header> <h1>Beads counter</h1> </header> <CounterBoard /> <ColorfulBeads count={0} /> </div> )
App.tsx
にcontainerコンポーネントCounterBoard.tsx
, ColorfulBeads.tsx
を読み込む。
src/components/container/organisms/CounterBoard.tsx
import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { ICounterState } from '../../../redux/reducer' import { decrement, increment, add } from '../../../redux/actions' import { CounterBoard } from '../../presentational/organisms/CounterBoard' export const EnhancedCounterBoard: React.FC = () => { const count = useSelector<ICounterState, number>((state) => state.count) const dispatch = useDispatch() return ( <CounterBoard count={count} decrement={() => dispatch(decrement())} increment={() => dispatch(increment())} add={(amount) => dispatch(add(amount))} /> ) }
src/components/container/molecules/ColorfulBeads.tsx
import React from 'react' import { useSelector } from 'react-redux' import { ICounterState } from '../../../redux/reducer' import { ColorfulBeads } from '../../presentational/molecules/ColorfulBeads' export const EnhancedColorfulBeads: React.FC<{ count: number }> = () => { const count = useSelector<ICounterState, number>((state) => state.count) return <ColorfulBeads count={count} /> }
containerコンポーネント側でuseSelector
, useDispatch
を利用してRedux storeとdispatcherとのやり取りをしている。
Redux actions, reducerは以下のようになっている。
src/redux/actions.ts
export enum CounterActionType { ADD = 'counter/ADD', DECREMENT = 'counter/DECREMENT', INCREMENT = 'counter/INCREMENT', } export interface ICounterActionDecrement { type: CounterActionType.DECREMENT } export interface ICounterActionIncrement { type: CounterActionType.INCREMENT } export interface ICounterActionAdd { type: CounterActionType.ADD amount: number } export const decrement = (): ICounterActionDecrement => ({ type: CounterActionType.DECREMENT, }) export const increment = (): ICounterActionIncrement => ({ type: CounterActionType.INCREMENT, }) export const add = (amount: number): ICounterActionAdd => ({ type: CounterActionType.ADD, amount, }) export type CounterActions = | ReturnType<typeof decrement> | ReturnType<typeof increment> | ReturnType<typeof add>
src/redux/reducer.ts
import { Reducer } from 'redux' import { CounterActions, CounterActionType as Type } from './actions' export interface ICounterState { count: number } export const initialState: ICounterState = { count: 0 } export const counterReducer: Reducer<ICounterState, CounterActions> = ( state: ICounterState = initialState, action: CounterActions, ): ICounterState => { switch (action.type) { case Type.ADD: return { ...state, count: state.count + action.amount, } case Type.DECREMENT: return { ...state, count: state.count - 1, } case Type.INCREMENT: return { ...state, count: state.count + 1, } default: { const _: never = action return state } } }
presentationalコンポーネント
src/components/presentational/organisms/CounterBoard.tsx
import React from 'react' import { Card, Statistic, Button } from 'semantic-ui-react' const BULK_UNIT = 10 interface ICounterBoardProps { count: number decrement: () => void increment: () => void add: (amount: number) => void } export const CounterBoard: React.FC<ICounterBoardProps> = ({ count = 0, decrement, increment, add, }) => ( <Card> <Statistic className="number-board"> <Statistic.Label>count</Statistic.Label> <Statistic.Value>{count}</Statistic.Value> </Statistic> <Card.Content> <div className="ui two buttons"> <Button color="red" onClick={decrement}> -1 </Button> <Button color="green" onClick={increment}> +1 </Button> </div> <div className="fluid-button"> <Button fluid color="grey" onClick={() => add(BULK_UNIT)}> +{BULK_UNIT} </Button> </div> </Card.Content> </Card> )
src/components/presentational/molecules/ColorfulBeads.tsx
import React from 'react' import { SemanticCOLORS, Container, Label } from 'semantic-ui-react' import './ColorfulBeads.css' // equivalent to https://lodash.com/docs/#range // see also https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from const range = (n: number): number[] => n < 0 ? [] : Array.from(Array(n), (_, i) => i) const colors: SemanticCOLORS[] = [ 'red', 'orange', 'yellow', 'olive', 'green', 'teal', 'blue', 'violet', 'purple', 'pink', 'brown', 'grey', 'black', ] export const ColorfulBeads: React.FC<{ count: number }> = ({ count = 0 }) => ( <Container className="beads-box"> {range(count).map((n) => ( <Label circular color={colors[n % colors.length]} key={n} /> ))} </Container> )
Redux toolkitの導入
Redux toolkitを用いれば、DUCKSパターンを用いたシンプルなRedux構造を考えることができる。
また、Redux toolkit提供メソッドの一つであるcreateSlice
を用いれば、
- initial state
- reducer
- action types
- actions
などをまとめて一つのファイル内に記述できる。
ディレクトリ構造
. ├── App.css ├── App.tsx ├── components │ ├── container │ │ ├── molecules │ │ │ └── ColorfulBeads.tsx │ │ └── organisms │ │ └── CounterBoard.tsx │ └── presentational │ ├── molecules │ │ ├── ColorfulBeads.css │ │ └── ColorfulBeads.tsx │ └── organisms │ ├── CounterBoard.css │ └── CounterBoard.tsx ├── features │ └── counter.ts └── index.tsx
コード
src/index.tsx
import React from 'react' import ReactDOM from 'react-dom' import { configureStore } from '@reduxjs/toolkit' import { Provider } from 'react-redux' import { counterSlice } from './features/counter' import { App } from './App' import 'semantic-ui-css/semantic.css' const store = configureStore({ reducer: counterSlice.reducer }) ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'), )
src/App.tsx
import React from 'react' import { EnhancedColorfulBeads as ColorfulBeads } from './components/container/molecules/ColorfulBeads' import { EnhancedCounterBoard as CounterBoard } from './components/container/organisms/CounterBoard' import './App.css' export const App: React.FC = () => ( <div className="container"> <header> <h1>Beads counter</h1> </header> <CounterBoard /> <ColorfulBeads count={0} /> </div> )
src/features/counter.ts
counterに関連するreduxコードがこちらのファイルに全てまとまる。
import { createSlice, PayloadAction } from '@reduxjs/toolkit' export interface ICounterSlice { count: number } const initialState: ICounterSlice = { count: 0 } export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { decremented: (state) => ({ ...state, count: state.count - 1 }), incremented: (state) => ({ ...state, count: state.count + 1 }), added: (state, action: PayloadAction<number>) => ({ ...state, count: state.count + action.payload, }), }, })
src/components/container/organisms/CounterBoard.tsx
import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { ICounterSlice, counterSlice } from '../../../features/counter' import { CounterBoard } from '../../presentational/organisms/CounterBoard' export const EnhancedCounterBoard: React.FC = () => { const count = useSelector<ICounterSlice, number>((state) => state.count) const dispatch = useDispatch() const { decremented, incremented, added } = counterSlice.actions return ( <CounterBoard count={count} decrement={() => dispatch(decremented())} increment={() => dispatch(incremented())} add={(amount) => dispatch(added(amount))} /> ) }
src/components/container/molecules/ColorfulBeads.tsx
import React from 'react' import { useSelector } from 'react-redux' import { ColorfulBeads } from '../../presentational/molecules/ColorfulBeads' import { ICounterSlice } from '../../../features/counter' export const EnhancedColorfulBeads: React.FC<{ count: number }> = () => { const count = useSelector<ICounterSlice, number>((state) => state.count) return <ColorfulBeads count={count} /> }