iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Modern Web

開始搞懂React生態系系列 第 21

Day 21 全站狀態管理的利器 - Redux (三) 元件使用 Redux

  • 分享至 

  • xImage
  •  

前情提要

在前面二篇,我們把 Redux API 的重點 Store、Actions、Reducer 都介紹完成了,也用範例來示範了建置的方式,接下來就要示範 React Compoent 要如何去使用 Redux 來完成狀態管理。

提供 Store 給 React App 使用

這個在前面介紹 Store 時有提到,元件要使用 Store,就必需使用 Provider 語法包住元件,並指定 Store 給 Provider。

// src/index.js
import { Provider } from 'react-redux';
import { App } from './App';

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

元件使用 Redux

在元件內使用 Redux,就需要用到二個 React Hook 如下

  • 使用 useSelector 取得 state 與元件綁定
  • 使用 useDispatch 取得 dispatch 讓事件觸發

useSelector 取得 state

useSelector Hook Function 需要傳入一個參數,這個參數也是 Function,在這個 Function 裡定義要如何從所有 state 中挑選需要的 state

import { useSelector } from 'react-redux';

const filter = useSelector((state) => state.filter);
const todos = useSelector((state) => state.todos);

useDispatch 取得 dispatch

使用 useDispatch 取得 dispatch 後,就可以套件前面做好的 Action Creators Function,讓事件透過 dispatch 發出 Action 及 Payload。

import { useRef } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from 'store/actins';

const TodoTextInput = () => {
  const dispatch = useDispatch();
  const inputRef = useRef(null);

  const onSubmitHandler = (e) => {
    if (e.which === 13) {
      if (!inputRef.current.value.trim()) {
        return;
      }
      dispatch(addTodo(inputRef.current.value));
      inputRef.current.value = "";
    }      
  };
    
  return (
    <div>
      <input
        type="text"
        ref={inputRef}
        onKeyDown={onSubmitHandler}
      />
    </div>
  );
}

完整的 TODO MVC 使用 React Reducx 的範例

前面的程式碼片段,比較偏重擷取重點呈現的部分,幫助大家一點一滴的去理解應用程式如何套用 Redux,在熟悉了整個 Redux 操作後,這邊提供了一個 CodeSandbox 模版,讓大家試著從頭開始建立 React Todo MVC with Redux。

單純使用 useState 的 TODO MVC

元件的相依性示意如下,如果要共享狀態,要層層傳遞 props

CodeSandbox 模版如下

https://codesandbox.io/s/react-todomvc-usestate-rjbu0w

你可以 Fork 一份,來試看看製作 Redux 的版本

改成使用 Redux 的 TODO MVC

建立 Store

  • src/store/index.js Store 模組

建立 Store 元件,並將 reducers 與其綁定

import { createStore } from "redux";
import reducers from "./reducers";
const store = createStore(reducers);
export default store;
  • src/action/actionTypes.js 建立 Action Types
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const DELETE_TODO = "DELETE_TODO";
export const SET_FILTER = "SET_FILTER";
  • src/action/index.js 建立 Action Creators
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER } from "./actionTypes";

export const addTodo = (text) => {
  return {
    type: ADD_TODO,
    id: new Date().getTime().toString(),
    text
  };
};

export const toggleTodo = (id) => {
  return {
    type: TOGGLE_TODO,
    id
  };
};

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

export const setFilter = (filter) => {
  return {
    type: SET_FILTER,
    filter
  };
};
  • src/reducers/index.js 建立 reducers 集合
import { combineReducers } from "redux";
import todos from "./todosReducer";
import filter from "./filterReducer";

const reducers = combineReducers({
  todos,
  filter
});

export default reducers;
  • src/reducers/todosReducer.js

用來對應與 todos state 相關的 action

import { ADD_TODO, TOGGLE_TODO, DELETE_TODO } from "./../actions/actionTypes";

const todosReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case TOGGLE_TODO:
      return state.map((todo) => {
        if (todo.id !== action.id) {
          return todo;
        }
        return Object.assign({}, todo, {
          completed: !todo.completed
        });
      });
    case DELETE_TODO:
      return state.filter((todo) => {
        return todo.id !== action.id;
      });
    default:
      return state;
  }
};

export default todosReducer;
  • src/reducers/filterReducer.js

用來對應與 filter state 相關的 action

import { SET_FILTER } from "./../actions/actionTypes";

const filterReducer = (state = "All", action) => {
  switch (action.type) {
    case SET_FILTER:
      return action.filter;
    default:
      return state;
  }
};

export default filterReducer;

使用 Store 讓元件操作 Redux

  • src/index.js React 程式進入點

在此使用 Provider 為整個專案加上 Store

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import store from "./store";
import "todomvc-app-css/index.css";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
  • src/components/App.js 根元件
import React from "react";
import Header from "./Header";
import Footer from "./Footer";
import TodoList from "./TodoList";

const App = () => {
  return (
    <div>
      <Header />
      <TodoList />
      <Footer />
    </div>
  );
};

export default App;
  • src/components/Header.js

Header 元件

import React, { useRef } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../store/actions";
import TodoTextInput from "./TodoTextInput";

const Header = () => {
  const dispatch = useDispatch();
  const inputRef = useRef(null);

  const onSubmitHandler = (e) => {
    if (e.which === 13) {
      if (!inputRef.current.value.trim()) {
        return;
      }
      dispatch(addTodo(inputRef.current.value));
      inputRef.current.value = "";
    }
  };

  return (
    <header className="header">
      <h1>todos</h1>
      <TodoTextInput
        ref={inputRef}
        placeholder="What needs to be done?"
        onSubmit={onSubmitHandler}
      />
    </header>
  );
};

export default Header;
  • src/components/TodoTextInput.js

