[FE302] React 基礎 - hooks 版本:再度中場休息


Posted by s103071049 on 2021-09-23

一、React 基礎總結

TodoList 的時候,比較注重在 component 之間的互動,以及基本功能的使用

  1. Component
  2. Props
  3. Style
  4. Event handler
  5. JSX
  6. state

接著關注 React 底層的一些機制,還有一些舊的東西、useEffect、life-cycle

  1. useEffect:要執行事情,要馬寫在 useEffect 裡面,要馬寫在 Event handler 裡面,因為寫在其他地方每次 render 就會跟著一起執行
  2. render 機制:避免 re-render 就和 3 4 5 有關
  3. useCallback:背後透過 useRef 實作,useRef 在不同的 render 之間保留一個 reference,功能同 useCallback
  4. useMemo:用一個 function 將你想要的值回傳回來
  5. memo:包 component
  6. Class Component:舊專案還是會看到,function component 可以被相關的 useEffect 取代
  7. useContext:解決 props drilling

建議將官方文件每一頁看過,至少對東西有印象,不求都了解,但要能知道這個名次、他在做甚麼、能用一句話簡單講出來。以後碰到類似問題,就可以知道去哪裡解決。

二、增進代碼品質

  1. eslint 開啟
  2. propType:檢查 prop 的 type 是否正確,因為 js 並沒有規定你只能寫甚麼型態,比方說今天 render 一個 this.props.nam,他可能是數字、陣列、物件...,所以 react 規定 component.propTypes = 一個物件,就可以去規定說這個 props 他吃的名字是甚麼、是甚麼型態。使用若不照這個規則,就會在 console 顯示 warning。會提示你 props 好像傳錯了。

要使用方式 2 需要改一下 eslint 的規則,在根目錄新增檔案 .eslintrc.json

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

react props type 的 rule,每個 component 都一定要加有甚麼 props。寫起來頗麻煩,因為每個 component 都要先寫好。

// TodoItem.js
import PropTypes from 'prop-types' // react 提供幫助我們定義已經有的類型

略
TodoItem.propTypes = {
  className: PropTypes.string, 
  size: PropTypes.string.isRequired, // 一定要傳,不然 console 會報錯 
  todo: PropTypes.object, // 也可以定義更詳細,裡面到底要有甚麼東西 
  handleDeleteTodo: PropTypes.func, 
  hadnleToggleIsDone: PropTypes.func
}

好處:快速知道這個 component 有甚麼參數,這些參數需要傳遞的類型是甚麼。某些 library 也會用這個文件產生東西出來。

三、補充:以 state 為中心去思考

在寫 react 時一定要做一個想法的轉換:以資料為中心思考

今天看到畫面、有資訊(資料格式可以先想成 json),如果要保畫面的資料保存或是下載,如果這個 data 可以幫我將畫面還原,他需要有甚麼資訊?

舉例:todos 而言,缺少下列任一東西都會無法還原畫面

  1. 紀錄輸入的值是甚麼
  2. 代辦事項 todos:應該是一個陣列,裡面每個 todo 是一個物件(todo 內容、以勾選/未勾選)
  3. 每個 todo 會希望給他一個流水號的 id,他就不會被放在 state 裡面,因為當 todo id 改變時,畫面不需要變動 => 用 useRef 存

將 state 規劃好可以與畫面進行對照,畫面未必要與 state 完全一比一,也就是說有些 state 不是必要的

資料改變會需要改變畫面的就是 state;需要改變但不影響畫面 useRef

{
  inputValue: 'abcde',
  todos: [
    {
      isChecked: true,
      content: 'ddw',
    },
    {
      isChecked: false,
      content: 'lob'
    }
  ],
  itemLeftCount: 1,
  filter: active
}
todoId: 2

state 分兩種

  1. state
  2. derived(衍伸) state:可以透過現有的 state 組合或計算而成,不應該放到 state 裡面。例如:itemLeftCount,只要在 todos 看有多少筆資料的 isChecked 是 false,最後得出的數字就是這個數字

放在 state 裡面的都是必要的,所以 inputValue、todos 都是必要的,但 derived state 非必要的,他可以透擴 state 的東西計算出來

不要將 derived state 放在 state 裡面會變得非常難維護

ex:
filterTodos 可以從 todos 和 filters 兩個狀態推測出來最後的 filterTodos 長相

// 改了 filter 顯示出來的東西就改變
  const [filter, setFilter] = useState("active");
