4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Redux ExampleのTodo Listをはじめからていねいにを@reduxjs/toolkitで

Last updated at Posted at 2020-02-02

概要

前回、Immerを使った書き方を試してみたが、これは @reduxjs/toolkitに組み込まれているらしい。*
なので、redux.js/toolkitを使った書き方を試してみる。
対象は未来の自分、あるいはReact触り始めたくらいの人。
Redux ExampleのTodo Listをはじめからていねいに(1)」の流れがほんとうに丁寧なのでなぞっていく。
docker上で動かしている点については前回と同様のため割愛。

環境

  • Windows 10 Home
  • virtualbox 6.1.2.20200116
  • Vagrant 2.2.7
  • ubuntu 18.04 LTS Linux ubuntu-bionic 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:24:28 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
  • docker-ce Docker version 19.03.5, build 633a0ea838
  • docker-compose version 1.25.3, build d4d1b42b
  • node 13.7
    • "@reduxjs/toolkit": "^1.2.3"
    • "react": "^16.12.0"
    • "react-dom": "^16.12.0"
    • "react-scripts": "^3.3.1"
    • "typescript": "^3.7.5"
    • "@types/react": "^16.9.19"
    • "@types/react-dom": "^16.9.5"
    • "@types/react-redux": "^7.1.7"

1. Hello World

  • まずはHello Worldを表示するところから。
  • index.tsが最上位のエンドポイント。Reactのコンポーネントをラップしている。
index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

// reactのコンポーネントを#root以下に作成する
ReactDOM.render(<App />, document.getElementById("root"));
  • 上記のファイルは以下のHTMLで読み込まれることを想定。(※DOM要素のIDがrootのものがあることに注目)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="initial-scale=1" />
  <title>TODO</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  • ReactのコンポーネントはHello Worldを表示するだけ。
components/App.tsx
import React from "react";

// React.FC は React.FunctionComponent の短縮形
// @types/reactとlib.dom.d.tsで型が衝突することがあるため、慣習としてReactの型はnamed import({FC} from 'react'みたいなやつ)を避ける
const App: React.FC = () => {
  return <div> Hello World!!! </div>;
};
export default App;

この時点のソース

2. actionCreatorで発行したactionをreducerに渡してstoreのstateを更新する

Actions

  • addTodoというactionCreatorを作る

  • actionCreatorは関数

    • actionを発行するだけ
  • actionとはactionTypeとデータで構成されるオブジェクトのこと

    • actionType = ADD_TODO
    • データ = {id: 数字, text:文字列}
    • FSAという推奨された規約がある
      • データをpayloadで渡す制約
      • payloadの型はなんでもよい。
        • payload: string
        • payload: number
        • payload: {id: number, text: string}
  • 参考:createAction , createAction(ts)

  • actionCreatorによって作られたactionはdispatchに渡される(後述)

actions/index.ts
import { createAction } from '@reduxjs/toolkit';

let nextTodoId = 0;
export const addTodo = createAction('ADD_TODO', (text: string) => ({ payload: { id: nextTodoId++, text } }));

Reducers

  • 純粋な関数
    • 現在のstateとactionを受け取り、新しいstateを返す。
    • 状態を持ってはいけない(=スコープの外から変数を持ってくるのはNG。)
      • 例えば、actionで使っていたnextTodoIdのようなことはダメ。
  • todoというreducerの動作について
    • actionTypeがADD_TODOのとき
    • { id: action.id, text: action.payload }という新しいstateを返す。
  • 参考: createReducer, createReducer(ts)
  • createReducerの第一引数には初期状態を入れておく
  • createReducerでbuilderを使うと、typescriptを使っている場合、actionの型を自動で推測してくれる
reducers/index.ts
import { createReducer } from '@reduxjs/toolkit';
import { addTodo } from "../actions";

function initialState() {
  return { id: 0, text: '' };
}

export const reducer = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => (action.payload))
);

Store

  • アプリケーションで単一
  • stateを保持する。
  • configureStore関数でreducerを呼び出すことで作られる。
  • 参考:configureStore
index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { configureStore } from '@reduxjs/toolkit';
import App from "./components/App";
import reducer from './reducers';

const store = configureStore({ reducer });

ReactDOM.render(<App />, document.getElementById("root"));

流れを確認

index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { configureStore } from '@reduxjs/toolkit';
import App from "./components/App";
import reducer from './reducers';
+ import { addTodo } from "./actions";

const store = configureStore({ reducer });


+ store.dispatch(addTodo("Hello World!"));
+ console.log(store.getState());

