React18

SRIGT發表於2024-06-20

0x01 React 基礎

(1)概述

  • React 框架由 Meta(原 Facebook)研發併發布
  • 用於構建 Web 和原生互動介面的庫
  • 優點:
    • 使用元件化的開發方式,效能較優
    • 具有豐富生態,並且支援跨平臺

(2)建立開發環境

a. 第一個工程

需要提前安裝並配置好 Node.js v16+、npm v9+ 等工具

  1. 使用命令 npm install -g create-react-app 安裝建立 React 應用的工具

    • create-react-app 是由 Webpack 構建的、用於快速建立 React 開發環境的工具
    • 以下內容使用的是 React v18.3
  2. 使用命令 create-react-app react-app 建立一個名為 react-app 的應用

  3. 使用命令 cd react-app 進入應用目錄

    • 目錄檔案說明:

      • node_modules:依賴目錄

      • public:靜態資源目錄

        • favicon.ico:圖示檔案
        • index.html:頁面檔案
      • src:程式碼資源目錄

        • App.js:應用檔案

          function App() {
            return <div className="App">Hello, react!</div>;
          }
          
          export default App;
          
          
        • index.js:入口檔案

          // React 必要的核心依賴(包)
          import React from "react";
          import ReactDOM from "react-dom/client";
          
          // 匯入應用元件的檔案
          import App from "./App";
          
          // 將應用元件渲染到頁面上
          const root = ReactDOM.createRoot(document.getElementById("root")); // 將 index.html 中 id 為 root 的標籤建立為 React 根元件
          root.render(<App />); // 將 App 元件渲染到根元件中
          
          
      • .gitignore:Git 忽略配置

      • package.json / package-lock.json:工程配置檔案

      • README.md:工程說明檔案

    • 業務規範專案目錄

      目錄 作用
      apis 介面
      assets 靜態資源
      components 元件
      pages 頁面
      router 路由
      store Redux 狀態管理
      utils 工具函式
  4. 使用命令 npm start 執行 React 應用

  5. 訪問 http://localhost:3000/ 檢視預設 React 應用頁面

b. 開發工具及外掛

  • 編輯器建議使用 VSCode,推薦其中新增以下外掛:
    • ES7+ React/Redux/React-Native snippets
    • ESLint
    • Prettier
    • Simple React Snippets
    • Typescript React code snippets
    • VSCode React Refactor
  • 瀏覽器中關於 React 除錯的擴充套件:
    • 安裝 Chrome 擴充套件
      • 如果無法訪問,則可以嘗試該連結:https://www.crx4chrome.com/crx/3068/
    • 安裝 Firefox 擴充套件
    • 安裝 Edge 擴充套件

(3)JSX

a. 概述

  • JSX(JavaScript and XML)是 Javascript 語法擴充套件,可以在 Javascript 檔案中書寫類似 HTML 的標籤
    • 元件使用 JSX 語法,從而使渲染邏輯和標籤共同存在於元件中
  • JSX 看起來和 HTML 很像,但它的語法更加嚴格並且可以動態展示資訊
    • JSX 轉化器:https://transform.tools/html-to-jsx
  • JSX 的優點在於:既可以使用 HTML 的宣告式模板寫法,還可以使用 JS 的可程式設計能力
  • JSX 程式碼需要透過 Babel 進行編譯,藉助 @babel/plugin-transform-react-jsx

b. JS 表示式

  • 在 JSX 中,使用 {} 識別 JS 表示式(在 App.js 中編輯)

    • 傳遞字串

      function App() {
        return <div>{"Hello, react!"}</div>;
      }
      
      export default App;
      
      
    • 使用變數

      const number = 123;
      
      function App() {
        return <div className="App">{number}</div>;
      }
      
      export default App;
      
      
    • 使用物件

      const object = {
        backgroundColor: "skyblue",
        color: "white",
        padding: "2rem",
      };
      
      function App() {
        return (
          <div className="App" style={object}>
            Hello, react!
          </div>
        );
      }
      
      export default App;
      
      
    • 呼叫函式與方法

      function getString() {
        return "Today is ";
      }
      
      function App() {
        return <div className="App">{getString()} {new Date().toLocaleDateString()}</div>;
      }
      
      export default App;
      
      
  • if 語句、switch 語句、變數宣告等屬於語句,而非 JS 表示式,因此不能出現在 JSX 的 {}

c. 列表渲染

  • 使用陣列的 map() 方法
  • 標籤其中必須設定屬性 key,其賦值使用獨一無二的數字或字串
const list = [
  { name: "Alex", age: 18 },
  { name: "Bob", age: 20 },
  { name: "Charlie", age: 22 },
];

