为什么出现 Hooks

Hooks 就是把某个目标结果钩到某个可能会变化的数据源或事件源上,当数据源或事件源发生变化时,产生目标结果的代码会重新执行,产生更新后的结果。

  • 简化逻辑复用,原本只能使用高阶组件
  • 关注分离

    useState

    state 中不要保存可以通过计算得到的值

  1. props 传递过来的值
  2. 从 URL 中读到的值
  3. 从 cookie、localStorage 中读取的值

    useEffect

    副作用 useEffect 在每一次快照中会将其 Array Dependency 中的 state 和 返回的 cleanup 方法存储在自己的 hooks[index] 中。在下一次更新时会先执行 cleanup 方法,然后对比依赖的state 与上一次相比是否发生变化,进而决定副作用回调方法是否执行。

  1. useEffect(cb, depArray) {
  2. const hasNoDeps = !depArray;
  3. hooks[idx] = hooks[idx] || {};
  4. const {deps, cleanup} = hooks[idx]; // undefined when first render
  5. const hasChanged = deps
  6. ? !depArray.every((el, i) => el === deps[i])
  7. : true;
  8. if (hasNoDeps || hasChanged) {
  9. cleanup && cleanup();
  10. hooks[idx].cleanup = cb();
  11. hooks[idx].deps = depArray;
  12. }
  13. idx++;
  14. }

使用原则:

  1. 在复杂的副作用里,将逻辑拆分为 action 和 callback 两部分,这与 flux 思想类似:useEffect 中避免直接修改 state,只能触发 action。管理 state 的逻辑放在 callback 中,通过侦听 action 来执行具体的操作;
  2. 管理 state 逻辑的 callback 要么通过 setState 的参数获取所需 state,要么通过 useReducer,我们可以在 reducer 参数里获取到全部 state;
  3. useEffect 的 Array Dependency 里只包含触发 action 的变量;

    1. const UserContainer = () => {
    2. const [user, setUser] = useState(null);
    3. // 使用 useCallback 包裹
    4. const handleUserFetch = useCalllback(async () => {
    5. const result = await fetchUserAction();
    6. setUser(result);
    7. }, []);
    8. useEffect(() => {
    9. handleUserFetch();
    10. }, [handleUserFetch]); /* 将 handleUserFetch 作为依赖项传入 */
    11. if (!user) return <p>No data available.</p>
    12. return <UserCard data={user} />
    13. };

    useRef

    在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值。

  • 在多次渲染之间共享数据
  • 保存某个 DOM 节点的引用 ```java import React, { useState, useCallback, useRef } from “react”;

export default function Timer() { // 定义 time state 用于保存计时的累积时间 const [time, setTime] = useState(0);

// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量 const timer = useRef(null);

// 开始计时的事件处理函数 const handleStart = useCallback(() => { // 使用 current 属性设置 ref 的值 timer.current = window.setInterval(() => { setTime((time) => time + 1); }, 100); }, []);

// 暂停计时的事件处理函数 const handlePause = useCallback(() => { // 使用 clearInterval 来停止计时 window.clearInterval(timer.current); timer.current = null; }, []);

return (

{time / 10} seconds.
); }

  1. 执行一次性的初始化逻辑
  2. ```java
  3. import { useRef } from 'react';
  4. // 创建一个自定义 Hook 用于执行一次性代码
  5. function useSingleton(callback) {
  6. // 用一个 called ref 标记 callback 是否执行过
  7. const called = useRef(false);
  8. // 如果已经执行过,则直接返回
  9. if (called.current) return;
  10. // 第一次调用时直接执行
  11. callBack();
  12. // 设置标记为已执行过
  13. called.current = true;
  14. }
  1. import useSingleton from './useSingleton';
  2. const MyComp = () => {
  3. // 使用自定义 Hook
  4. useSingleton(() => {
  5. console.log('这段代码只执行一次');
  6. });
  7. return (
  8. <div>My Component</div>
  9. );
  10. };