ReactDOM.render(<App />, document.getElementById("root"));
  • action -> reducer -> storeの一連の流れ
    • 実際にstateが更新されるのを見る
  1. addTodo('Hello World!')
  • actionCreator
  • {payload: { id: 0, text: 'Hello World' }}というactionを作成
  1. store.dispatch(addTodo("Hello World!"));
  • dispatch関数にactionを渡す
  • todo reducerの引数にstateとactionが渡される
    • state = 現在のstate(初期値の{id:0, text ''})
    • action = {payload: { id: 0, text: 'Hello World' }},
  • todo reducerでは、新しいstate = { id: 0, text: 'Hello World' }を返す。
  1. store.getState()
  • storeが保持しているstateを取得
chrome developer toolで確認。

image.png

この時点のソース

3. storeで保持したstateをViewで表示する

Todo Listの作成

reducerを書き換える

todoのreducerを別ファイルに外だし
  • stateでtodoを保持することはできた
  • このままでは1つのtodoしか保持できない
  • 複数のtodoを保持できるよう拡張
  • 今後のことを考えtodosのreducersを別ファイル(index.js -> todos.js)にする。
    • exampleではtodos以外のreducerを使うため
    • 1つしか使わない場合は別ファイル不要
  • state.pushで、一見stateを直接書き換えているように見えるが実は新しいstateを作って返している
@types/index.d.ts
export interface ITodo {
  id: number;
  text: string;
}
reducers/todos.ts
import { createReducer } from '@reduxjs/toolkit';
import { addTodo } from '../actions';
import { Todo } from '../@types';


function initialState(): ITodo [] {
  return [];
}

const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
      state.push(action.payload);
    })
);
export default todos;
reducer/index.tsで外だししたreducerを統合
  • todoreducerを別ファイルにしたため、reducers/index.tsは各reducerを統合する役割にする
  • configureStorereducerプロパティ」に、「各reducerをプロパティにしたオブジェクト」を入れると、内部的にcombineReducersの挙動をする
  • combineReducers
    • 複数のreducersを結合する
    • reducerが結合された場合のstateの挙動も変化する
  • storeで作成されるstateの違い
    • createStore(todos)
      • state = [todo1, todo2]
    • createStore(combineReducers(todos))
      • state = {todos = [todo1, todo2]}
reducers/index.ts
import todos from "./todos";

const rootReducer = { todos };
export default rootReducer;

この時点のソース

ToDoコンポーネントとToDoListコンポーネントを作る

Todoコンポーネント

  • propとして渡されてきたtextを表示する
components/Todo.tsx
import React from "react";

const Todo: React.FC<{ text: string }> = ({ text }) => {
  return <li>{text} </li>;
};

export default Todo;

TodoListコンポーネント

components/TodoList.tsx
import React from "react";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';

const TodoList: React.FC = props => {
  const todos = useTodoItems();
  return (
    <ul>
      {todos.map(todo => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
};

export default TodoList;
  • reducerと一緒にselectorも定義してしまう
reducers/todos.tsx
import { createReducer } from '@reduxjs/toolkit';
+ import { useSelector } from "react-redux";
import { addTodo } from '../actions';
import { ITodo } from '../@types';

function initialState(): ITodo[] {
  return [];
}

const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
      state.push(action.payload);
    })
);
export default todos;


+ export const useTodoItems = () => {
+   return useSelector((state: { todos: ReturnType<typeof todos> }) => state.todos);
+ }

Providerの設定

  • 作成したstoreをコンポーネントで使用するため、一番大きい単位を<Provider>で囲む
  • Providerの属性に作成したstoreを指定する
index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { configureStore } from '@reduxjs/toolkit';
import App from "./components/App";
import reducer from './reducers';
import { addTodo } from "./actions";


const store = configureStore({ reducer });
store.dispatch(addTodo("Hello World!"));
console.log(store.getState());

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

ブラウザで確認

image.png

この時点のソース

4. フォームからtodoを追加

components/AddTodo.tsx
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../actions";

const AddTodo: React.FC = props => {
  // local state
  const [input, setInput] = useState("");
  // dispatch を用意
  const dispatch = useDispatch();
  // ハンドラーを用意。タスクを追加したらテキストエリアのクリア
  const clickHandler = () => {
    if (input !== "") {
      dispatch(addTodo(input));
      setInput("");
    }
  }

  return (
    <div>
      <input
        type="text"
        onChange={e => {
          setInput(e.target.value);
        }}
        value={input}
      />
      <button onClick={clickHandler}>add</button>
    </div>
  );
};

