[Day 28 - 小试身手] Todolist with React (3)

在上一章Todolist with React (2),完成所有样式设定後,现在就让我们在 React 中加入 Redux,使用 React-redux 动态产生任务清单。

建立架构

将状态资料分成 Todo 任务资料和 Filter 筛选器资料,事先建立好 Store、Reducer、Action 的档案。

src
├── ...
├── actions
    ├── ActionTypes.js
    ├── filter.js
    ├── todos.js
├── reducer
    ├── filter.js
    ├── index.js
    ├── todos.js
├── store.js
  • actions:ActionTypes.js 负责定义 Todo 和 Filter 的所有 Action
  • reducer:index.js 会整合 Todo 和 Filter 的 Reducer
  • store.js:处理 Store

初始化 Task 资料

首先先处理渲染任务资料的部分,在 todos.js 定义出原始的任务资料 initialTasks,包含任务的名称和状态,方便我们产生清单。

reducer/todos.js

const initialTasks = [
  { taskName: "task1", isCompleted: false },
  { taskName: "task2", isCompleted: true },
  { taskName: "task3", isCompleted: false },
];

Todo Reducer

继续在同一份程序码建立 Todo Reducer,State 参数的预设值为之前定义的 initialTasks,目前尚未定义 Action,直接回传当前原本的 State。

reducer/todos.js

import * as types from '../actions/ActionTypes';

export default function todos(state = initialTasks, action) {
  switch (action.type) {
    default:
      return state;
  }
}

因为我们会有两份 Reducer,要用 combineReducers 将两者合并,目前还没做到 Filter Reducer,这边就先填上 Todo Reducer。

reducer/index.js

import { combineReducers } from 'redux';

import todosReducer from './todos';

const todoApp = combineReducers({
    todosReducer
});

export default todoApp;

Create Store

再来建立 Store 引入 rootReducer,透过 <Provider> 的方式来传递 Store 让网页能够读取更新状态。

src/store.js

import { createStore } from "redux";
import rootReducer from "./reducer/index";

const store = createStore(rootReducer);

export default store;

src/index.js

...
import { Provider } from "react-redux";
import store from "./store";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  ...
);

Render TaskItem

将 Store 传入 React Component 後,TaskList Component 就可以使用 useSelector 取得任务清单。原本我们是直接复制三个 <TaskItem /> 产生三个任务,修改成利用 forEach 读取 Store 的动态产生任务,并且把任务资料和编号 task={{ ...item, idx: index }} 传入 <TaskItem />

components/TaskList.js

...
import { useSelector } from "react-redux";

function TaskList() {
  const tasks = useSelector((store) => store.todosReducer);

  const renderItems = () => {
    let list = [];
    tasks.forEach((item, index) => {
        list.push(
          <TaskItem key={item.taskName} task={{ ...item, idx: index }} />
        );
    });
    return list;
  };

  return (
    <Wrapper>
      ...
      <TaskItemContainer>{renderItems()}</TaskItemContainer>
    </Wrapper>
  );
}

components/TaskItem.js

function TaskItem(props) {

  return (
    <Container>
      <CheckBox
        type="checkbox"
        checked={props.task.isCompleted}
      />
      <TaskName>{props.task.taskName}</TaskName>
      <Button>Delete</Button>
    </Container>
  );
}

新增 / 删除 Task

所有任务都能动态渲染後,再来处理任务新增 / 删除的功能,在 ActionTypes.js 定义 ADD_TASKDELETE_TASK,这样可以统一方便管理所有 Action。

actions/ActionTypes.js

export const ADD_TASK = 'ADD_TASK';
export const DELETE_TASK = 'DELETE_TASK';

新增 Task

Step1:建立 Action Creator 产生新增 Task 的动作 Function addTask,并取得新增的任务名称 taskName

actions/todos.js

import * as types from './ActionTypes';

export function addTask(taskName){
    return {
        type: types.ADD_TASK,
        taskName
    };
}