function App() {
  return (
    <div className="App">
      <ul>
        {list.map((item, index) => (
          <li key={index}>
            {item.name} - {item.age}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

d. 條件渲染

  • 使用邏輯與運算子 && 以及三元表示式 ?: 實現基礎條件渲染

    const flag = true;
    
    function App() {
      return (
        <div className="App">
          運算子: {flag && <span>flag is true</span>} <br />
          表示式: {!flag ? <span>flag is true</span> : <span>flag is false</span>}
        </div>
      );
    }
    
    export default App;
    
    
  • 當條件較為複雜時,使用函式來處理

    function judge(condition) {
      if (condition === 0) {
        return <span>空</span>;
      } else if (condition === 1) {
        return <span>唯一</span>;
      } else {
        return <span>兩個及以上</span>;
      }
    }
    
    function App() {
      return (
        <div className="App">
          <p>0: {judge(0)}</p>
          <p>1: {judge(1)}</p>
          <p>2: {judge(2)}</p>
          <p>3: {judge(3)}</p>
        </div>
      );
    }
    
    export default App;
    
    

e. 事件繫結

  • on[事件名稱] = {事件處理程式},如點選事件:

    function App() {
      const handleClick = () => {
        alert("Clicked");
      };
    
      return (
        <div className="App">
          <button onClick={handleClick}>Click Me</button>
        </div>
      );
    }
    
    export default App;
    
    
  • 事件物件引數

    function App() {
      const handleClick = (e) => {
        console.dir(e);
      };
    
      return (
        <div className="App">
          <button onClick={handleClick}>Click Me</button>
        </div>
      );
    }
    
    export default App;
    
    
  • 自定義引數

    • 需要函式引用,而不能直接呼叫函式
    function App() {
      const handleClick = (value) => {
        alert(`name is ${value}`);
      };
    
      return (
        <div className="App">
          <button onClick={() => handleClick("Alex")}>Click Me</button>
        </div>
      );
    }
    
    export default App;
    
    
    • 同時傳遞事件物件和自定義引數

      function App() {
        const handleClick = (e, value) => {
          console.dir(e);
          alert(`name is ${value}`);
        };
      
        return (
          <div className="App">
            <button onClick={(e) => handleClick(e, "Alex")}>Click Me</button>
          </div>
        );
      }
      
      export default App;
      
      

(4)元件

a. 元件使用

  • 元件是 React 的核心概念之一,是構建使用者介面(UI)的基礎

    • 元件是獨立的 UI 片段,一個或多個元件構建成 React 應用
    • 元件本質上是可以任意新增標籤的 Javascript 函式
    // 自定義元件
    function Paragraph() {
      return <p>This is a paragraph.</p>;
    }
    
    function App() {
      return (
        <div>
          <h1>This is a heading.</h1>
    
          // 使用自定義元件
          <Paragraph />
          <Paragraph />
          <Paragraph />
        </div>
      );
    }
    
    export default App;
    
    

b. 元件樣式

  • 元件基礎樣式控制有兩種方案:

    1. 行內樣式

      <div style={{ color: 'red' }}>content</div>
      
    2. class 類名控制

      /* index.css */
      .content {
        color: red;
      }
      
      // App.js
      import './index.css'
      
      function App() {
        return <div className='content'>content</div>
      }
      
      • 推薦使用 class 類名控制
  • 使用 classnames 最佳化類名控制

    • classnames 是 JavaScript 庫,可以透過條件動態控制 class 類名的顯示

    • 使用命令 npm install classnames 安裝

    • 舉例:

      import "./index.css";
      
      import { useState } from "react";
      import classNames from "classnames";
      
      function App() {
        const [flag, setFlag] = useState(false);
      
        const handleClick = () => {
          setFlag(true);
        };
      
        return (
          <div>
            <p className={classNames("content", { active: flag })}>文字內容</p>
            <button onClick={handleClick}>點選顯示文字內容</button>
          </div>
        );
      }
      
      export default App;
      
      

(5)Hooks

  • React 鉤子(hook)的使用規則
    • 只能在元件中或其他自定義 Hook 中呼叫
    • 只能在元件的頂層呼叫,不能巢狀在 iffor、其他函式中

a. useState

  • useState 是一個 React Hook,用於向元件新增狀態變數,從而控制並影響元件的渲染結果

    • 資料驅動檢視:當狀態變數變化,元件的檢視也會跟著變化
    • useState():一個函式,其返回值是一個陣列
  • 語法:const [var, func] = useState(initValue)

    • 基於 useState() 函式的返回值進行解構賦值
    • var:狀態變數
    • func:修改狀態變數的函式方法
    • initValue:狀態變數初始值
  • 舉例:

    import { useState } from "react";
    
    function App() {
      const [count, setCount] = useState(0);
    
      const handleClick = () => {
        setCount(count + 1);
      };
    
      return (
        <div>
          <p>{count}</p>
          <button onClick={handleClick}>+ 1</button>
        </div>
      );
    }
    
    export default App;
    
    
  • 狀態不可變規則:即狀態是只讀的,不可被修改,只能被替換

    • 簡單型別,如上述案例

    • 複雜型別,如物件:

      import { useState } from "react";
      
      function App() {
        const [form, setForm] = useState({
          username: "Alex",
          password: "123456"
        });
      
        const handleClick = () => {
          setForm({
            ...form,
            username: "Bob",
            password: "654321"
          });
        };
      
        return (
          <div>
            <p>使用者名稱: {form.username}</p>
            <p>密碼: {form.password}</p>
            <button onClick={handleClick}>修改</button>
          </div>
        );
      }
      
      export default App;
      
      
  • 使用 useState 控制表單狀態

    • 使用 onChange 監測輸入的內容是否發生改變,從而修改 content 的值
    import { useState } from "react";
    
    function App() {
      const [content, setContent] = useState("");
    
      const handleChange = (value) => {
        setContent(value);
      };
    
      return (
        <div>
          <p>輸入的內容: {content}</p>
          <input
            type="text"
            onChange={(e) => {
              handleChange(e.target.value);
            }}
          />
        </div>
      );
    }
    
    export default App;
    
    

b. useReducer

  • useRuducer 作用與 useState 類似,用於管理相對複雜的狀態資料

  • 舉例:

    import { useReducer } from "react";
    
    // 1. 定義 reducer 函式, 根據不同的 action 返回不同的新狀態
    function reducer(state, action) {
      switch (action.type) {
        case "increment":
          return state + 1;
        case "decrement":
          return state - 1;
        default:
          return state;
      }
    }
    
    function App() {
      // 2. 呼叫 useReducer 並傳入 reducer 函式和初始值
      const [state, dispatch] = useReducer(reducer, 0);
    
      return (
        <div>
          {/* 3. 事件觸發 dispatch 並分派 action 物件 */}
          <button onClick={() => dispatch({ type: "decrement" })}>-1</button>
          <span>{state}</span>
          <button onClick={() => dispatch({ type: "increment" })}>+1</button>
        </div>
      );
    }
    
    export default App;
    
    

c. useRef

  • useRef 用於在 React 元件中建立和操作 DOM

  • 舉例:

    import { useRef } from "react";
    
    function App() {
      const inputRef = useRef(null); // 1. 建立 Ref 物件
    
      function handleClick() {
        console.dir(inputRef.current); // 3. 獲取 DOM 節點
      }
    
      return (
        <div>
          <input
            type="text"
            ref={inputRef} // 2. 將 Ref 物件與 input 元素繫結
          />
          <button onClick={handleClick}>獲取 DOM</button>
        </div>
      );
    }
    
    export default App;
    
    

d. useEffect

  • useEffect 用於建立由渲染觸發的操作(不是由事件觸發),如傳送 Ajax 請求、更改 DOM 等

  • 語法:useEffect(() => {}, [])

    • 回撥函式稱為“副作用函式”,其中是需要執行的操作
    • 陣列可選,其中是監聽項,作為空陣列時副作用函式僅在元件渲染完畢之後執行一次
  • 舉例:

    import { useEffect, useState } from "react";
    
    function App() {
      const [data, setData] = useState("");
      useEffect(() => {
        async function getData() {
          const response = await fetch("https://api.ipify.org?format=json");
          const data = await response.json();
          setData(data.ip);
        }
        getData();
      }, []);
    
      return <div>IP address: {data}</div>;
    }
    
    export default App;
    
    
  • 監聽項陣列影響副作用函式

    監聽項 副作用函式執行時機
    元件初始渲染和更新時執行
    空陣列 僅在初始渲染時執行
    特定監聽項 元件初始渲染和監聽項變化時執行
    • 無監聽項

      import { useEffect, useState } from "react";
      
      function App() {
        const [count, setCount] = useState(0);
        useEffect(() => {
          console.log("副作用函式執行");
        });
      
        return (
          <div>
            <span>count: {count}</span>
            <button onClick={() => setCount(count + 1)}>+1</button>
          </div>
        );
      }
      
      export default App;
      
      
    • 空陣列

      import { useEffect, useState } from "react";
      
      function App() {
        const [count, setCount] = useState(0);
        useEffect(() => {
          console.log("副作用函式執行");
        }, []);
      
        return (
          <div>
            <span>count: {count}</span>
            <button onClick={() => setCount(count + 1)}>+1</button>
          </div>
        );
      }
      
      export default App;
      
      
    • 特定監聽項

      import { useEffect, useState } from "react";
      
      function App() {
        const [count, setCount] = useState(0);
        useEffect(() => {
          console.log("副作用函式執行");
        }, [count]);
      
        return (
          <div>
            <span>count: {count}</span>
            <button onClick={() => setCount(count + 1)}>+1</button>
          </div>
        );
      }
      
      export default App;
      
      
  • 當元件解除安裝時,需要清理副作用函式

    useEffect(() => {
      // 實現副作用操作邏輯
      return () => {
        // 清除副作用操作邏輯
      }
    },[])
    
    • 舉例:清除定時器

      import { useEffect, useState } from "react";
      
      function Child() {
        useEffect(() => {
          const timer = setInterval(() => {
            console.log("定時器");
          }, 500);
      
          return () => {
            clearInterval(timer);
            console.log("清除定時器");
          };
        }, []);
      
        return <div>子元件</div>;
      }
      
      function App() {
        const [show, setShow] = useState(true);
      
        return (
          <div>
            {show && <Child />}
            <button onClick={() => setShow(false)}>解除安裝子元件</button>
          </div>
        );
      }
      
      export default App;
      
      

e. useMemo

  • useMemo 用於在元件每次重新渲染的時候快取計算的結果

  • 舉例:

    import { useMemo, useState } from "react";
    
    function fibonacci(n) {
      console.log("斐波那契數列計算");
      if (n < 3) {
        return 1;
      }
      return fibonacci(n - 2) + fibonacci(n - 1);
    }
    
    function App() {
      const [count1, setCount1] = useState(0);
      const [count2, setCount2] = useState(0);
    
      const result = useMemo(() => {
        return fibonacci(count1);
      }, [count1]);
      console.log("元件重新渲染");
    
      return (
        <div>
          <button onClick={() => setCount1(count1 + 1)}>count1: {count1}</button>
          <button onClick={() => setCount2(count2 + 1)}>count2: {count2}</button>
          {result}
        </div>
      );
    }
    
    export default App;
    
    

f. useCallback

  • useCallback 用於在元件多次重新渲染時快取函式

  • 舉例:

    import { memo, useCallback, useState } from "react";
    
    const Comp = memo(function Comp({ prop }) {
      console.log("子元件重新渲染");
      return (
        <input type="number" onChange={(e) => prop(parseInt(e.target.value))} />
      );
    });
    
    function App() {
      const [count, setCount] = useState(0);
      const changeHandler = useCallback((value) => {
        setCount(value);
      }, []);
    
      return (
        <div>
          {count}
          <button onClick={() => setCount(count + 1)}>+1</button>
          <Comp prop={changeHandler} />
        </div>
      );
    }
    
    export default App;
    
    

g. 自定義 Hook

  • 自定義 Hook 是以 use 為字首的函式方法

  • 透過自定義 Hook 可以實現邏輯的封裝與複用

  • 舉例:

    import { useState } from "react";
    
    function useToggle() {
      const [value, setValue] = useState(true);
      const toggle = () => setValue(!value);
      return [value, toggle];
    }
    
    function App() {
      const [value, toggle] = useToggle();
    
      return (
        <div>
          <button onClick={toggle}>Toggle</button>
          {value && <div>Div</div>}
        </div>
      );
    }
    
    export default App;
    
    

(6)元件通訊

  • 元件通訊是指元件之間的資料傳遞
    • 不同的元件巢狀方式有不同的資料傳遞方法,如父傳子、子傳父、兄弟間等

a. 父傳子

  • 透過 props 實現父傳子元件通訊步驟:

    1. 父元件傳送資料:在子元件標籤上繫結屬性
    2. 子元件接受資料:子元件透過 props 引數接收資料
    function Child(props) {
      console.log(props);
      return <p>Child Comp</p>
    }
    
    function App() {
      const name = "Alex"
    
      return (
        <div>
          <Child name={name} />
        </div>
      );
    }
    
    export default App;
    
    
  • props 可以傳遞任意型別的資料,如數字、字串、陣列、物件、方法、JSX

  • props 是隻讀物件,資料只能在父元件修改

  • 當父元件中把內容巢狀在子元件中時,props 會自動新增 children 屬性

    function Child(props) {
      console.log(props);
      return <p>Child Comp</p>
    }
    
    function App() {
      return (
        <div>
          <Child>
            <span>Alex</span>
          </Child>
        </div>
      );
    }
    
    export default App;
    
    

b. 子傳父

  • 透過呼叫父元件方法傳遞引數實現子傳父元件通訊

    function Child({ onHandle }) {
      const value = "Alex";
      return <button onClick={() => onHandle(value)}>傳送</button>;
    }
    
    function App() {
      const getValue = (value) => console.log(value);
      return (
        <div>
          <Child onHandle={getValue} />
        </div>
      );
    }
    
    export default App;
    
    

c. 兄弟間

  • 使用狀態提升實現兄弟元件通訊

    • 即透過子傳父的方法將子元件 A 的資料傳遞到父元件,再透過父傳子的方法將父元件接收的資料傳遞到子元件 B
    import { useState } from "react";
    
    function A({ onHandle }) {
      const value = "Alex";
      return (
        <label>
          A: <button onClick={() => onHandle(value)}>傳送</button>
        </label>
      );
    }
    
    function B(props) {
      return <div>B: {props.value}</div>;
    }
    
    function App() {
      const [value, setValue] = useState("");
      const getValue = (value) => setValue(value);
      return (
        <div>
          <A onHandle={getValue} />
          <B value={value} />
        </div>
      );
    }
    
    export default App;
    
    

d. 跨層級(任意元件間)

  • 使用 Context 機制實現跨層級元件通訊步驟:

    1. 使用 createContext 方法建立上下文物件
    2. 在頂層元件中,透過 Provider 傳遞(提供)資料
    3. 在底層元件中,透過 useContext 方法接收(消費)資料
    import { createContext, useContext } from "react";
    
    const ctx = createContext();
    
    function Child() {
      const name = useContext(ctx);
      return <span>Child: {name}</span>;
    }
    
    function Parent() {
      return <Child />;
    }
    
    function App() {
      const name = "Alex";
    
      return (
        <div>
          <ctx.Provider value={name}>
            <Parent />
          </ctx.Provider>
        </div>
      );
    }
    
    export default App;
    
    
  • 該方法可用於任意元件間進行元件通訊

0x02 Redux

(1)概述

  • Redux 是 React 最常用的集中狀態關聯工具,可以獨立於框架執行

  • 快速上手案例:

    <!DOCTYPE html>
    <html lang="en">
      <body>
        <button id="decrement">-1</button>
        <span>0</span>
        <button id="increment">+1</button>
    
        <script>
          // 第一步
          function reducer(state = { count: 0 }, action) {
            if (action.type === "DECREMENT") {
              return { count: state.count - 1 };
            }
            if (action.type === "INCREMENT") {
              return { count: state.count + 1 };
            }
            return state;
          }
    
          // 第二步
          const store = Redux.createStore(reducer);
    
          // 第三步
          store.subscribe(() => {
            console.log("資料改變");
            document.querySelector("span").textContent = store.getState().count;  // 第五步
          });
    
          // 第四步
          const decrement = document.getElementById("decrement");
          decrement.addEventListener("click", () =>
            store.dispatch({ type: "DECREMENT" })
          );
    
          const increment = document.getElementById("increment");
          increment.addEventListener("click", () =>
            store.dispatch({ type: "DECREMENT" })
          );
        </script>
      </body>
    </html>
    
    1. 定義 reducer 函式
      • 作用:根據不同的 action 物件,返回不同的、新的 state
      • state:管理資料初始狀態
      • action:物件 type 標記
    2. 生成 store 例項
    3. 訂閱資料變化
    4. 透過 dispatch 提交 action 更改狀態
    5. 透過 getState 方法獲取最新狀態資料並更新到檢視
  • 在 Chrome 瀏覽器中可以使用 Redux DevTools 外掛對 Redux 進行除錯

(2)結合 React

a. 配置環境

  • 在 React 中使用 Redux 前,需要安裝 Redux Toolkitreact-redux

    • Redux Toolkit(RTK)是官方推薦編寫 Redux 邏輯的方式,簡化書寫方式,包括:

      • 簡化 store 配置方式
      • 內建 immer 支援可變式狀態修改
      • 內建 thunk 更好地非同步建立
    • react-redux 是用於連結 React 元件和 Redux 的中介軟體

      graph LR Redux--獲取狀態-->React元件 --更新狀態-->Redux
    • 使用命令 npm install @reduxjs/toolkit react-redux 安裝

  • 安裝完成後,在 src 目錄下新建 store 目錄

    graph TB store-->modules & index.js modules-->subStore.js & ...
    • store 目錄是集中狀態管理的部分
    • 其中新建 index.js,是入口檔案,組合子 store 模組
    • 其中新建 modules 目錄,用於包括多個子 store 模組

b. 實現 counter

  1. 在 src/store/modules 中建立 counterStore.js

    import { createSlice } from "@reduxjs/toolkit";
    
    const counterStore = createSlice({
      name: "counter",
      initialState: {
        // 初始狀態資料
        count: 0,
      },
      reducers: {
        // 修改資料的同步方法
        increment(state) {
          state.count++;
        },
        decrement(state) {
          state.count--;
        },
      },
    });
    
    // 解構出建立 action 物件的函式
    const { increment, decrement } = counterStore.actions;
    
    // 獲取 reducer 函式
    const counterReducer = counterStore.reducer;
    
    // 匯出建立 action 物件和 reducer 函式
    export { increment, decrement };
    export default counterReducer;
    	
    
  2. 修改 src/store/index.js

    import { configureStore } from "@reduxjs/toolkit";
    import counterReducer from "./modules/counterStore";
    
    // 建立根 store 來組合子 store 模組
    const store = configureStore({
      reducer: {
        counter: counterReducer,
      },
    });
    
    export default store;
    
    
  3. 修改 src/index.js,將 store 匯入 React 元件

    import store from "./store";
    import { Provider } from "react-redux";
    
    // ...
    
    root.render(
      <Provider store={store}>
        <App />
      </Provider>
    );
    
    
  4. 修改 src/App.js,在元件中使用 store,透過 useSelector 鉤子函式

    • 該函式將 store 中的資料對映到元件中
    import { useSelector } from "react-redux";
    
    function App() {
      const { count } = useSelector(state => state.counter);
      return <div>{count}</div>;
    }
    
    export default App;
    
    
  5. 修改 src/App.js,使用 useDispatch 鉤子函式修改資料

    • 該函式生成提交 action 物件的 dispatch 函式
    import { useDispatch, useSelector } from "react-redux";
    import { decrement, increment } from "./store/modules/counterStore"; // 匯入建立 action 物件的方法
    
    function App() {
      const { count } = useSelector((state) => state.counter);
      const dispatch = useDispatch(); // 得到 dispatch 函式
    
      return (
        <div>
          {/*呼叫 dispatch 提交 action 物件*/}
          <button onClick={() => dispatch(decrement())}>-</button>
          <span>{count}</span>
          <button onClick={() => dispatch(increment())}>+</button>
        </div>
      );
    }
    
    export default App;
    
    

c. 提交 action 傳參

  • 以上述案例為例,為了實現點選不同按鈕可以直接把 count 的值修改為指定數字,需要在提交 action 物件時傳遞引數

  • 原理:在 reducer 的同步修改方法中新增 action 物件引數,在呼叫 actionCreater 方法時傳參,其中引數會被傳遞到物件的 payload 屬性上

  • 修改上述案例:

    1. 修改 src\store\modules\counterStore.js,建立 change 方法用於修改 count 變數到指定的值

      import { createSlice } from "@reduxjs/toolkit";
      
      const counterStore = createSlice({
        // ...
        reducers: {
          // ...
          change(state, action) {
            state.count = action.payload;
          },
        },
      });
      
      const { increment, decrement, change } = counterStore.actions;
      const counterReducer = counterStore.reducer;
      export { increment, decrement, change };
      export default counterReducer;
      
      
    2. 修改 src\App.js,在元件中使用

      import { useDispatch, useSelector } from "react-redux";
      import { change } from "./store/modules/counterStore";
      
      function App() {
        const { count } = useSelector((state) => state.counter);
        const dispatch = useDispatch();
      
        return (
          <div>
            <span>{count}</span>
            <button onClick={() => dispatch(change(10))}>to 10</button>
            <button onClick={() => dispatch(change(100))}>to 100</button>
          </div>
        );
      }
      
      export default App;
      
      

d. 非同步狀態操作

  • 非同步修改需要單獨封裝一個函式,其中返回一個新函式,這個新函式可以:

    • 封裝非同步請求,並獲取資料
    • 呼叫同步 actionCreater 傳入非同步資料生成 action 物件,並使用 dispatch 提交
  • 舉例:src\store\modules\channelStore.js

    import { createSlice } from "@reduxjs/toolkit";
    
    const channelStore = createSlice({
      name: "channel",
      initialState: {
        channelList: [],
      },
      reducers: {
        setChannels(state, action) {
          state.channelList = action.payload;
        },
      },
    });
    
    const { setChannels } = channelStore.actions;
    const url = "";
    const fetchChannelList = () => {
      return async (dispatch) => {
        const res = await axios.get(url);
        dispatch(setChannels(res.data.channels));
      };
    };
    
    const reducer = channelStore.reducer;
    
    export { fetchChannelList };
    export default reducer;
    
    

0x03 React Router

(1)概述

  • React Router 實現客戶端路由,使響應速度更快

  • 建立路由開發環境

    1. 使用命令 npm install react-router-dom 安裝 React Router

    2. 修改 src\index.js

      // ...
      import { createBrowserRouter, RouterProvider } from "react-router-dom";
      
      // 1. 建立 router 例項物件並配置路由對應關係
      const router = createBrowserRouter([
        {
          path: "/login",
          element: <div>登入頁</div>,
        },
        {
          path: "/register",
          element: <div>註冊頁</div>,
        },
      ]);
      
      const root = ReactDOM.createRoot(document.getElementById("root"));
      
      // 2. 路由繫結
      root.render(<RouterProvider router={router} />);
      
      
    3. 使用命令 npm start 啟動專案

    4. 依次訪問 http://localhost:3000/loginhttp://localhost:3000/register

(2)抽象路由模組

  1. 在 src 目錄下新建 page 目錄,其中包含各個頁面,如 Login/index.js 和 Register/index.js

    // src\page\Login\index.js
    const Login = () => {
      return <div>登入頁</div>;
    };
    
    export default Login;
    
    
    // src\page\Register\index.js
    const Register = () => {
      return <div>註冊頁</div>;
    };
    
    export default Register;
    
    
  2. 在 src 目錄下新建 router 目錄,其中新建 index.js,包含路由配置

    import Login from "../page/Login";
    import Register from "../page/Register";
    
    import { createBrowserRouter } from "react-router-dom";
    
    const router = createBrowserRouter([
      {
        path: "/login",
        element: <Login />,
      },
      {
        path: "/register",
        element: <Register />,
      },
    ]);
    
    export default router;
    
    
  3. 修改 src\index.js

    // ...
    import { RouterProvider } from "react-router-dom";
    import router from "./router";
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<RouterProvider router={router} />);
    
    
  4. 依次訪問 http://localhost:3000/loginhttp://localhost:3000/register

(3)路由導航

  • 路由導航是指多個路由之間需要進行路由跳轉,並且跳轉過程中可能需要傳參通訊

  • 主要用兩種導航方式:宣告式導航和程式設計式導航

    • 宣告式導航是指在模板中透過 Link 元件指定跳轉的目標路由,如:

      // src\page\Login\index.js
      import { Link } from "react-router-dom";
      
      const Login = () => {
        return (
          <div>
            <p>登入頁</p>
            <Link to="/register">前往註冊頁</Link>
          </div>
        );
      };
      
      export default Login;
      
      

      此時,訪問 http://localhost:3000/login 並點選連結即可跳轉至 http://localhost:3000/register

    • 程式設計式導航是指透過 useNavigate 鉤子得到方法,並透過呼叫方法以命令式的形式實現路由跳轉,如:

      // src\page\Login\index.js
      import { useNavigate } from "react-router-dom";
      
      const Login = () => {
        const navigate = useNavigate();
      
        return (
          <div>
            <p>登入頁</p>
            <button onClick={() => navigate("/register")}>前往註冊頁</button>
          </div>
        );
      };
      
      export default Login;
      
      

      此時,訪問 http://localhost:3000/login 並點選按鈕即可跳轉至 http://localhost:3000/register

(4)傳遞引數

  1. 基於程式設計式導航,使用 useSearchParams 鉤子獲取 URI 中的查詢字串,如:

    • src\page\Login\index.js

      import { useNavigate } from "react-router-dom";
      
      const Login = () => {
        const navigate = useNavigate();
      
        return (
          <div>
            <p>登入頁</p>
            <button onClick={() => navigate("/register?phone=138&email=example@site.com")}>前往註冊頁</button>
          </div>
        );
      };
      
      export default Login;
      
      
    • src\page\Register\index.js

      import { useSearchParams } from "react-router-dom";
      
      const Register = () => {
        const [params] = useSearchParams();
        let phone = params.get("phone");
        let email = params.get("email");
      
        return (
          <div>
            <p>註冊頁</p>
            <p>手機: {phone}</p>
            <p>郵箱: {email}</p>
          </div>
        );
      };
      
      export default Register;
      
      
  2. 基於程式設計式導航,使用 useParams 鉤子獲取 URI 中的動態路由引數,如:

    • src\router\index.js

      // ...
      {
        path: "/register/:phone/:email",
        element: <Register />,
      },
      // ...
      
    • src\page\Login\index.js

      import { useNavigate } from "react-router-dom";
      
      const Login = () => {
        const navigate = useNavigate();
      
        return (
          <div>
            <p>登入頁</p>
            <button onClick={() => navigate("/register/138123456/example@site.com")}>前往註冊頁</button>
          </div>
        );
      };
      
      export default Login;
      
      
    • src\page\Register\index.js

      import { useParams } from "react-router-dom";
      
      const Register = () => {
        const params = useParams();
        let phone = params.phone;
        let email = params.email;
      
        return (
          <div>
            <p>註冊頁</p>
            <p>手機: {phone}</p>
            <p>郵箱: {email}</p>
          </div>
        );
      };
      
      export default Register;
      
      

(5)巢狀路由

  • 巢狀路由是指在一級路由下內嵌其他路由(二級路由)

  • 舉例:

    • 建立 src\page\About\index.js

      const About = () => {
        return <div>關於頁</div>;
      };
      
      export default About;
      
      
    • 建立 src\page\Blog\index.js

      const Blog = () => {
        return <div>部落格頁</div>;
      };
      
      export default Blog;
      
      
    • 建立 src\page\Home\index.js

      import { Link, Outlet } from "react-router-dom";
      
      const Home = () => {
        return (
          <div>
            <p>主頁</p>
            <Link to="about">關於</Link>
            <br />
            <Link to="blog">部落格</Link>
      
            <div
              style={{
                border: "2px solid black",
              }}
            >
              <Outlet />
            </div>
          </div>
        );
      };
      
      export default Home;
      
      
    • 修改 src\router\index.js,配置巢狀路由

      import { createBrowserRouter } from "react-router-dom";
      import Home from "../page/Home";
      import About from "../page/About";
      import Blog from "../page/Blog";
      
      const router = createBrowserRouter([
        {
          path: "/home",
          element: <Home />,
          children: [
            {
              path: "about",
              element: <About />
            },
            {
              path: "blog",
              element: <Blog />
            }
          ]
        },
      ]);
      
      export default router;
      
      
  • 預設二級路由:當一級路由顯示時需要某個指定二級路由同時顯示時,需要設定預設二級路由

    • 修改 src\router\index.js,配置預設二級路由

      // ...
      children: [
        {
          index: true,
          element: <About />
        },
        {
          path: "blog",
          element: <Blog />
        }
      ]
      // ...
      
    • 修改 src\page\Home\index.js

      {/* ... */}
      <Link to="/home">關於</Link>
      <br />
      <Link to="blog">部落格</Link>
      {/* ... */}
      

(6)通配路由

  • 所謂通配路由其實常用於,當訪問的路由不存在時,彈出的 404 頁面

  • 舉例:

    • 建立 src\page\NotFound\index.js

      const NotFound = () => {
        return <div>404 Not Found</div>;
      };
      
      export default NotFound;
      
      
    • 修改 src\router\index.js,配置通配路由

      import { createBrowserRouter } from "react-router-dom";
      import NotFound from "../page/NotFound";
      
      const router = createBrowserRouter([
        {
          path: "*",
          element: <NotFound />,
        },
      ]);
      
      export default router;
      
      

(7)路由模式

  • 路由模式主要包括 hash 模式和 history 模式,兩者相比:

    路由模式 URL 原理 是否需要後端支援 建立方法
    hash url/#/route 監聽 hashChange 事件 createHashRouter
    history url/route history 物件 + pushState 事件 createBrowerRouter
  • 路由模式的選擇在 src\router\index.js 中實現,如:

    import { createBrowserRouter, createHashRouter } from "react-router-dom";
    
    const routerHash = createHashRouter();
    const routerHistory = createBrowserRouter();
    
    export { routerHash, routerHistory };
    
    

0x04 React 高階

(1)React.memo

  • React.memo 用於允許元件在 Props 沒有改變的情況下跳過渲染

    • React 預設渲染機制:當父元件重新渲染,則子元件也重新渲染

    • 使用 memo 函式包裹生成的快取元件只有在 props 發生改變時重新渲染

      const MemoComp = memo(function CusComp(props) {})
      
  • 舉例:

    import { memo, useState } from "react";
    
    const Comp = () => {
      console.log("子元件重新渲染");
      return <div>Comp</div>;
    };
    
    const MemoComp = memo(function Comp(props) {
      console.log("快取子元件重新渲染");
      return <div>MemoComp</div>;
    });
    
    function App() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>change</button>
          <Comp />
          <MemoComp />
        </div>
      );
    }
    
    export default App;
    
    
  • 在使用 memo 快取元件後,React 會對每個 prop 使用 Object.is 比較,true 為沒有變化,false 為有變化

    • 當 prop 是簡單型別(如數字、字串等),Object.is(3, 3) 返回 true,即沒有變化
    • 當 prop 是引用型別(如陣列、物件等),Object.is([], []) 返回 false,即有變化,因為引用發生變化
    import { memo, useMemo, useState } from "react";
    
    const AComp = memo(function Comp({ prop }) {
      console.log("A 元件重新渲染");
      return <div>AComp: {prop}</div>;
    });
    
    const BComp = memo(function Comp({ prop }) {
      console.log("B 元件重新渲染");
      return <div>BComp: {prop}</div>;
    });
    
    function App() {
      const [count, setCount] = useState(0);
      const list = useMemo(() => {
        return [1, 2, 3];
      }, []);
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>change</button>
          <AComp prop={count} />
          <BComp prop={list} />
        </div>
      );
    }
    
    export default App;
    
    

(2)React.forwardRef

  • React.forwardRef 用於透過使用 ref 暴露 DOM 節點給父元件

  • 舉例:

    import { forwardRef, useRef } from "react";
    
    const Comp = forwardRef((props, ref) => {
      return <input type="text" ref={ref} autoFocus />;
    });
    
    function App() {
      const compRef = useRef(null);
      const handleClick = () => {
        console.log(compRef.current.value);
      };
      return (
        <div>
          <Comp ref={compRef} />
          <button onClick={handleClick}>Click</button>
        </div>
      );
    }
    
    export default App;
    
    

(3)useInperativeHandle

  • useInperativeHandle 鉤子用於透過 ref 暴露子元件的方法給父元件使用

  • 舉例:

    import { forwardRef, useImperativeHandle, useRef } from "react";
    
    const Comp = forwardRef((props, ref) => {
      const compRef = useRef(null);
      const focusHandle = () => {
        compRef.current.focus();
      };
      useImperativeHandle(ref, () => {
        return {
          focusHandle,
        };
      });
      return <input type="text" ref={compRef} />;
    });
    
    function App() {
      const compRef = useRef(null);
      const handleClick = () => {
        compRef.current.focusHandle();
      };
      return (
        <div>
          <Comp ref={compRef} />
          <button onClick={handleClick}>Click</button>
        </div>
      );
    }
    
    export default App;
    
    

(4)常用第三方包

a. SASS/SCSS

  • SCSS 是一種預編譯 CSS 語言,其檔案字尾名為 .scss,支援一些原生 CSS 不支援的高階用法,如變數使用、巢狀語法等

  • 使用命令 npm install sass -D 安裝 SASS,-D 表示僅在開發模式使用,不會打包到生產模式

  • 使用 SCSS:

    1. 修改 src/index.css 為 src/index.scss

      body {
        div {
          color: red;
        }
      }
      
      
    2. 修改 src/index.js

      // ...
      import "./index.scss"
      // ...
      
    3. 修改 src/App.js

      function App() {
        return <div>文字內容</div>;
      }
      
      export default App;
      
      

b. Ant Design

  • Ant Design(簡稱 AntD)是 React PC 端元件庫,由螞蟻金服出品,內建常用元件

  • 使用命令 npm i antd --save 安裝 AntD,--save 表示將模組新增到配置檔案中的執行依賴中

  • 使用 AntD:修改 src/App.js

    import { Button } from "antd";
    
    function App() {
      return <Button type="primary">Button</Button>;
    }
    
    export default App;
    
    

c. Zustand

  • Zustand 用於狀態管理

  • 使用命令 npm i zustand 安裝 Zustand

  • 使用 Zustand:修改 src/App.js

    import { create } from "zustand";
    
    const store = create((set) => {
      return {
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
        decrement: () => set((state) => ({ count: state.count - 1 })),
      };
    });
    
    function App() {
      const { count, increment, decrement } = store();
      return (
        <div>
          <button onClick={decrement}>-1</button>
          <span>{count}</span>
          <button onClick={increment}>+1</button>
        </div>
      );
    }
    
    export default App;
    
    
  • 在非同步方面,Zustand 支援直接在函式中編寫非同步邏輯

    const store = create((set) => {
      return {
        channelList: [],
        fetchChannelList: async () => {
          const URL = "";
          const res = await axios.get(URL);
          const data = await res.json();
          set({
            channelList: data.data.channelList,
          });
        },
      };
    });
    
  • 當單個 store 較大時,可以透過切片模式進行模組拆分組合,即模組化

    const createAStore = create((set) => {
      return {};
    });
    
    const createBStore = create((set) => {
      return {};
    });
    
    const store = create((...a) => ({
      ...createAStore(...a),
      ...createBStore(...a),
    }));
    

(5)類元件

a. 概述

  • 類元件是透過 JavaScript 中的類來組織元件的程式碼

    1. 透過屬性 state 定義狀態資料
    2. 透過方法 setState 修改狀態資料
    3. 透過方法 render 渲染 JSX
  • 舉例:

    import { Component } from "react";
    
    class Counter extends Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0,
        };
      }
    
      increment = () => {
        this.setState({ count: this.state.count + 1 });
      };
    
      decrement = () => {
        this.setState({ count: this.state.count - 1 });
      };
    
      render() {
        return (
          <div>
            <button onClick={this.decrement}>-1</button>
            <span>{this.state.count}</span>
            <button onClick={this.increment}>+1</button>
          </div>
        );
      }
    }
    
    function App() {
      return <Counter />;
    }
    
    export default App;
    
    