export default AddTodo;
  • App.tsxにAddTodoコンポーネントを追加
components/App.tsx
import React from "react";
import AddTodo from './AddTodo';
import ToddoList from './TodoList';

const App: React.FC = () => {
  return <div>
    <AddTodo />
    <ToddoList />
  </div>;
};

export default App;
ブラウザで確認

image.png

この時点のソース

5. 完了・未完了を表すcompletedによってスタイルを変える

ここからは、Todoの完了・未完了を切り替える「Toggle Todo」の機能を作っていく。

todoにcompleted要素を追加して、とりあえず取り消し線を表示する

  • completed要素を追加
    • todoごとに完了・未完了を区別するため
    • デフォルトはfalse
      • todo作成時は、未完了のため
@types/index.d.ts
export interface ITodo {
  id: number;
  text: string;
+  completed: boolean;
}
reducers/todos.ts
const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
-      state.push(action.payload);
+      state.push(
+        {
+          completed: false,
+          ...action.payload,
+        });
    })
);

Todoコンポーネントを修正

  • completedがtrueだったらtextDecorationをline-throughにする
components/Todo.tsx
import React from "react";

const Todo: React.FC<{ completed: boolean, text: string, }> = ({ completed, text }) => {
  return <li style={{ textDecoration: completed ? 'line-through' : 'none' }}>
    {text}
  </li>;
};

export default Todo;
  • 一時的にreducers/todos.jsのcompleted: falseをtrueにして動作確認

image.png

この時点のソース

actionCreatorからcompleted要素を操作する

  • action経由で取り消し線のON/OFFを行う

actionCreatorの追加

  • actionCreatorで必要なのは、todoのidだけ
actions/index.ts
import { createAction } from '@reduxjs/toolkit';

let nextTodoId = 0;
export const addTodo = createAction('ADD_TODO', (text: string) => ({ payload: { id: nextTodoId++, text } }));
+ export const toggleTodo = createAction('TOGGLE_TODO', (id: number) => ({ payload: id }));

reducerの追加

  • payloadで送られたidと同じidの要素のcompletedを反転させる
  • ここでは.mapで要素を作り直しているため、immerは使用していない
reducers/todos.ts
const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
      state.push(
        {
          completed: true,
          ...action.payload,
        });
    })
+    .addCase(toggleTodo, (state, { payload }) => state.map(todo => ({ ...todo, completed: todo.id === payload ? !todo.completed : todo.completed })))
);
  • dispatchを呼んで、true/falseが変更されるのを確かめてみる
index.tsx
 import reducer from './reducers';
-import { addTodo } from "./actions";
+import { addTodo, toggleTodo } from "./actions";

 const store = configureStore({ reducer });
 store.dispatch(addTodo("Hello World!"));
 console.log(store.getState());
+store.dispatch(toggleTodo(0));

image.png

この時点のソース

6. クリックしてcompletedの値を変える

子コンポーネントの修正

  • onClickイベントで親から渡されたtoggle関数を実行する
components/Todo.tsx
import React from "react";

const Todo: React.FC<{ completed: boolean, text: string, toggle: () => void }> = ({ completed, text, toggle }) => {
  return <li style={{ textDecoration: completed ? 'line-through' : 'none' }} onClick={toggle}>
    {text}
  </li >;
};

export default Todo;

親コンポーネントの修正

components/TodoList.tsx
import React from "react";
import { useDispatch } from "react-redux";
import { toggleTodo } from "../actions";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';

const TodoList: React.FC = props => {
  const todos = useTodoItems();
  const dispatch = useDispatch();

  return (
    <ul>
      {todos.map(todo => <Todo key={todo.id} {...todo} toggle={() => dispatch(toggleTodo(todo.id))} />)}
    </ul>
  );
};

export default TodoList;

この時点のソース

7 Filter Todo

  • 表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」機能を作る
  • 以下の3つのフィルターによって表示を変更する
    • SHOW_ALL: 全部表示
    • SHOW_COMPLETED: 完了しているtodoのみ
    • SHOW_ACTIVE: 完了していないtodoのみ
  • 開発順は以下とする
    1. actionCreatorとreducerでフィルターの値をstore(state)に格納
    2. フィルターの値によってviewを変更(手動でフィルターを操作して動作確認)
    3. リンクをクリックしてフィルターを操作してviewを変更

7-1 actionCreatorとreducerでフィルターの値をstore(state)に格納