Step2:处理 Todo Reducer,读取 action.type 为 ADD_TASK 时,储存新任务的资料并回传新的 State。

reducer/todos.js

import * as types from '../actions/ActionTypes';

const initialTasks = ...

export default function todos(state = initialTasks, action) {
  switch (action.type) {
    case types.ADD_TASK:
      return [
        ...state,
        {
          taskName: action.taskName,
          isCompleted: false,
        },
      ];
    default:...
  }
}

Step3:完成 Action 和 Reducer 的设定後,在 AddTask Component 利用 useDispatch() 来调用 Action。点击 <AddBtn /> 後呼叫 handleClick(),处理新增任务的程序。

components/AddTask.js

import { useDispatch } from "react-redux";

function AddTask() {
  const dispatch = useDispatch();

  const [newTask, setnewTask] = ...
  const handleChange = ...

  const handleClick = (event) => {
    if(newTask === "") return;    //检查有没有输入任务名称
    dispatch(actions.addTask(newTask));
    setnewTask("");
  };

  return (
    <Wrapper>
        ...
      <AddBtn onClick={() => handleClick()} >
        <img src={addIcon} alt=""/>
      </AddBtn>
    </Wrapper>
  );
}

删除 Task

Step1:删除 Task 的部分也大同小异,第一步建立 Action Creator 产生删除 Task 的动作 Function deleteTask,并取得要删除的任务索引值 idx

actions/todos.js

export function deleteTask(idx){
    return {
        type: types.DELETE_TASK,
        idx
    };
}

Step2:处理 Todo Reducer,读取 action.type 为 DELETE_TASK 时,删除该任务的资料并回传的新 State。

actions/todos.js

export default function todos(state = initialTasks, action) {
  switch (action.type) {
    case types.ADD_TASK:...
    case types.DELETE_TASK:
      return [
        ...state.slice(0, action.idx),
        ...state.slice(action.idx + 1)
      ];
    default:...
  }
}

Step3:完成 Action 和 Reducer 的设定後,在 TaskItem Component 利用 useDispatch() 来调用 Action。点击 <Button/> 後直接呼叫 deleteTask(),传入要删除的任务索引值。

components/TaskItem.js

import { useDispatch } from "react-redux";

function TaskItem(props) {
  const dispatch = useDispatch();

  return (
    <Container>
      ...
      <Button onClick={() => dispatch(actions.deleteTask(props.task.idx))}>
        Delete
      </Button>
    </Container>
  );
}


小结

今天完成了渲染任务清单、和任务新增删除的动作,在下一篇文章,我们会继续完成最後一个部分 — Filter 筛选器,那我们就明天见罗!

范例程序码

如果文章中有错误的地方,要麻烦各位大大不吝赐教;喜欢的话,也要记得帮我按赞订阅喔❤️


<<:  爬虫怎麽爬 从零开始的爬虫自学 DAY27 python网路爬虫开爬8-储存问题解决

>>:  IT 铁人赛 k8s 入门30天 -- day27 Communicate Between Containers in the Same Pod Using a Shared Volume

Day4-React Hook 篇-认识 useRef & useImperativeHandle

今天要介绍的是可以用来操作 DOM 元素的 useRef 及和它有关的 useImperativeH...

[day13] 快速建构JSON Server

前面几篇做了几个交易相关的API,而为了快速跳过後端API的建置,前端网页要留存的资料源,就打算偷懒...

Day15# Goroutines

铁人赛终於要过一半了,今天要来介绍 Go 的特色之一 -- Goroutines。 那麽话不多说,我...

冒险村08 - Preitter output in rails console

08 - Preitter output in rails console Rails 的 defa...

[Day-26] R语言 - 分群应用(五) 分群预测 - 资料分群 ( data clustering in R.Studio )

您的订阅是我制作影片的动力 订阅点这里~ 影片程序码(延续昨天) #步骤二: 资料分群,哪个演算法?...