b. 生命週期

  • 生命週期指元件從建立到銷燬的各個階段,這些階段自動執行的函式稱為生命週期函式

    https://ask.qcloudimg.com/http-save/yehe-10021778/0e399bb140db5e51ef0ef635a6b06747.png
  • 常用生命週期函式:

    • componentDidMount:元件掛載完成後執行,常用於非同步資料獲取
    • componentWillUnmount:元件解除安裝時執行,常用於清理副作用方法
  • 舉例:

    import { Component, useState } from "react";
    
    class Child extends Component {
      componentDidMount() {
        console.log("元件掛載完成");
      }
      componentWillUnmount() {
        console.log("元件即將解除安裝");
      }
      render() {
        return <div>子元件</div>;
      }
    }
    
    function App() {
      const [show, setShow] = useState(true);
      return (
        <div>
          {show && <Child />}
          <button onClick={() => setShow(!show)}>
            {show ? "隱藏" : "顯示"}子元件
          </button>
        </div>
      );
    }
    
    export default App;
    
    

c. 元件通訊

  • 方法與元件通訊類似

    • 父傳子:透過 prop 繫結資料
    • 子傳父:透過 prop 繫結父元件方法
    • 兄弟間:狀態提示,透過父元件做狀態橋接
  • 舉例:

    import { Component } from "react";
    
    class AChild extends Component {
      render() {
        return <div>子元件 A: {this.props.data}</div>;
      }
    }
    
    class BChild extends Component {
      render() {
        return (
          <div>
            <p>子元件 B</p>
            <button onClick={() => this.props.onGetData(456)}>傳送</button>
          </div>
        );
      }
    }
    
    class Parent extends Component {
      state = {
        data: 123,
      };
    
      getData = (data) => {
        console.log(data);
      };
    
      render() {
        return (
          <div>
            <p>父元件</p>
            <AChild data={this.state.data} />
            <BChild onGetData={this.getData} />
          </div>
        );
      }
    }
    
    function App() {
      return (
        <div>
          <Parent />
        </div>
      );
    }
    
    export default App;
    
    