moduleの作成

  • reducerとactionsを同時につくる
@types/index.d.ts
export type FilterType = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';
modules/visibleFilterModule.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { useSelector } from "react-redux";
import { FilterType } from '../@types';

function initialState(): FilterType {
  return 'SHOW_ALL';
}

// actions と reducers の定義
const visibilityFilterModules = createSlice({
  name: "visibilityFilter",
  initialState: initialState(),
  reducers: {
    visibilityFilter: (state, action: PayloadAction<FilterType>) => action.payload,
  }
});
export default visibilityFilterModules;
reducers/index.ts
import todos from "./todos";
import visibleFilterModule from '../module/visibilityFilterModules';
const rootReducer = { todos, visibleFilter: visibleFilterModule.reducer };
export default rootReducer;

手動で動作確認

index.tsx
// 省略
import visibilityFilterModules from './module/visibilityFilterModules';

const store = configureStore({ reducer });

console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_ALL"}
store.dispatch(visibilityFilterModules.actions.visibilityFilter('SHOW_COMPLETED'))
console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_COMPLETED"}

// 省略

image.png

この時点のソース

7-2. フィルターの値によってviewを変更

module/visibilityFilterModule.ts
// 省略
export const useVisibleFilter = () => {
  return useSelector((state: { visibleFilter: ReturnType<typeof visibilityFilterModules.reducer> }) => state.visibleFilter);
}
components/TodoList.tsx
import React from "react";
import { useDispatch } from "react-redux";
import { toggleTodo } from "../actions";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';
import { useVisibleFilter } from '../module/visibilityFilterModules';

const TodoList: React.FC = props => {
  let todos = useTodoItems();
  const dispatch = useDispatch();
  const filter = useVisibleFilter();

  switch (filter) {
    case 'SHOW_ALL':
      break;
    case 'SHOW_COMPLETED':
      todos = todos.filter((t) => t.completed);
      break;
    case 'SHOW_ACTIVE':
      todos = todos.filter((t) => !t.completed);
  }

  return (
    <ul>
      {todos.map(todo => <Todo key={todo.id} {...todo} toggle={() => dispatch(toggleTodo(todo.id))} />)}
    </ul>
  );
};

export default TodoList;

7-3. リンクをクリックしてフィルターを操作してviewを変更

  • リンクを表示させる
  • dispatch(setVisibilityFilter())を呼び出す
  • クリックしたときにonClickを呼ぶ
components/Link.tsx
import React from "react";

const Link: React.FC<{ children: any, onClick: () => void }> = ({ children, onClick }) => {
  // eslint-disable-next-line jsx-a11y/anchor-is-valid
  return <a href="#" onClick={(e) => { e.preventDefault(); onClick(); }}>{children}</a>;
};

export default Link;
components/Footer.tsx
import React from "react";
import { useDispatch } from "react-redux";
import Link from './Link';
import visibilityFilterModules from '../module/visibilityFilterModules';

const Footer: React.FC = () => {
  const dispatch = useDispatch();
  const visibilityFilter = visibilityFilterModules.actions.visibilityFilter;

  return <p>
    Show:
  {" "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ALL'))}>
      All
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ACTIVE'))}>
      Active
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_COMPLETED'))}>
      Completed
  </Link>
  </p>;
};

export default Footer;
components/App.tsx
import React from "react";
import AddTodo from './AddTodo';
import ToddoList from './TodoList';
+ import Footer from './Footer';

const App: React.FC = () => {
  return <div>
    <AddTodo />
    <ToddoList />
+    <Footer />
  </div>;
};

export default App;

activeな状態のリンクを押せないようにする

components/Link.tsx
import React from "react";

const Link: React.FC<{ active: boolean, children: any, onClick: () => void }> = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>;
  }
  // eslint-disable-next-line jsx-a11y/anchor-is-valid
  return <a href="#" onClick={(e) => { e.preventDefault(); onClick(); }}>{children}</a>;
};

export default Link;
components/Footer.tsx
import React from "react";
import { useDispatch } from "react-redux";
import Link from './Link';
import visibilityFilterModules, { useVisibleFilter } from '../module/visibilityFilterModules';