import "./styles.css";
import { useState } from "react";
export default function App() {
  const [todos, setTodos] = useState([
    { content: "todo1", id: 1, isChecked: true },
    { content: "todo2", id: 2, isChecked: false }
  ]);
  const [filter, setFilter] = useState("active");
  const filterTodos = todos.filter((todo) => {
    if (filter === "all") return true;
    return filter === "active" ? todo.isChecked : !todo.isChecked;
  });
  return (
    <div>
      {filterTodos.map((todo) => (
        <div key={todo.id}>{todo.content}</div>
      ))}
    </div>
  );

這樣不是每次 re-render 都要重新計算 ? 就算沒有改 todos 也沒有改 filter 今天有其他 state,例如:每次輸入數字就加一,因為每打一次就 setState 每次 setState 就觸發 re-render,重新呼叫 App

import "./styles.css";
import { useState } from "react";
export default function App() {
  const [todos, setTodos] = useState([
    { content: "todo1", id: 1, isChecked: true },
    { content: "todo2", id: 2, isChecked: false }
  ]);
  const [filter, setFilter] = useState("active");
  const [value, setValue] = useState("");
  const filterTodos = todos.filter((todo) => {
    if (filter === "all") return true;
    return filter === "active" ? todo.isChecked : !todo.isChecked;
  });
  console.log("render");
  return (
    <div>
      todo: <input value={value} onChange={(e) => setValue(e.target.value)} />
      {filterTodos.map((todo) => (
        <div key={todo.id}>{todo.content}</div>
      ))}
    </div>
  );
}

但打字並不影響 filterTodos,所以這個不用再計算一次,這種計算的東西透過 useMemo 去處理。

// 用 useMemo 包起來打字還是會印出 calculate todo,因為第二個參數 dependency 
import { useState, useMemo } from "react";
  const filterTodos = useMemo(() => {
    console.log('calculate todo')
    return todos.filter((todo) => {
      if (filter === "all") return true;
      return filter === "active" ? todo.isChecked : !todo.isChecked;
      })
  });
  console.log("render");

[] 表示沒有 dependency,之後怎麼改都不會觸發 calculate todo

  const filterTodos = useMemo(() => {
    console.log("calculate todo");
    return todos.filter((todo) => {
      if (filter === "all") return true;
      return filter === "active" ? todo.isChecked : !todo.isChecked;
    });
  }, []);

上面的狀況以下面的代碼說明:按清空按鈕東西並沒有重新 render,但事實上 todos 有 re-render 他已經變成空陣列了。

但 useMemo 寫成沒有 dependency 所以 state 變化 useMemo 並不會重算一次。還會停留在剛剛的值。當我的 todos 改變 filterTodos 也要一起改變,因為 filterTodos 是透過 todos 根 filters 兩個組合而成的。

import "./styles.css";
import { useState, useMemo } from "react";
export default function App() {
  const [todos, setTodos] = useState([
    { content: "todo1", id: 1, isChecked: true },
    { content: "todo2", id: 2, isChecked: false }
  ]);
  const [filter, setFilter] = useState("active");
  const [value, setValue] = useState("");
  const filterTodos = useMemo(() => {
    console.log("calculate todo");
    return todos.filter((todo) => {
      if (filter === "all") return true;
      return filter === "active" ? todo.isChecked : !todo.isChecked;
    });
  }, []);
  console.log("render");
  return (
    <div>
      todo: <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button onClick={() => setTodos([])}>清空</button>
      {filterTodos.map((todo) => (
        <div key={todo.id}>{todo.content}</div>
      ))}
    </div>
  );
}

改成這樣,當 [todos, filter] 任一改變,filterTodos 都會重算一遍

 const filterTodos = useMemo(() => {
    console.log("calculate todo");
    return todos.filter((todo) => {
      if (filter === "all") return true;
      return filter === "active" ? todo.isChecked : !todo.isChecked;
    });
  }, [todos, filter]);

useCallback 也是一樣,
當值改變將 todos 印出來,但將 todos 清空拿到的值還是一個陣列

使用 useCallback react 會將這個 function 給記住。可以將每次的 render 當作他重新 call 這個 function。

// useCallback 記憶 function
  const handleChange = useCallback((e) => {
    console.log(todos);
    setValue(e.target.value);
  }, []); // 沒有東西變了,我才要變 ie 任何東西改變我都不會變,都拿之前已經存好的 function 過來

useEffect 同理


小結

  1. 看畫面上有那些東西,所以我那些資料是必備的
  2. 可以透過 state 算出或組合出來的就是 derived state,對這類 state 外面可以包 useMemo 就不用每次都算,只有特定資料改變他才需要重新計算
  3. useCall, useEffect 第二個參數也是同理
  4. eslint 會提醒,9 成情況會需要填入她叫你填的東西
  5. useRef 需要改變但不影響畫面,


#ESLint #React #State #propTypes #Props







Related Posts

【文章筆記 】CS75 (Summer 2012) Lecture 9 Scalability Harvard Web Development David Malan

【文章筆記 】CS75 (Summer 2012) Lecture 9 Scalability Harvard Web Development David Malan

Day 5 - For Loop

Day 5 - For Loop

[24] 強制轉型 - parseInt、Number、ToPrimitive、Boolean

[24] 強制轉型 - parseInt、Number、ToPrimitive、Boolean


Comments