React18

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. 开发工具及插件

(3)JSX

a. 概述

  • JSX(JavaScript and XML)是 Javascript 语法扩展,可以在 Javascript 文件中书写类似 HTML 的标签
    • 组件使用 JSX 语法,从而使渲染逻辑和标签共同存在于组件中
  • JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息
  • 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-

posted @ 2024-06-20 00:17  SRIGT  阅读(34)  评论(0编辑  收藏  举报