const Footer: React.FC = () => {
  const dispatch = useDispatch();
  const visibilityFilter = visibilityFilterModules.actions.visibilityFilter;
  const filter = useVisibleFilter();

  return <p>
    Show:
  {" "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ALL'))} active={filter === 'SHOW_ALL'}>
      All
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ACTIVE'))} active={filter === 'SHOW_ACTIVE'}>
      Active
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_COMPLETED'))} active={filter === 'SHOW_COMPLETED'}>
      Completed
  </Link>
  </p>;
};

export default Footer;

image.png

この時点のソース

これでactiveな状態のリンクを押せないようになり、「Filter Todo」機能が完成。

備考

メモ化によるパフォーマンス改善について

  • これくらいの小さいものなら不要そう。
  • 子コンポーネントに dispatch を渡す場合、useCallbackを利用して、メモ化する
  • 親の再レンダリングによって子コンポーネントが不必要に再レンダリングされることを回避するため

子コンポーネントの修正

  • onClickイベントで親から渡されたtoggle関数を実行する
  • React.memoでpropsが変わらなかったときには変更されないようにする。
components/Todo.tsx
import React from "react";

const Todo: React.FC<{ completed: boolean, text: string, id: number, toggle: (id: number) => void }> = React.memo(({ completed, text, id, toggle }) => {
  return <li style={{ textDecoration: completed ? 'line-through' : 'none' }} onClick={() => toggle(id)}>
    {text}
  </li >;
});

export default Todo;

親コンポーネントの修正

  • useCallbackでメモ化する
  • dispatchでActionを呼び出す関数を子コンポーネントに渡す
  • ここでuseCallbackを行うことにより、関数は不変なものとして子コンポーネントに渡される
    • 使わなかった場合、アロー関数は毎回違うオブジェクトと認識されてしまう。
      • せっかく、React.memoでpropsが変わらなかったら再レンダリングしないとしている意味がなくなる
      • React.memouseCallbackはバグのもとになりやすいので、パフォーマンス上問題なければ使わないほうが無難かも
components/TodoList.tsx
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { toggleTodo } from "../actions";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';

const TodoList: React.FC = props => {
  const todos = useTodoItems();
  const dispatch = useDispatch();
  const clickHandler = useCallback((id: number) => dispatch(toggleTodo(id)), [dispatch]);
  return (
    <ul>
      {todos.map(todo => <Todo key={todo.id} {...todo} toggle={clickHandler} />)}
    </ul>
  );
};

export default TodoList;

効率化?

  • memoを使った場合と、使わない場合で、dispatchの動作は、一見、変わっていないように見えた
    • DOMでの確認なので、内部的には違うのかも
  • liがすべて書き換わるようだったら、問題だけど、これならパフォーマンスが問題になるまで対応不要かも。

image.png

関数メモ化時点のソース

環境詳細

バージョン

OS

  • Windows 10 Home

アプリケーション

PS C:\WINDOWS\system32> clist -l | Select-String "virtualbox"
virtualbox 6.1.2.20200116
PS C:\WINDOWS\system32> vagrant -v
Vagrant 2.2.7

仮想環境

vagrantfile

vagrant@ubuntu-bionic[]:~$ uname -a
Linux ubuntu-bionic 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:24:28 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
vagrant@ubuntu-bionic[]:~$ docker -v
Docker version 19.03.5, build 633a0ea838
vagrant@ubuntu-bionic[]:~$ docker-compose --version
docker-compose version 1.25.3, build d4d1b42b

インストールしたパッケージ

package.json
{
  "name": "my-react",
  "version": "0.1.0",
  "private": true,
  "license": "MIT",
  "dependencies": {
    "@reduxjs/toolkit": "^1.2.3",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-redux": "^7.1.3",
    "redux": "^4.0.5"
  },
  "devDependencies": {
    "@types/jest": "^25.1.1",
    "@types/node": "^13.5.3",
    "@types/react": "^16.9.19",
    "@types/react-dom": "^16.9.5",
    "@types/react-redux": "^7.1.7",
    "@types/redux": "^3.6.0",
    "react-scripts": "^3.3.1",
    "typescript": "^3.7.5"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

参考

Redux の記述量多すぎなので、 Redux の公式ツールでとことん楽をする。 ( Redux Toolkit)
フック API リファレンス
Redux ExampleのTodo ListをはじめからていねいにをもういちどTypescriptとImmerで
redux-toolkit
Redux Starter KitでHooksとReduxを使いこなそう
ハンバーガー屋で隣の女子高生が語るReact 第3話 Hooks
Redux ExampleのTodo Listをはじめからていねいに(1)
React HooksのuseCallbackを正しく理解する
最近Reactを始めた人向けのReact Hooks入門
雰囲気で使わない React hooks の useCallback/useMemo
本当は怖いReact.memo
Reduxの個人的チュートリアル(redux-toolkitあり)

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?