0x05 結合 TypeScript

(1)建立開發環境

  1. 使用命令 npm create vite@latest react-ts-app -- --template react-ts 建立使用 TypeScript 的 React 工程

    • 首次執行該命令時,需要同意安裝 Vite,選擇 React 框架,選擇 TypeScript
  2. 使用命令 cd react-ts-app 進入工程目錄

  3. 使用命令 npm install 安裝必要依賴

  4. 使用命令 npm run dev 啟動工程

  5. 工程目錄中,程式碼資原始檔在 src 目錄下,其中:

    • App.tsx:應用檔案

      function App() {
        return <>App</>;
      }
      
      export default App;
      
      
    • main.tsx:入口檔案

      import ReactDOM from "react-dom/client";
      import App from "./App.tsx";
      
      ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
      
      
    • vite-env.d.ts:環境配置檔案

      /// <reference types="vite/client" />
      
      

(2)useState

  • React 會根據傳入 useState 的預設值來自動推導資料型別,無需顯式標註

    import { useState } from "react";
    
    function App() {
      const [value, setValue] = useState(0);
    
      const change = () => {
        setValue(100);
      };
    
      return (
        <>
          {value}
          <button onClick={() => change()}>Click</button>
        </>
      );
    }
    
    export default App;
    
    
  • useState 本身是泛型函式,可以傳入自定義型別

    type User = {
      name: string;
      age: number;
    };
    
    const [user, setUser] = useState<User>();
    
    • 限制 useState 函式引數的初始值必須滿足型別 User | () => User
    • 限制 useState 函式的引數必須滿足型別 User | () => User | undefined
    • 狀態資料 user 具備 User 型別相關型別提示
  • 當不確定初始值應該為什麼型別時,將 useState 的初始值設為 null,如:

    type User = {
      name: string;
      age: number;
    };
    
    const [user, setUser] = useState<User | null>(null);
    