父组件调用子组件的方法

useImperativeHandle 可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过 useImperativeHandle 添加到父组件中 useRef 定义的对象中。

1、useRef 创建引用变量
2、React.forwardRef 将引用变量传递给子组件
3、useImperativeHandle 将子组件内定义的函数作为属性,添加到父组件中的 ref 对象上。

  1. import React,{useState,useImperativeHandle} from 'react'
  2. function ChildComponent(props,ref) {
  3. const [count,setCount] = useState(0); //子组件定义内部变量count
  4. //子组件定义内部函数 addCount
  5. const addCount = () => {
  6. setCount(count + 1);
  7. }
  8. //子组件通过useImperativeHandle函数,将addCount函数添加到父组件中的ref.current中
  9. useImperativeHandle(ref,() => ({addCount}));
  10. return (
  11. <div>
  12. {count}
  13. <button onClick={addCount}>child</button>
  14. </div>
  15. )
  16. }
  17. //子组件导出时需要被React.forwardRef包裹,否则无法接收 ref这个参数
  18. export default React.forwardRef(ChildComponent);
  1. import React,{useRef} from 'react'
  2. import ChildComponent from './childComponent'
  3. function Imperative() {
  4. const childRef = useRef(null); //父组件定义一个对子组件的引用
  5. const clickHandle = () => {
  6. childRef.current.addCount(); //父组件调用子组件内部 addCount函数
  7. }
  8. return (
  9. <div>
  10. {/* 父组件通过给子组件添加 ref 属性,将childRef传递给子组件,
  11. 子组件获得该引用即可将内部函数添加到childRef中 */}
  12. <ChildComponent ref={childRef} />
  13. <button onClick={clickHandle}>child component do somting</button>
  14. </div>
  15. )
  16. }
  17. export default Imperative;

自定义 Hooks

封装通用逻辑

在 Hooks 中,你可以管理当前组件的 state,从而将更多的逻辑写在可重用的 Hooks 中。 普通的工具类中无法直接修改组件 state ,那么也就无法在数据改变的时候触发组件的重新渲染。

  1. import { useState } from 'react';
  2. const useAsync = (asyncFunction) => {
  3. // 设置三个异步逻辑相关的 state
  4. const [data, setData] = useState(null);
  5. const [loading, setLoading] = useState(false);
  6. const [error, setError] = useState(null);
  7. // 定义一个 callback 用于执行异步逻辑
  8. const execute = useCallback(() => {
  9. // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
  10. setLoading(true);
  11. setData(null);
  12. setError(null);
  13. return asyncFunction()
  14. .then((response) => {
  15. // 请求成功时,将数据写进 state,设置 loading 为 false
  16. setData(response);
  17. setLoading(false);
  18. })
  19. .catch((error) => {
  20. // 请求失败时,设置 loading 为 false,并设置错误状态
  21. setError(error);
  22. setLoading(false);
  23. });
  24. }, [asyncFunction]);
  25. return { execute, loading, data, error };
  26. };
  1. import React from "react";
  2. import useAsync from './useAsync';
  3. export default function UserList() {
  4. // 通过 useAsync 这个函数,只需要提供异步逻辑的实现
  5. const {
  6. execute: fetchUsers,
  7. data: users,
  8. loading,
  9. error,
  10. } = useAsync(async () => {
  11. const res = await fetch("https://reqres.in/api/users/");
  12. const json = await res.json();
  13. return json.data;
  14. });
  15. return (
  16. // 根据状态渲染 UI...
  17. );
  18. }

监听数据源状态

