LoginSignup
3
3

More than 1 year has passed since last update.

React Hooks + Redux(旧) vs React Hooks + Redux Toolkit(+Redux Hooks)

Posted at

Reduxを思い出しながらRedux toolkitへ移行してみる

Reduxを使う機会があるのですが、Redux自体は最近だと使わなくてもReactのcontextさえあれば代替出来てしまったり、そもそも使わなくても行けてしまうことも多く、完全に使い方を忘れていました。
その為、Reduxってそもそもどうやって使うんだっけ?(そもそも使っていたものReactがクラスコンポーネントだった時代)を振り返りつつ、Redux Toolkitの使い方も調べてみました。

React HooksでRedux(toolkitじゃない方)を使う

①store

まずは、storeを設定し、Providerに渡す。

index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import ConnectedTodo from "./components/Todo";
import { createStore } from "redux";
import rootReducer from "./redux/reducer";
import reportWebVitals from './reportWebVitals';

const store = createStore(rootReducer);

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={store}>
    <StrictMode>
      <ConnectedTodo />
    </StrictMode>
  </Provider>,
  rootElement
);

reportWebVitals();

②action

ActionのtypeをStringでつくって、ActionCreator関数でObjectを返す。

action.js
const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";
const COMPLETE_TODO = "COMPLETE_TODO";

export const addTodoCreator = (text) => {
  return {
    type: ADD_TODO,
    text
  };
};

export const deleteTodoCreator = (id) => {
  return {
    type: DELETE_TODO,
    id
  };
};

export const completeTodoCreator = (id) => {
  return {
    type: COMPLETE_TODO,
    id
  };
};

③reducer

actionのtypeに応じて、新しいstateを返します。(mutateしちゃだめ)

reducer.js
let count = 0;

const rootReducer = (state = [], action) => {
    switch (action.type) {
      case "ADD_TODO":
        return [...state, { id: count++, todo: action.text, completed: false, deleted: false }];
      case "COMPLETE_TODO":
        let todo = [];
        state.forEach(item => {
          if(item.id === action.id)
            item.completed = true;

          todo.push(item);
        })
          return todo;
      case "DELETE_TODO":
        let todoarr = [];;
        state.forEach(item => {
          if(item.id === action.id)
            item.deleted = true;

            todoarr.push(item);
        })
          return todoarr;
      default:
        return state;
    }
  };

  export default rootReducer;

最後にstateをReactでレンダリング

Todo.js
import React, {useState} from 'react'
import {deleteTodoCreator,completeTodoCreator, addTodoCreator} from '../redux/action'
import {connect} from 'react-redux'
import styles from './styles.css'
import ConnectedItem from './Item'

const Todo = (todo) => {
  const [input, setInput] = useState('');
  let todoArr = todo.todo;

  function submitTodo(text) {
    todo.addTodo(text);
    setInput('');
  }


  return (
    <div>
      <form>
        <h2>Todo Input</h2>
        <input type="text" value={input} onChange={(e) => setInput(e.target.value)}/>
        <button onClick={(e)=> {e.preventDefault(); submitTodo(input)}}>Submit</button>
      </form>
        {todoArr.length !== 0 ? (<ConnectedItem/>):(<div></div>)}
    </div>
  );
}

const mapStateToProps = (state) => {
  return {
    todo: state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    addTodo: (todo) => dispatch(addTodoCreator(todo)),
    completeTodo: (id) => dispatch(completeTodoCreator(id)),
    deleteTodo: (id) => dispatch(deleteTodoCreator(id))
  }
}

const ConnectedTodo = connect(mapStateToProps,mapDispatchToProps)(Todo)

export default ConnectedTodo

子コンポーネント:

Item.js
import React from 'react'
import style from './styles.css'
import {connect} from 'react-redux'
import {deleteTodoCreator,completeTodoCreator, addTodoCreator} from '../redux/action'


const Item = (todo) => {
  let todoArr = todo.todo;

  function completeTodoItem(id) {
    todo.completeTodo(id);
  }

  function deleteTodoItem(id) {
    todo.deleteTodo(id);
  }

  return (
    <ul>
      {todoArr.map(item => 
        item.deleted === false ? 
          (<li key={item.id} className="todoitem">
          <input type='checkbox' onChange={() => completeTodoItem(item.id)} />
          {item.completed === true? (<div>completed</div>):(<div></div>)}
          <br/>
        {item.todo}
       <button onClick={() => deleteTodoItem(item.id)}>Delete?</button>
        </li>):(<div></div>)
      )}
    </ul>
  )
}