(3)Props

  • Props 新增型別是在給函式的引數做型別註解,可以使用 type 物件型別或 interface 介面,如:

    type Props = {
      className: string;
      style: object;
      onGetData?: (data: number) => void;
    };
    
    function Comp(props: Props) {
      const { className, style, onGetData } = props;
      const handleClick = () => {
        onGetData?.(123);
      };
    
      return (
        <div>
          <div className={className} style={style}>
            子元件文字內容
          </div>
          <button onClick={handleClick}>傳送 123</button>
        </div>
      );
    }
    
    function App() {
      const getData = (data: number) => {
        console.log(data);
      };
      return (
        <>
          <Comp className="foo" style={{ color: "red" }} onGetData={getData} />
        </>
      );
    }
    
    export default App;
    
    
  • children 是一個比較特殊的 prop,支援多種不同型別資料的輸入

    type Props = {
      children: React.ReactNode;
    };
    
    function Comp(props: Props) {
      const { children } = props;
      return <div>{children}</div>;
    }
    
    function App() {
      return (
        <>
          <Comp>
            <button>Click</button>
          </Comp>
        </>
      );
    }
    
    export default App;
    
    

(4)useRef

  • 可以直接把需要獲取的 DOM 元素的型別,作為泛型引數傳遞給 useRef

    import { useEffect, useRef } from "react";
    
    function App() {
      const inputRef = useRef<HTMLInputElement>(null);
      useEffect(() => {
        inputRef.current?.focus();
      }, []);
    
      return (
        <>
          <input ref={inputRef} />
        </>
      );
    }
    
    export default App;
    
    

-End-