Hooks 可以让 React 的组件绑定在任何可能的数据源上。这样当数据源发生变化时,组件能够自动刷新。

  1. import { useState, useEffect } from 'react';
  2. // 获取横向,纵向滚动条位置
  3. const getPosition = () => {
  4. return {
  5. x: document.body.scrollLeft,
  6. y: document.body.scrollTop,
  7. };
  8. };
  9. const useScroll = () => {
  10. // 定一个 position 这个 state 保存滚动条位置
  11. const [position, setPosition] = useState(getPosition());
  12. useEffect(() => {
  13. const handler = () => {
  14. setPosition(getPosition(document));
  15. };
  16. // 监听 scroll 事件,更新滚动条位置
  17. document.addEventListener("scroll", handler);
  18. return () => {
  19. // 组件销毁时,取消事件监听
  20. document.removeEventListener("scroll", handler);
  21. };
  22. }, []);
  23. return position;
  24. };
  1. import React, { useCallback } from 'react';
  2. import useScroll from './useScroll';
  3. function ScrollTop() {
  4. const { y } = useScroll();
  5. const goTop = useCallback(() => {
  6. document.body.scrollTop = 0;
  7. }, []);
  8. const style = {
  9. position: "fixed",
  10. right: "10px",
  11. bottom: "10px",
  12. };
  13. // 当滚动条位置纵向超过 300 时,显示返回顶部按钮
  14. if (y > 300) {
  15. return (
  16. <button onClick={goTop} style={style}>
  17. Back to Top
  18. </button>
  19. );
  20. }
  21. // 否则不 render 任何 UI
  22. return null;
  23. }

拆分复杂组件

把 Hooks 就看成普通的函数,能隔离的尽量去做隔离,从而让代码更加模块化 尽量将相关的逻辑做成独立的 Hooks,然后在函数组中使用这些 Hooks,通过参数传递和返回值让 Hooks 之间完成交互。

  1. import React, { useEffect, useCallback, useMemo, useState } from "react";
  2. import { Select, Table } from "antd";
  3. import _ from "lodash";
  4. import useAsync from "./useAsync";
  5. const endpoint = "https://myserver.com/api/";
  6. const useArticles = () => {
  7. // 使用上面创建的 useAsync 获取文章列表
  8. const { execute, data, loading, error } = useAsync(
  9. useCallback(async () => {
  10. const res = await fetch(`${endpoint}/posts`);
  11. return await res.json();
  12. }, []),
  13. );
  14. // 执行异步调用
  15. useEffect(() => execute(), [execute]);
  16. // 返回语义化的数据结构
  17. return {
  18. articles: data,
  19. articlesLoading: loading,
  20. articlesError: error,
  21. };
  22. };
  23. const useCategories = () => {
  24. // 使用上面创建的 useAsync 获取分类列表
  25. const { execute, data, loading, error } = useAsync(
  26. useCallback(async () => {
  27. const res = await fetch(`${endpoint}/categories`);
  28. return await res.json();
  29. }, []),
  30. );
  31. // 执行异步调用
  32. useEffect(() => execute(), [execute]);
  33. // 返回语义化的数据结构
  34. return {
  35. categories: data,
  36. categoriesLoading: loading,
  37. categoriesError: error,
  38. };
  39. };
  40. const useCombinedArticles = (articles, categories) => {
  41. // 将文章数据和分类数据组合到一起
  42. return useMemo(() => {
  43. // 如果没有文章或者分类数据则返回 null
  44. if (!articles || !categories) return null;
  45. return articles.map((article) => {
  46. return {
  47. ...article,
  48. category: categories.find(
  49. (c) => String(c.id) === String(article.categoryId),
  50. ),
  51. };
  52. });
  53. }, [articles, categories]);
  54. };
  55. const useFilteredArticles = (articles, selectedCategory) => {
  56. // 实现按照分类过滤
  57. return useMemo(() => {
  58. if (!articles) return null;
  59. if (!selectedCategory) return articles;
  60. return articles.filter((article) => {
  61. console.log("filter: ", article.categoryId, selectedCategory);
  62. return String(article?.category?.name) === String(selectedCategory);
  63. });
  64. }, [articles, selectedCategory]);
  65. };
  66. const columns = [
  67. { dataIndex: "title", title: "Title" },
  68. { dataIndex: ["category", "name"], title: "Category" },
  69. ];
  70. export default function BlogList() {
  71. const [selectedCategory, setSelectedCategory] = useState(null);
  72. // 获取文章列表
  73. const { articles, articlesError } = useArticles();
  74. // 获取分类列表
  75. const { categories, categoriesError } = useCategories();
  76. // 组合数据
  77. const combined = useCombinedArticles(articles, categories);
  78. // 实现过滤
  79. const result = useFilteredArticles(combined, selectedCategory);
  80. // 分类下拉框选项用于过滤
  81. const options = useMemo(() => {
  82. const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({
  83. value: c.name,
  84. label: c.name,
  85. }));
  86. arr.unshift({ value: null, label: "All" });
  87. return arr;
  88. }, [categories]);
  89. // 如果出错,简单返回 Failed
  90. if (articlesError || categoriesError) return "Failed";
  91. // 如果没有结果,说明正在加载
  92. if (!result) return "Loading...";
  93. return (
  94. <div>
  95. <Select
  96. value={selectedCategory}
  97. onChange={(value) => setSelectedCategory(value)}
  98. options={options}
  99. style={{ width: "200px" }}
  100. placeholder="Select a category"
  101. />
  102. <Table dataSource={result} columns={columns} />
  103. </div>
  104. );
  105. }