Header 裡包含的 TodoTextInput 元件

這邊使用了 forwardRef 把 原生 input 的參考曝露出去,讓上層元件操作

把對 Redux 的操作放在 專屬於應用程式的 Container 類型元件是比較好的作法

這樣基礎元件能夠被其他元件重用性會更高。

import React, { forwardRef } from "react";

const TodoTextInput = forwardRef((props, ref) => {
  const { placeholder, onSubmit } = props;
  return (
    <div>
      <input
        type="text"
        className="new-todo"
        ref={ref}
        placeholder={placeholder}
        onKeyDown={onSubmit}
      />
    </div>
  );
});

export default TodoTextInput;
  • src/components/TodoList.js

TodoList 元件

import React from "react";
import TodoItem from "./TodoItem";
import { useDispatch, useSelector } from "react-redux";
import { toggleTodo, deleteTodo } from "../store/actions";

const TodoList = () => {
  const todos = useSelector((state) => state.todos);
  const filter = useSelector((state) => state.filter);
  const dispatch = useDispatch();

  // 對應狀態的TodoList
  const filteredTodos = todos.filter((item) => {
    if (filter === "Completed") {
      return item.completed;
    }
    if (filter === "Active") {
      return !item.completed;
    }
    return true;
  });

  return (
    <>
      <section className="main">
        <ul className="todo-list">
          {filteredTodos.map((todo) => (
            <TodoItem
              key={todo.id}
              todo={todo}
              onToggleItem={() => dispatch(toggleTodo(todo.id))}
              onDeleteItem={() => dispatch(deleteTodo(todo.id))}
            />
          ))}
        </ul>
      </section>
    </>
  );
};

export default TodoList;
  • src/components/TodoItem.js

TodoList 下的 TodoItem 元件

import React from "react";
import classnames from "classnames";

const TodoItem = ({ onToggleItem, onDeleteItem, todo }) => {
  const { text, completed } = todo;

  return (
    <li
      className={classnames({
        completed: completed
      })}
    >
      <div className="view">
        <input className="toggle" type="checkbox" checked={completed} onChange={onToggleItem} />
        <label>{text}</label>
        <button className="destroy" onClick={onDeleteItem} />
      </div>
    </li>
  );
};

export default TodoItem;
  • src/components/Footer.js

Footer 元件 用來切換 Filter

import React from "react";
import classnames from "classnames";
import { useDispatch, useSelector } from "react-redux";
import { setFilter } from "../store/actions";

const Footer = (props) => {
  const todos = useSelector((state) => state.todos);
  const filter = useSelector((state) => state.filter);
  const dispatch = useDispatch();
  // 待完成的TodoList  
  const activeTodos = todos.filter((item) => !item.completed);
  const itemWord = activeTodos.length === 1 ? "item" : "items";
  const FILTER_TITLES = ["All", "Active", "Completed"];

  return (
    <footer className="footer">
      <span className="todo-count">
        <strong>{activeTodos.length || "No"}</strong> {itemWord} left
      </span>
      <ul className="filters">
        {FILTER_TITLES.map((filterTitle) => (
          <li key={filterTitle}>
            <a
              className={classnames({ selected: filterTitle === filter })}
              style={{ cursor: "pointer" }}
              onClick={() => dispatch(setFilter(filterTitle))}
            >
              {filterTitle}
            </a>
          </li>
        ))}
      </ul>
    </footer>
  );
};

export default Footer;

完成後的 CodeSandbox 連結如下

https://codesandbox.io/s/react-todomvc-redux-6qnps3

總結

可以觀察 Redux 與元件的關係,只要把 Store 提供給 APP,在任何層級的元件,就可以使用 useSelector 取得 Store 裡的任意 state 來使用,同時 state 也只會用預先定義好的 Action(規則) 做更新。

selector 會辨識元件選取的 state 是否有被更新,所以 todos 更新,不會造成 filter 相關的元件被 Re-Render。

Components - Container v.s. Presentational

這邊裡我們的 TodoTextInput 及 TodoItem 元件的資料,還是透過 props 傳入,因為我們會希望這種只是展現 UI 的基礎元件是能夠被其他上層元件重複使用組合的,所以讓它們與 Redux 連結並不合適。

TodoTextInput 及 TodoItem 是 Presentational Component,而 Header、TodoList 及 Footer 則歸類為 Container Components

Presentational Components

  • 用途:呈現 UI
  • Redux 連結:否
  • 取資料:從 props 拿
  • 寫資料:從 props 呼叫 callback function

Container Components

  • 用途:擷取資料,更新 State
  • Redux 連結:是
  • 取資料:使用 useSelector 拿到 State
  • 寫資料:使用 dispatch(useDispatch) 發出 Action

Presentational Component 主要負責單純的 UI 的渲染,而 Container 則負責和 Redux 的 Store 溝通。這樣的分法可以讓程式架構和職責更清楚。

Next

開發者要正確的使用 Redux,就要先掌握 Store、Action、Reducer 這些基本概念,但如果想進一步透過 Redux 處理非同步、API 請求等進階需求,就要學會 Redux Middleware。

接下來會先介紹 Redux Middleware 的基本觀念,再來介紹 Redux 衍生出來的生態系套件。

Reference

https://www.freecodecamp.org/news/what-is-redux-store-actions-reducers-explained/

https://www.laitimes.com/article/1od0a_1u849.html

https://chentsulin.github.io/redux/index.html

https://pjchender.dev/webdev/note-without-redux/

https://ithelp.ithome.com.tw/articles/10219962

https://github.com/kdchang/reactjs101/blob/master/Ch08/container-presentational-component-.md


上一篇
Day 20 全站狀態管理的利器 - Redux (二) Reducer
下一篇
Day 22 Redux Middleware 基本運作原理
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言