今回はReduxについての記事です。
Reduxとは -> Reduxの原理図(仕組み) -> 使い方1: createStore -> 使い方2: createSlice という流れで進めていきます。使い方1のcreateStoreは公式ドキュメントによって既に破棄されていますが、この使い方は原理図に非常に合致していて、原理図をそのまま反映したものだと言っても過言ではないので、初心者にとって非常にわかりやすいです。それに対して、使い方2のcreateSliceは、原理図内のものを組み立てたり新しいものを提唱したりしたものですので、まずcreateStoreを勉強して、その後createSliceに移行するほうが、reduxに対する理解が深まりやすいと私は思います。
1. Reduxとは何か
Reduxは、アプリの状態(ステート)を一元管理する仕組みです。特にアプリが大きくなり、コンポーネントや画面間で多くのデータをやり取りする場合に、その「状態」が複雑化して混乱しないように設計されている仕組みです。
あるアプリでA>B>C>Dという四つのコンポーネントが存在しているとします。>は包含関係を表します。もしあるステートがすべてのコンポーネントで使う必要がある場合、どうしますか?
もちろんpropsを使って親コンポーネントから子コンポーネントへ渡すという方法はありますが、四つのコンポーネントもありますので流石に手間がかかりすぎます。
この場合reduxを使えば相当楽になれます。Reduxを使ったら、この四つのコンポーネントから独立した状態センターを作ることができ、どのコンポーネントでも使うときはこの状態センターから取ったらOKです。また、どのコンポーネントでステートを変更したら、ほかのコンポーネントでも自動的に更新されます。
2. Reduxの仕組み
まずReduxの原理図を張り付けておきます:

ReduxにはAction Creator, Store, Reducerという三つのメンバーがあります。storeは中枢に相当する存在で、すべてのステートはstoreで格納されています。React Componentでステートに対して変更を行おうとする時、まず変更の種類と変更後の値に基づいてAction creatorsを作成します。Action creatorsの型はオブジェクトもしくは関数で、ここでオブジェクトとします。Action creatorsにはtypeとdataという二つの属性が必要で、それぞれ変更の種類と変更後の値を表します。
例えば、storeでcount(総和)というステートがあるとして、+1したいなら{ type:"increment", data:1}、-1したいなら{type:"decrement", data:1}、という感じですね。
ステートの変更を表す「action」はありました。そしてこの変更を実際に実行するというものが要ります。それはreducersです。reducerは、受け取ったactionのtypeとdataに従って適切な変更操作を実行します。reducerの型は関数しかできなくて、二つのパラメータを持っています。一つ目はpreviousState、文字通りactionを受け取った現時点のstateを表します。二つ目はactionです。で、reducerには返り値も必要で、そのstateの新しい値を表します。どこへreturnするかというと、storeへです。storeはreducerからreturnされた値を受け取って、stateの値を変えます。
ここまで読んできたら、たぶん一つの疑問が浮かび上がるかもしれない。reducerはどうやってactionを受け取れるの⁇
上の原理図を見ていただければなんとなくわかると思います。そう、dispatchです。store.dispatch(action)で、actionをreducerに渡すわけです。
3.使い方1:createStore()
reduxを実装するには、最低でも二つのファイルが必要です。一つ目はstore.jsで、もう一つはreducer.jsです(実際の場合はどのステートを操作するのによって複数のreducerが必要ですが(例えばstudent_reducer.js、teacher_reducer.jsなど)、便宜上ここでは一つにしておきます)。
3.1 store.js
/*
storeを外へexportする専用ファイルです
storeは一つしかいない
*/
import {createStore} from 'redux'
// reducerをimportしないといけない
import countReducer from './count_reducer'
// 導入したreducerをstoreに紐づける
export default createStore(countReducer)
3.2 count_reducer.js
ここでもう一つの疑問が浮かび上がるかもしれない:初めてreducerを呼び出すとき、previousStateは一体なんですか
答えはundefinedです。しかしここでのstateはcountですので、初期値は0であってほしい。
そこでinitState変数を宣言し、previousStateのデフォルト値とします。
const initState = 0
export default function countReducer(previousState=initState,action){
const {type,data} = action
// switch case文を使って、typeごとの処理を定義する
switch (type) {
case 'increment':
return previousState + data
case 'decrement':
return previousState - data
default:
return previousState
}
}
3.3 Reactコンポーネント
まず、今回例として取り上げられたReactコンポーネントの様子を確認してください:

コードは以下の通りです:
import React, { Component } from 'react'
// storeで格納されているステートを入手するには、storeをimportする必要がある
import store from '../../redux/store'
export default class Count extends Component {
componentDidMount(){
store.subscribe(()=>{
this.setState({})
})
}
increment = ()=>{
const {value} = this.selectNumber
// 足し算のボタンを押すと、store.dispatchで足し算を意味するactionをreducerへ渡す そこで実行してもらう
store.dispatch({type:'increment',data:value*1})
}
decrement = ()=>{
const {value} = this.selectNumber
// 引き算のボタンを押すと、store.dispatchで引き算を意味するactionをreducerへ渡す そこで実行してもらう
store.dispatch({type:'decrement',data:value*1})
}
render() {
return (
<div>
<h1>現時点での総和は:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
)
}
}
上記のコードで
componentDidMount(){
store.subscribe(()=>{
this.setState({})
})
}
がないと画面上のcountはずっと変わらないです。
reduxだけでは、ページの再レンダリングはできないので、setState()で強制的に再レンダリングを実行します。
4. createSlice()
createStore()は現時点で、公式ドキュメントでは破棄されています(使えるは使えるけど、使用すると線が引かれています)。いま公式ドキュメントでのおすすめの使い方は、createSlice()です。
4.1 createSliceとは
もともとの一つしかないstoreは、複数のsliceで代替されます。storeで格納されていた各ステートは、今異なるsliceで管理されています。ただし、sliceはステートを格納するところだけではなく、そのステートに対応するaction creatorとreducerもsliceに収納されています。
createSliceを使ったら最低三つのファイルが必要です。
一つ目:store.js 各sliceを集めるファイル
二つ目:slice.js
三つ目:reducer.js
今回取り上げた例は、ポストを掲載するサイトです。reduxで管理されるステートは、postsだけだと想定します。
早速例を見せたいと思います:

4.2 slice.js
import { createSlice } from "@reduxjs/toolkit";
const initState = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]
const postsSlice = createSlice({
name:"posts",// ステートの名前
initialState:initState,// 初期値
reducers:{ // このステートを処理するreducer
addPost(state, action){
state.push(action.payload) // 前と違って、変更後の値はaction.dataではなくaction.payload
}
}
})
// 上記のreducersで新しいreducerを追加するたびに、reduxは自動的にそれを対応するaction creatorを作って、このsliceのactionsに収納している
// sliceのaction creatorsとreducersをexportする必要がある
export const {addPost} = postsSlice.actions
export default postsSlice.reducer
4.2 store.js
import { configureStore } from "@reduxjs/toolkit";
import PostsReducer from './postSlice'
export default configureStore({
reducer:{
posts: PostsReducer
}
})
4.3 Reactコンポーネン PostsList.jsx
import React from 'react'
import { useSelector } from 'react-redux'
export default function PostsList(){
// ステートを得るには、useSelectorというhookを使う必要がある
const posts = useSelector(state => state.posts)
return (
<div>
<h2>Posts</h2>
{posts.map((post)=>{
return (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.content.substring(0,100)}</p>
</article>
)
})}
</div>
)
}