复杂状态管理:如何保证状态一致性

  • 保证状态最小化
  • 避免中间状态,确保唯一数据源

    保证状态最小化

    某些数据如果能从已有的 State 中计算得到,那么我们就应该始终在用的时候去计算,而不要把计算的结果存到某个 State 中。 实用 useMemo 缓存计算的结果

  1. function FilterList({ data }) {
  2. // 设置关键字的 State
  3. const [searchKey, setSearchKey] = useState('');
  4. // 设置最终要展示的数据状态,并用原始数据作为初始值
  5. const [filtered, setFiltered] = useState(data);
  6. // 在 data 变化的时候,也重新生成最终数据
  7. useEffect(() => { setFiltered(data => {...}) }, [data, searchKey])
  8. // 处理用户的搜索关键字
  9. const handleSearch = useCallback(evt => {
  10. setSearchKey(evt.target.value);
  11. setFiltered(data.filter(item => {
  12. return item.title.includes(evt.target.value)));
  13. }));
  14. }, [filtered])
  15. return (
  16. <div>
  17. <input value={searchKey} onChange={handleSearch} />
  18. {/* 根据 filtered 数据渲染 UI */}
  19. </div>
  20. );
  21. }
  1. import React, { useState, useMemo } from "react";
  2. function FilterList({ data }) {
  3. const [searchKey, setSearchKey] = useState("");
  4. // 每当 searchKey 或者 data 变化的时候,重新计算最终结果
  5. const filtered = useMemo(() => {
  6. return data.filter((item) =>
  7. item.title.toLowerCase().includes(searchKey.toLowerCase())
  8. );
  9. }, [searchKey, data]);
  10. return (
  11. <div className="08-filter-list">
  12. <h2>Movies</h2>
  13. <input
  14. value={searchKey}
  15. placeholder="Search..."
  16. onChange={(evt) => setSearchKey(evt.target.value)}
  17. />
  18. <ul style={{ marginTop: 20 }}>
  19. {filtered.map((item) => (
  20. <li key={item.id}>{item.title}</li>
  21. ))}
  22. </ul>
  23. </div>
  24. );
  25. }

避免中间状态,确保唯一数据源

微信截图_20210619132250.png

异步处理:如何向服务器发送请求

无论大小项目,在开始实现第一个请求的时候,通常我们要做的第一件事应该都是创建一个自己的 API Client,之后所有的请求都会通过这个 Client 发出去。

封装远程资源

