iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
Modern Web

職缺資訊平台—Jobscanner系列 第 23

[開發] React 從 0 到 0.1 (7)

  • 分享至 

  • xImage
  •  

任務清單的實作中,可能有新增、修改、刪除的按鈕動作,如果使用 useState,三個事件處理函式都要設定 state,例如:

const [tasks, setTasks] = useState(initialTasks);

function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
}

function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
}

function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
}

handleAddTaskhandleChangeTaskhandleDeleteTask都使用了 setTasks去更改 task 內容。隨著元件的功能增加,程式碼可能會越變越複雜且不好維護。為了降低複雜度,且保持邏輯寫在同一個區塊,可以將這些狀態相關的邏輯移到一個函式中,這個函式稱為 reducer


使用 reducer 管理狀態不是直接設定新的 state,而是在告訴 React 設定 state 要做什麽事。

原本是透過事件處理函式來更新 task 狀態,改寫成 task 有三種 action,added/changed/deleted (改用行為/動作作為名稱,也更貼近使用者的角度)


function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

// 改寫成
function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

不是直接設定 task,而是使用 dispatch action,將 action 傳入 dispatch 中。
(這裡不用告訴 React 新的 state 長什麽樣子,而是告訴 React 要做的事是 added,這件事具體要做什麽則寫在 reducer 中)


再來將 state 邏輯判斷寫在 reducer 函式中。函式傳入兩個參數,current stateaction 物件,函式會回傳下一個 state。

function yourReducer(state, action) {
  // return next state for React to set
}

reducer 函式 return 的內容也就是 React 會設定的 state

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

最後在元件中加入 tasksReducer

// 載入 useReducer Hook
import { useReducer } from 'react';

// 原本使用 useState
// const [tasks, setTasks] = useState(initialTasks);

// 改為 useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer Hook 需要傳入兩個參數:

  1. reducer 函式 -> tasksReducer
  2. 初始狀態 -> initialTasks

會回傳:

  1. stateful 狀態 -> tasks
  2. dispatch 函式 (將使用者的操作 dispatch 給 reducer 的函式) -> dispatch

handleAddTask、handleChangeTask、handleDeleteTask 改用 dispatch 傳入 action

function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
}

function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
}

function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
}

把可能要做的 action 統一寫在 reducer 函式 中,觸發時使用 dispatch() 告訴 reducer函式 是哪個 action type發生,資料內容有什麽,reducer 函式 return 的內容更新至 state 中。


Heading 元件會收到 level 值,識別為 h1~h6

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Heading level={2}>Heading</Heading>
      <Heading level={3}>Sub-heading</Heading>
      <Heading level={4}>Sub-sub-heading</Heading>
      <Heading level={5}>Sub-sub-sub-heading</Heading>
      <Heading level={6}>Sub-sub-sub-sub-heading</Heading>
    </Section>
  );
}

同一個 section 包住的 heading 都使用一樣的 heading

<Section>
  <Heading level={3}>About</Heading>
  <Heading level={3}>Photos</Heading>
  <Heading level={3}>Videos</Heading>
</Section>

期望可以改成由 section 設定統一的 level

<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>

必須讓子元件去 tree 中的某個地方詢問資訊,也就是 context,使用 context 可以將父元件將資訊傳給 UI tree 中任意一個元件。


  1. 建立 context
    import { createContext } from 'react';
    
    export const LevelContext = createContext(1);
    

  1. 使用 context
    在需要資料的元件中使用 context,就像 Heading 需要用到 LevelContext

    import { useContext } from 'react';
    import { LevelContext } from './LevelContext.js';
    
    export default function Heading({ children }) {
      const level = useContext(LevelContext);
      // ...
    }
    

    useContextuseStateuseReducer 一樣也是 Hook,useContext 告訴 React Heading 元件想要讀取 LevelContext。


  1. 提供內容給 context
    Section 元件 render children

    export default function Section({ children }) {
      return (
        <section className="section">
          {children}
        </section>
      );
    }
    

    context provider 包住 children,提供 LevelContext 給 children。用來告訴 React,如果有任何 <Section> 中有任何元件要 LevelContext,就給它們 level 值,元件會使用最接近的 LevelContext.Provider。 (沒有提供 context 時,React 會使用預設值)

    import { LevelContext } from './LevelContext.js';
    
    export default function Section({ level, children }) {
      return (
        <section className="section">
          <LevelContext.Provider value={level}>
            {children}
          </LevelContext.Provider>
        </section>
      );
    }
    

context 容易被過度使用,如果要將 props 往不同層的元件傳遞,不代表一定要用 context,在使用 context 之前:

  1. 先從傳遞 props 開始,傳 props 也許變得很冗長,但也很清楚可以知道元件所使用的資料
  2. 拆分元件,使用 children 作為 prop
    <Layout posts={posts} /> 改為 <Layout><Posts posts={posts} /></Layout>

如果以上都不適用時,再去考慮 context!


小結

React 從 0 到 0.1 (1)~(7) 都是 React 官方文件 的內容,官方提供的學習文件很清楚。也會以常見情境作為範例,再用同一個例子解釋可能遇到的問題,以及可以用什麽方式改寫。

每一個主題底下有很多單元,每個單元的內容長度很剛好,閱讀起來不會有太大的負擔。此外內容中也會夾雜 DEEP DIVEPitfallNote 區塊,對實際應用很有幫助,每個單元最後也幾乎會有 Challenges,加深學習的記憶。


上一篇
[開發] React 從 0 到 0.1 (6)
下一篇
[開發] 資料彙整 - 觀察
系列文
職缺資訊平台—Jobscanner31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言