const mapStateToProps = (state) => {
  return {
    todo: state
  }
}

const mapDispatchToPorps = (dispatch) => {
  return {
    addTodo: (todo) => dispatch(addTodoCreator(todo)),
    completeTodo: (id) => dispatch(completeTodoCreator(id)),
    deleteTodo: (id) => dispatch(deleteTodoCreator(id))
  }
}

const ConnectedItem = connect(mapStateToProps,mapDispatchToPorps)(Item)


export default ConnectedItem

改めてReduxを書いてみた気づき:

store, reducer, actionを宣言するところまでは良いが、connectするところがめちゃくちゃ面倒というか、書き方やそもそもどういうものなのかという機能も完全に忘れていました。

特にReduxをはじめたばかりのころはmapStateToPropsmapDispatchToPropsってそもそも何で、何のためにconnectするのか?をまったく分かっていなかったのですが、長期間触っていなかったのでこれを完全に忘れていました。

簡単に言えば、mapStateToPropsはReduxで管理しているstateをReactのコンポーネントで使えるようにするため、mapDispatchToPropsはReactからaction関数をdispatch出来るようにするためで、connectでコンポーネントに紐づけます。

ここでconnectして初めてReactのコンポーネント上でpropsとしてこのaction関数やstateが使えるようになります。

React HooksでRedux Toolkitを使う

そもそもRedux Toolkitが今回初めてだったのですが、案外簡単にできてしまいました。

①store

Redux toolkitではcreateStoreconfigureStoreになりました。

index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import todoSlice from "./redux/reducer";

const store = configureStore({
  reducer: todoSlice.reducer
});

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={store}>
    <StrictMode>
      <App />
    </StrictMode>
  </Provider>,
  rootElement
);

②createSlice

最初これを見たときはなんじゃこりゃと思ったのですが、actionとreducerを合体して宣言できるのがcreateSliceのようです。(別々にやる方もある)

しかも、ReduxやReactだと基本stateは直接いじるのはダメ(don't mutate the state)!🙅ってこっぴどく学ぶと思うのですが、完全に直接いじってるように見えます。

が、この辺りはtoolkitだとImmerを使ってReducerがちゃんとmutateしないように内部でなっているようです。(参照: https://redux-toolkit.js.org/usage/immer-reducers)

Reduxの今までだとどうしてもコード量が最低でも多くなってしまうのが面倒、という人が多かったと思うのですが、toolkitだとcreateSliceなどを使って簡潔に書けるのはよさそうです。

reducer.js

const todoSlice = createSlice({
  name: "todo",
  initialState: {
    todo: []
  },
  reducers: {
    addTodo(state, action) {
      state.todo.push({todo: action.payload})
    },
    toggleTodo(state, action) {
      const todo = state.todo.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    }
  }
})

export default todoSlice

③Redux Hooksも使っちゃう

Redux Hooksというのも出ており、useSelectoruseDispatchが使えることで「Reduxワカラナイ」の原因になっていたmapStateToPropsmapDispatchToPropsを使わなくてもReduxのstateが取れるようになりました。

これでかなりコード量も減ってシンプルに書けるようになったのではないでしょうか。

App.js
import "./styles.css";
import {useSelector, useDispatch} from 'react-redux'
import todoSlice from './redux/reducer'
import {useState} from 'react'

const {addTodo, deleteTodo} = todoSlice.actions;

export default function App() {
  const todo = useSelector(state => state.todo)
  const dispatch = useDispatch()

  const [input, setInput] = useState('')
  return (
    <div className="App">
      <h2>Todo</h2>
      <ul>{todo.map(item => <li key={Math.random()}>{item.todo}</li>)}</ul>
      <form>
        <input type="text" value={input} onChange={(e)=>setInput(e.target.value)}/>
        <button onClick={(e) => {e.preventDefault(); dispatch(addTodo(input))}}>Submit</button>
      </form>
    </div>
  );
}

Typescriptはまだ勉強中なので、次はこれにさらにTypescriptも導入してやっていきたいと思います!!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3