Hooks 的一个特性是可以绑定任何数据源,可以把 Get 请求当作一个远程数据源

  1. import { useState, useEffect } from "react";
  2. import apiClient from "./apiClient";
  3. // 将获取文章的 API 封装成一个远程资源 Hook
  4. const useArticle = (id) => {
  5. // 设置三个状态分别存储 data, error, loading
  6. const [data, setData] = useState(null);
  7. const [loading, setLoading] = useState(false);
  8. const [error, setError] = useState(null);
  9. useEffect(() => {
  10. // 重新获取数据时重置三个状态
  11. setLoading(true);
  12. setData(null);
  13. setError(null);
  14. apiClient
  15. .get(`/posts/${id}`)
  16. .then((res) => {
  17. // 请求成功时设置返回数据到状态
  18. setLoading(false);
  19. setData(res.data);
  20. })
  21. .catch((err) => {
  22. // 请求失败时设置错误状态
  23. setLoading(false);
  24. setError(err);
  25. });
  26. }, [id]); // 当 id 变化时重新获取数据
  27. // 将三个状态作为 Hook 的返回值
  28. return {
  29. loading,
  30. error,
  31. data
  32. };
  33. };

函数组件设计模式:如何应对复杂条件渲染场景

容器模式:实现按条件执行 Hooks

把原来需要条件运行的 Hooks 拆分成子组件,然后通过一个容器组件来进行实际的条件判断,从而渲染不同的组件,实现按条件渲染的目的。

  • Hooks 必须按顺序被执行到,不能放到条件判断、循环等语句中。 ```javascript // 定义一个容器组件用于封装真正的 UserInfoModal export default function UserInfoModalWrapper({ visible, …rest, // 使用 rest 获取除了 visible 之外的属性 }) { // 如果对话框不显示,则不 render 任何内容 if (!visible) return null; // 否则真正执行对话框的组件逻辑 return ; }
  1. <a name="w6xdr"></a>
  2. ## 使用 render props 模式重用 UI 逻辑
  3. > render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容
  4. > Hooks 有一个局限,那就是只能用作数据逻辑的重用,而一旦涉及 UI 表现逻辑的重用,而这可以使用 render props 来解决
  5. ```javascript
  6. import { useState, useCallback } from "react";
  7. function CounterRenderProps({ children }) {
  8. const [count, setCount] = useState(0);
  9. const increment = useCallback(() => {
  10. setCount(count + 1);
  11. }, [count]);
  12. const decrement = useCallback(() => {
  13. setCount(count - 1);
  14. }, [count]);
  15. return children({ count, increment, decrement });
  16. }
  1. function CounterRenderPropsExample() {
  2. return (
  3. <CounterRenderProps>
  4. {({ count, increment, decrement }) => {
  5. return (
  6. <div>
  7. <button onClick={decrement}>-</button>
  8. <span>{count}</span>
  9. <button onClick={increment}>+</button>
  10. </div>
  11. );
  12. }}
  13. </CounterRenderProps>
  14. );
  15. }

事件处理:如何创建自定义事件

是否需要 useCallback ,和函数的复杂度没有必然关系,而是和回调函数绑定到哪个组件有关。这是为了避免因组件属性变化而导致不必要的重新渲染。

  • 原生的 DOM 节点,比如 button、input 等,不用担心重新渲染的
  • 自定义组件,或者一些 UI 框架的组件需要考虑性能问题

    Hooks 具备绑定任何数据源的能力 使用 Hooks 封装事件,相当于将这些动作事件看作不断变化的数据源

  1. import { useEffect, useState } from "react";
  2. // 使用 document.body 作为默认的监听节点
  3. const useKeyPress = (domNode = document.body) => {
  4. const [key, setKey] = useState(null);
  5. useEffect(() => {
  6. const handleKeyPress = (evt) => {
  7. setKey(evt.keyCode);
  8. };
  9. // 监听按键事件
  10. domNode.addEventListener("keypress", handleKeyPress);
  11. return () => {
  12. // 接触监听按键事件
  13. domNode.removeEventListener("keypress", handleKeyPress);
  14. };
  15. }, [domNode]);
  16. return key;
  17. };
  1. import useKeyPress from './useKeyPress';
  2. function UseKeyPressExample() => {
  3. const key = useKeyPress();
  4. return (
  5. <div>
  6. <h1>UseKeyPress</h1>
  7. <label>Key pressed: {key || "N/A"}</label>
  8. </div>
  9. );
  10. };

