为什么出现 Hooks
Hooks 就是把某个目标结果钩到某个可能会变化的数据源或事件源上,当数据源或事件源发生变化时,产生目标结果的代码会重新执行,产生更新后的结果。
- props 传递过来的值
- 从 URL 中读到的值
- 从 cookie、localStorage 中读取的值
useEffect
副作用 useEffect 在每一次快照中会将其 Array Dependency 中的 state 和 返回的 cleanup 方法存储在自己的 hooks[index] 中。在下一次更新时会先执行 cleanup 方法,然后对比依赖的state 与上一次相比是否发生变化,进而决定副作用回调方法是否执行。
useEffect(cb, depArray) {const hasNoDeps = !depArray;hooks[idx] = hooks[idx] || {};const {deps, cleanup} = hooks[idx]; // undefined when first renderconst hasChanged = deps? !depArray.every((el, i) => el === deps[i]): true;if (hasNoDeps || hasChanged) {cleanup && cleanup();hooks[idx].cleanup = cb();hooks[idx].deps = depArray;}idx++;}
使用原则:
- 在复杂的副作用里,将逻辑拆分为 action 和 callback 两部分,这与 flux 思想类似:useEffect 中避免直接修改 state,只能触发 action。管理 state 的逻辑放在 callback 中,通过侦听 action 来执行具体的操作;
- 管理 state 逻辑的 callback 要么通过 setState 的参数获取所需 state,要么通过 useReducer,我们可以在 reducer 参数里获取到全部 state;
useEffect 的 Array Dependency 里只包含触发 action 的变量;
const UserContainer = () => {const [user, setUser] = useState(null);// 使用 useCallback 包裹const handleUserFetch = useCalllback(async () => {const result = await fetchUserAction();setUser(result);}, []);useEffect(() => {handleUserFetch();}, [handleUserFetch]); /* 将 handleUserFetch 作为依赖项传入 */if (!user) return <p>No data available.</p>return <UserCard data={user} />};
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 (
执行一次性的初始化逻辑```javaimport { useRef } from 'react';// 创建一个自定义 Hook 用于执行一次性代码function useSingleton(callback) {// 用一个 called ref 标记 callback 是否执行过const called = useRef(false);// 如果已经执行过,则直接返回if (called.current) return;// 第一次调用时直接执行callBack();// 设置标记为已执行过called.current = true;}
import useSingleton from './useSingleton';const MyComp = () => {// 使用自定义 HookuseSingleton(() => {console.log('这段代码只执行一次');});return (<div>My Component</div>);};
父组件调用子组件的方法
useImperativeHandle 可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过 useImperativeHandle 添加到父组件中 useRef 定义的对象中。
1、useRef 创建引用变量
2、React.forwardRef 将引用变量传递给子组件
3、useImperativeHandle 将子组件内定义的函数作为属性,添加到父组件中的 ref 对象上。
import React,{useState,useImperativeHandle} from 'react'function ChildComponent(props,ref) {const [count,setCount] = useState(0); //子组件定义内部变量count//子组件定义内部函数 addCountconst addCount = () => {setCount(count + 1);}//子组件通过useImperativeHandle函数,将addCount函数添加到父组件中的ref.current中useImperativeHandle(ref,() => ({addCount}));return (<div>{count}<button onClick={addCount}>child</button></div>)}//子组件导出时需要被React.forwardRef包裹,否则无法接收 ref这个参数export default React.forwardRef(ChildComponent);
import React,{useRef} from 'react'import ChildComponent from './childComponent'function Imperative() {const childRef = useRef(null); //父组件定义一个对子组件的引用const clickHandle = () => {childRef.current.addCount(); //父组件调用子组件内部 addCount函数}return (<div>{/* 父组件通过给子组件添加 ref 属性,将childRef传递给子组件,子组件获得该引用即可将内部函数添加到childRef中 */}<ChildComponent ref={childRef} /><button onClick={clickHandle}>child component do somting</button></div>)}export default Imperative;
自定义 Hooks
封装通用逻辑
在 Hooks 中,你可以管理当前组件的 state,从而将更多的逻辑写在可重用的 Hooks 中。 普通的工具类中无法直接修改组件 state ,那么也就无法在数据改变的时候触发组件的重新渲染。
import { useState } from 'react';const useAsync = (asyncFunction) => {// 设置三个异步逻辑相关的 stateconst [data, setData] = useState(null);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);// 定义一个 callback 用于执行异步逻辑const execute = useCallback(() => {// 请求开始时,设置 loading 为 true,清除已有数据和 error 状态setLoading(true);setData(null);setError(null);return asyncFunction().then((response) => {// 请求成功时,将数据写进 state,设置 loading 为 falsesetData(response);setLoading(false);}).catch((error) => {// 请求失败时,设置 loading 为 false,并设置错误状态setError(error);setLoading(false);});}, [asyncFunction]);return { execute, loading, data, error };};
import React from "react";import useAsync from './useAsync';export default function UserList() {// 通过 useAsync 这个函数,只需要提供异步逻辑的实现const {execute: fetchUsers,data: users,loading,error,} = useAsync(async () => {const res = await fetch("https://reqres.in/api/users/");const json = await res.json();return json.data;});return (// 根据状态渲染 UI...);}
监听数据源状态
Hooks 可以让 React 的组件绑定在任何可能的数据源上。这样当数据源发生变化时,组件能够自动刷新。
import { useState, useEffect } from 'react';// 获取横向,纵向滚动条位置const getPosition = () => {return {x: document.body.scrollLeft,y: document.body.scrollTop,};};const useScroll = () => {// 定一个 position 这个 state 保存滚动条位置const [position, setPosition] = useState(getPosition());useEffect(() => {const handler = () => {setPosition(getPosition(document));};// 监听 scroll 事件,更新滚动条位置document.addEventListener("scroll", handler);return () => {// 组件销毁时,取消事件监听document.removeEventListener("scroll", handler);};}, []);return position;};
import React, { useCallback } from 'react';import useScroll from './useScroll';function ScrollTop() {const { y } = useScroll();const goTop = useCallback(() => {document.body.scrollTop = 0;}, []);const style = {position: "fixed",right: "10px",bottom: "10px",};// 当滚动条位置纵向超过 300 时,显示返回顶部按钮if (y > 300) {return (<button onClick={goTop} style={style}>Back to Top</button>);}// 否则不 render 任何 UIreturn null;}
拆分复杂组件
把 Hooks 就看成普通的函数,能隔离的尽量去做隔离,从而让代码更加模块化 尽量将相关的逻辑做成独立的 Hooks,然后在函数组中使用这些 Hooks,通过参数传递和返回值让 Hooks 之间完成交互。
import React, { useEffect, useCallback, useMemo, useState } from "react";import { Select, Table } from "antd";import _ from "lodash";import useAsync from "./useAsync";const endpoint = "https://myserver.com/api/";const useArticles = () => {// 使用上面创建的 useAsync 获取文章列表const { execute, data, loading, error } = useAsync(useCallback(async () => {const res = await fetch(`${endpoint}/posts`);return await res.json();}, []),);// 执行异步调用useEffect(() => execute(), [execute]);// 返回语义化的数据结构return {articles: data,articlesLoading: loading,articlesError: error,};};const useCategories = () => {// 使用上面创建的 useAsync 获取分类列表const { execute, data, loading, error } = useAsync(useCallback(async () => {const res = await fetch(`${endpoint}/categories`);return await res.json();}, []),);// 执行异步调用useEffect(() => execute(), [execute]);// 返回语义化的数据结构return {categories: data,categoriesLoading: loading,categoriesError: error,};};const useCombinedArticles = (articles, categories) => {// 将文章数据和分类数据组合到一起return useMemo(() => {// 如果没有文章或者分类数据则返回 nullif (!articles || !categories) return null;return articles.map((article) => {return {...article,category: categories.find((c) => String(c.id) === String(article.categoryId),),};});}, [articles, categories]);};const useFilteredArticles = (articles, selectedCategory) => {// 实现按照分类过滤return useMemo(() => {if (!articles) return null;if (!selectedCategory) return articles;return articles.filter((article) => {console.log("filter: ", article.categoryId, selectedCategory);return String(article?.category?.name) === String(selectedCategory);});}, [articles, selectedCategory]);};const columns = [{ dataIndex: "title", title: "Title" },{ dataIndex: ["category", "name"], title: "Category" },];export default function BlogList() {const [selectedCategory, setSelectedCategory] = useState(null);// 获取文章列表const { articles, articlesError } = useArticles();// 获取分类列表const { categories, categoriesError } = useCategories();// 组合数据const combined = useCombinedArticles(articles, categories);// 实现过滤const result = useFilteredArticles(combined, selectedCategory);// 分类下拉框选项用于过滤const options = useMemo(() => {const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({value: c.name,label: c.name,}));arr.unshift({ value: null, label: "All" });return arr;}, [categories]);// 如果出错,简单返回 Failedif (articlesError || categoriesError) return "Failed";// 如果没有结果,说明正在加载if (!result) return "Loading...";return (<div><Selectvalue={selectedCategory}onChange={(value) => setSelectedCategory(value)}options={options}style={{ width: "200px" }}placeholder="Select a category"/><Table dataSource={result} columns={columns} /></div>);}
复杂状态管理:如何保证状态一致性
- 保证状态最小化
- 避免中间状态,确保唯一数据源
保证状态最小化
某些数据如果能从已有的 State 中计算得到,那么我们就应该始终在用的时候去计算,而不要把计算的结果存到某个 State 中。 实用 useMemo 缓存计算的结果
function FilterList({ data }) {// 设置关键字的 Stateconst [searchKey, setSearchKey] = useState('');// 设置最终要展示的数据状态,并用原始数据作为初始值const [filtered, setFiltered] = useState(data);// 在 data 变化的时候,也重新生成最终数据useEffect(() => { setFiltered(data => {...}) }, [data, searchKey])// 处理用户的搜索关键字const handleSearch = useCallback(evt => {setSearchKey(evt.target.value);setFiltered(data.filter(item => {return item.title.includes(evt.target.value)));}));}, [filtered])return (<div><input value={searchKey} onChange={handleSearch} />{/* 根据 filtered 数据渲染 UI */}</div>);}
import React, { useState, useMemo } from "react";function FilterList({ data }) {const [searchKey, setSearchKey] = useState("");// 每当 searchKey 或者 data 变化的时候,重新计算最终结果const filtered = useMemo(() => {return data.filter((item) =>item.title.toLowerCase().includes(searchKey.toLowerCase()));}, [searchKey, data]);return (<div className="08-filter-list"><h2>Movies</h2><inputvalue={searchKey}placeholder="Search..."onChange={(evt) => setSearchKey(evt.target.value)}/><ul style={{ marginTop: 20 }}>{filtered.map((item) => (<li key={item.id}>{item.title}</li>))}</ul></div>);}
避免中间状态,确保唯一数据源
异步处理:如何向服务器发送请求
无论大小项目,在开始实现第一个请求的时候,通常我们要做的第一件事应该都是创建一个自己的 API Client,之后所有的请求都会通过这个 Client 发出去。
封装远程资源
Hooks 的一个特性是可以绑定任何数据源,可以把 Get 请求当作一个远程数据源
import { useState, useEffect } from "react";import apiClient from "./apiClient";// 将获取文章的 API 封装成一个远程资源 Hookconst useArticle = (id) => {// 设置三个状态分别存储 data, error, loadingconst [data, setData] = useState(null);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);useEffect(() => {// 重新获取数据时重置三个状态setLoading(true);setData(null);setError(null);apiClient.get(`/posts/${id}`).then((res) => {// 请求成功时设置返回数据到状态setLoading(false);setData(res.data);}).catch((err) => {// 请求失败时设置错误状态setLoading(false);setError(err);});}, [id]); // 当 id 变化时重新获取数据// 将三个状态作为 Hook 的返回值return {loading,error,data};};
函数组件设计模式:如何应对复杂条件渲染场景
容器模式:实现按条件执行 Hooks
把原来需要条件运行的 Hooks 拆分成子组件,然后通过一个容器组件来进行实际的条件判断,从而渲染不同的组件,实现按条件渲染的目的。
- Hooks 必须按顺序被执行到,不能放到条件判断、循环等语句中。
```javascript
// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
…rest, // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return
; }
<a name="w6xdr"></a>## 使用 render props 模式重用 UI 逻辑> render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容> Hooks 有一个局限,那就是只能用作数据逻辑的重用,而一旦涉及 UI 表现逻辑的重用,而这可以使用 render props 来解决```javascriptimport { useState, useCallback } from "react";function CounterRenderProps({ children }) {const [count, setCount] = useState(0);const increment = useCallback(() => {setCount(count + 1);}, [count]);const decrement = useCallback(() => {setCount(count - 1);}, [count]);return children({ count, increment, decrement });}
function CounterRenderPropsExample() {return (<CounterRenderProps>{({ count, increment, decrement }) => {return (<div><button onClick={decrement}>-</button><span>{count}</span><button onClick={increment}>+</button></div>);}}</CounterRenderProps>);}
事件处理:如何创建自定义事件
是否需要 useCallback ,和函数的复杂度没有必然关系,而是和回调函数绑定到哪个组件有关。这是为了避免因组件属性变化而导致不必要的重新渲染。
- 原生的 DOM 节点,比如 button、input 等,不用担心重新渲染的
- 自定义组件,或者一些 UI 框架的组件需要考虑性能问题
Hooks 具备绑定任何数据源的能力 使用 Hooks 封装事件,相当于将这些动作事件看作不断变化的数据源
import { useEffect, useState } from "react";// 使用 document.body 作为默认的监听节点const useKeyPress = (domNode = document.body) => {const [key, setKey] = useState(null);useEffect(() => {const handleKeyPress = (evt) => {setKey(evt.keyCode);};// 监听按键事件domNode.addEventListener("keypress", handleKeyPress);return () => {// 接触监听按键事件domNode.removeEventListener("keypress", handleKeyPress);};}, [domNode]);return key;};
import useKeyPress from './useKeyPress';function UseKeyPressExample() => {const key = useKeyPress();return (<div><h1>UseKeyPress</h1><label>Key pressed: {key || "N/A"}</label></div>);};
使用浮动层:如何展示对话框,并给对话框传递参数
一个实现业务逻辑的 Modal 究竟应该在哪个组件中去声明?又该怎么和它进行交互呢?
如何用一个统一的方式去管理对话框,从而让对话框相关的业务逻辑能够更加模块化,以及和其他业务逻辑进行解耦。
使用全局状态管理所有对话框
对话框这种模式的本质就是一个独立的窗口,它和一个拥有独立 URL 的页面在功能上和形式上都是极为类似的
处理对话框返回值
把用户在对话框中的操作看成一个异步操作逻辑,那么用户在完成了对话框中内容的操作之后,就认为异步逻辑完成了
const modal = useNiceModal('my-modal');// 实现一个 promise API 来处理返回值modal.show(args).then(result => {});
// 使用一个 object 缓存 promise 的 resolve 回调函数const modalCallbacks = {};export const useNiceModal = (modalId) => {const dispatch = useDispatch();const show = useCallback((args) => {return new Promise((resolve) => {// 显示对话框时,返回 promise 并且将 resolve 方法临时存起来modalCallbacks[modalId] = resolve;dispatch(showModal(modalId, args));});},[dispatch, modalId],);const resolve = useCallback((args) => {if (modalCallbacks[modalId]) {// 如果存在 resolve 回调函数,那么就调用modalCallbacks[modalId](args);// 确保只能 resolve 一次delete modalCallbacks[modalId];}},[modalId],);// 其它逻辑...// 将 resolve 也作为返回值的一部分return { show, hide, resolve, visible, hiding };};
路由管理
- 提供了按页面去组织整个应用程序的能力
统一资源定位符:对于能够通过前端应用展现的每一个资源,你都要考虑 URL 是否能唯一地定位到这个资源
按需加载
使用 import 语句,定义按需加载的起始模块
function ProfilePage() {// 定义一个 state 用于存放需要加载的组件const [RealPage, setRealPage] = useState(null);// 根据路径动态加载真正的组件实现import('./RealProfilePage').then((comp) => {setRealPage(Comp);});// 如果组件未加载则显示 Loading 状态if (!RealPage) return 'Loading....';// 组件加载成功后则将其渲染到界面return <RealPage />}
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, }); ```