使用浮动层:如何展示对话框,并给对话框传递参数

一个实现业务逻辑的 Modal 究竟应该在哪个组件中去声明?又该怎么和它进行交互呢?

如何用一个统一的方式去管理对话框,从而让对话框相关的业务逻辑能够更加模块化,以及和其他业务逻辑进行解耦。

使用全局状态管理所有对话框

对话框这种模式的本质就是一个独立的窗口,它和一个拥有独立 URL 的页面在功能上和形式上都是极为类似的

处理对话框返回值

把用户在对话框中的操作看成一个异步操作逻辑,那么用户在完成了对话框中内容的操作之后,就认为异步逻辑完成了

  1. const modal = useNiceModal('my-modal');
  2. // 实现一个 promise API 来处理返回值
  3. modal.show(args).then(result => {});
  1. // 使用一个 object 缓存 promise 的 resolve 回调函数
  2. const modalCallbacks = {};
  3. export const useNiceModal = (modalId) => {
  4. const dispatch = useDispatch();
  5. const show = useCallback(
  6. (args) => {
  7. return new Promise((resolve) => {
  8. // 显示对话框时,返回 promise 并且将 resolve 方法临时存起来
  9. modalCallbacks[modalId] = resolve;
  10. dispatch(showModal(modalId, args));
  11. });
  12. },
  13. [dispatch, modalId],
  14. );
  15. const resolve = useCallback(
  16. (args) => {
  17. if (modalCallbacks[modalId]) {
  18. // 如果存在 resolve 回调函数,那么就调用
  19. modalCallbacks[modalId](args);
  20. // 确保只能 resolve 一次
  21. delete modalCallbacks[modalId];
  22. }
  23. },
  24. [modalId],
  25. );
  26. // 其它逻辑...
  27. // 将 resolve 也作为返回值的一部分
  28. return { show, hide, resolve, visible, hiding };
  29. };

路由管理

  • 提供了按页面去组织整个应用程序的能力
  • 统一资源定位符:对于能够通过前端应用展现的每一个资源,你都要考虑 URL 是否能唯一地定位到这个资源

    按需加载

    使用 import 语句,定义按需加载的起始模块

    1. function ProfilePage() {
    2. // 定义一个 state 用于存放需要加载的组件
    3. const [RealPage, setRealPage] = useState(null);
    4. // 根据路径动态加载真正的组件实现
    5. import('./RealProfilePage').then((comp) => {
    6. setRealPage(Comp);
    7. });
    8. // 如果组件未加载则显示 Loading 状态
    9. if (!RealPage) return 'Loading....';
    10. // 组件加载成功后则将其渲染到界面
    11. return <RealPage />
    12. }

    import() 这个语句完全是由 Webpack 进行处理的。Webpack 会将以“./RealProfilePage”模块为起点的所有依赖模块,单独打成一个包。并且,Webpack 还会生成代码,用于按需加载这个模块。

  • 按业务模块为目标去做隔离,尽量在每个模块的起始页面去定义这个拆分点

    使用 react-lodable,实现组件的异步加载

    ```javascript import Loadable from “react-loadable”;

// 创建一个显示加载状态的组件 function Loading({ error }) { return error ? ‘Failed’ : ‘Loading’; } // 创建加载器组件 const HelloLazyLoad = Loadable({ loader: () => import(“./RealHelloLazyLoad”), loading: Loading, }); ```

使用 service worker 缓存前端资源

d7b22ce8e29eb1a8bee2e2c44a4b82f1.webp

参考资料

  1. 两年React老兵的总结 - 如何组织React项目
  2. React Hooks 设计思想 #12
  3. React Hooks Explained: useImperativeHandle
  4. 13 useImperativeHandle基础用法