第一部分
前言
Part 1 of React Hooks in Action with Suspense and Concurrent Mode introduces React Hooks and covers the key hooks in the first stable release of React 17. You’ll see how to manage state within function components, share state with children and deeper descendants, and synchronize state with outside services, servers, and APIs. You’ll also learn how to create your own hooks (while follow- ing the rules) and make the most of third-party hooks from established libraries like React Router, React Query, and React Spring.
这个部分主要介绍了React Hooks基本概念和React 17中一些关键hooks。通过阅读这个部分,我们将学会如何在函数组件中管理状态、进行状态通信、与服务端交互、如何创造自定义hook以及使用第三方hook。
第1章 React is envolving
1)函数组件 + hooks 代替类组件的可行性
使用函数组件+hooks,我们将得到:
- 更少的代码
- 更干净的代码组织
- 提炼出可复用或共享的feature
- 组件更易测试
- 不需要使用
super()、this和bind - 和类组件相似的生命周期模式
- 拥有状态管理、副作用和UI渲染
...
总的来说,类组件能做到的事情函数组件几乎都能做到。
在类组件中,副作用处理被分散在各个生命周期里。但仔细一想,我们真的需要这么多生命周期吗?我们真正需要的可能只是捕捉状态的变化,根据这个变化作出响应而已。hooks相当于把响应行为从按照生命周期区分转化到按照状态变化区分。这种区分方式从逻辑上来说更合理,从代码组织上来说则达到了解耦的效果。
2)Concurrent Mode & Suspense
The new React architecture breaks its tasks into smaller units of work, providing regular points for the browser or operating system to inform the application that a user is trying to interact with it. React’s scheduler can then decide what jobs to do based on the priority of each
Concurrent Mode带来了更丝滑的体验,原因在于它更加细分的任务颗粒度。根据优先级调度任务更精准和合理,用户体验也更流畅。
Wrap sections of your UI in Suspense components and use their fallback properties to let React know what content to show if one or more of the wrapped components suspends
类似于兜底。
第2章 Managing component state with the useState hook
1)使用 useState 的必要性
Just because you change the value of a variable within your component function doesn’t mean React will notice. If you want to get noticed, you can’t just say “Hello, World!” to people in your head; you have to say it out loud.
直接改变状态不会引起视图更新,而useState返回的setXXX可以。因为React承诺数据视图保持一致(涉及到DOM Tree对比),当我们使用setXXX,相当于使用了React暴露给我们的update function,从而借助React实现数据视图的一致。
2)类组件setState与函数组件useState 更新对象的区别
类组件setState是merge,函数组件useState是replace。useState更新对象时可以先用展开运算符展开旧对象,避免更新过程丢失属性。
3)使用过去的状态
By using hooks to hand over management of our state values to React, we don’t just ask it to update values and trigger re-renders; we also give it permission to efficiently schedule when any updates take place. React can intelligently batch updates together and ignore redundant updates.
我们把状态管理让渡给React,不仅是让它帮我们更新值和重绘页面,也是让它决定何时作出更新。React能高效地调度更新行为并忽视冗余的更新行为。
因此,当新值的更新依赖于旧值时,为了确保更新符合预期,我们应该给更新函数传一个function而不是value,类似于 setMyVal(oldVal ⇒ oldVal + 1)。这样React才能保证这个function里的旧值是过去所有旧值里的最新值。
4)多次调用 useState
React uses the order of the calls to consistently assign values and updater functions to the correct variables.
React依靠useState的调用顺序,确定不同state与不同useState的对应关系。
React是通过链表实现 hooks的调用的,所以只能在最顶层使用hook,不能在循环、条件或嵌套函数中调用hook。如果不这么做,就会造成链表环节的丢失,对应状态错乱。
拓展:
Dan Abramov. overreacted.io/why-do-hook…
为什么不能用key做映射取代「保持hook调用顺序一致」的限制?
因为难以保证key的唯一性,可能会出现同名冲突。
为什么不用symbol作为key,这样就不会有同名冲突了吧?
如果使用symbol,当你复用hook的时候,由于symbol生成的key的唯一性,大家的key就都是同一个,复用的主体之间就会互相污染了。
The actual Hooks proposal doesn’t have this problem because each call to useState() gets its own isolated state. Relying on a persistent call index frees us from worrying about name clashes.
而使用调用顺序来区分,能让每次被调用的useState都有自己独立的状态,就没有以上那些问题了。
第3章 Managing component state with the useReducer hook
当触发一个事件需要更新一系列相关状态时,就是「用一个reducer」管理代替「用N个useState」的时候了。
1)useReducer 初始化只会被调用一次
React pays attention to only the arguments passed to useReducer (in our case, reducer and initialState) the first time React invokes the component. On subsequent invocations, it ignores the arguments but still returns the current state and the dispatch function for the reducer.
这点类似useState。useState初始化时,就算给的初始值是个function,它也只会执行一次,节省了昂贵的初始化开销。
用函数给useReducer初始化,需要这么做:
const [state, dispatch] = useReducer(reducer, initArgument, initFunction);
2)通过 useReducer 拿到的 dispatch 函数总是同一个实例
For a particular call to useReducer, React will always return the same dispatch function. (Having an unchang- ing function is important when re-renders may depend on changing props or depen- dencies, as you’ll see in later chapters.)
函数保持是同一个实例非常重要,否则会触发无限重复渲染的问题。
第4章 Working with side effects
1)useLayoutEffect 的必要性和用法
This hook has the same API as useEffect but runs synchronously after React updates the DOM and before the browser repaints.
useEffect:在DOM Tree更新且浏览器重绘后触发
useLayoutEffect: 在Dom Tree更新后、浏览器重绘前触发。如果其中涉及对state的更新,直到更新完成浏览器才会重绘,类似于类组件的 componentDidMount 和 componentDidUpdate 。在涉及到视图上的变量时,使用 useLayoutEffect 代替 useEffect 可以避免状态闪烁问题。
官方文档推荐尽可能使用 useEffect 以避免阻塞视觉更新。
2)在 useEffect 中使用 async/await
async functions return a promise by default. Setting the effect function as async will cause trouble because React is looking for the return value of an effect to be a cleanup function. To solve the issues, remember to put the async function inside the effect function, rather than making the effect function async itself
async/await 是 promise 的语法糖,最后会返回一个 promise。对于React来说,它需要找到 useEffect的返回值作为收尾函数——一个promise不能满足这个需求。所以在 useEffect 中使用 async/await时,需要把 async 写在useEffect的函数体内部。
// 这样写会报错:
// Effect callbacks are synchronous to prevent race conditions. Put the async function inside.
useEffect(async () => {...}, [...])
// 正确写法:
useEffect(() => {
async function funcName(...){...};
funcName();
},
[...])
第5章 Managing component state with the useRef hook
1)useRef 和 useState 的区别
useState 更新值会引起页面重新渲染,更新后页面立即显示最新值。
useRef 更新值不会引起页面渲染,页面刷新后才会显示最新值。
2)用useRef代替直接操作DOM的必要性
使用useRef操作DOM的时候不需要初始值,初始值在JSX属性 ref 被赋值时确定。
- React会随着状态改变更新DOM,JS直接操作DOM可能会有问题,而useRef得到的ref和DOM是绑定的。
- 当组件被复用,用类似
document.getElementById取得的ref会丢失唯一性,而useRef得到的ref保证是唯一的。
第6章 Managing application state
1)父子通信
状态提升,父组件向子组件传递prop。prop可以是对子组件而言只读的一个value,也可以是一个能更新状态的function。
2)拆分组件
较小的组件颗粒度,能避免状态更新时触发多余的部分重新渲染。组件的作用域需要控制在合理范围。
3)通过 useReducer 传递和更新状态
总体流程:
- 父组件给予useReducer一个reducer函数和一个初始值,并取得返回的state和dispatch
- 将state和dispatch分发到有需要到子组件中
- 子组件接收state/dispatch。子组件内使用useEffect时如果用到了父组件下发的dispatch,则需要将其加入dependency array。如果dispatch在子组件内部取得,子组件可以确定这个dispatch是不变的;如果是从外部获取,子组件无法确定这个dispatch是否会变化。如果不加入,会报warning:missing a dependency
In the previous version, when we called useReducer from within the BookablesList component and assigned the dispatch function to the dispatch variable, React knew that the identity of the dispatch function would never change, so it didn’t need to be declared as a dependency for the effect. Now that a parent component passes dispatch in as a prop, BookablesList doesn’t know where it comes from so can’t be sure it won’t change.
4)一个函数组件的简约结构
export default function SomeComponent({state, dispatch, ...}) {
// 1. Variables
// 2. Effect
// 3. Handler functions
// 4. UI
}
5)useCallback 的必要性和用法
从 useState 或 useReducer 中取得的函数,React保证其始终是同一个实例。
但如果是自定义函数,那么每次渲染自定义函数都会被重新创建一遍。对于依赖了这个函数的 useEffect 而言,它的这个依赖项相当于一直在变化,于是这个 useEffect 会被无限触发。
useCallback 能保证自定义函数始终是同一个实例,只在该函数依赖项发生变化时才重新创建。
const stableFunction = useCallback(funtionToCache, dependencyList);
第7章 Managing performance with useMemo
1)useMemo 的必要性和用法
当组件使用的值来自一项高成本计算,如果什么也不做,那么每次组件更新重新渲染,这个高成本计算都会被重新跑一遍。这种重复计算是不必要的,因为在高成本计算依赖的某些值发生变化时,我们才需要重新计算。为了解决这个问题,我们需要使用 useMemo。
const memoizedValue = useMemo( () => expensiveFn(a, b), [a, b] );
React有时会为了清理内存释放 useMemo 的缓存,所以可能会出现依赖项没有改变,使用了useMemo的函数也被重新计算的情况。
如果没有赋予 useMemo 依赖项,那么 useMemo 包裹的函数每次渲染都会重新计算,相当于没有加 useMemo;如果给 useMemo 的依赖项传了一个空列表,那么 useMemo 可能会不进行重复运算,每次渲染都返回同一个值,也可能每次渲染都清掉缓存重新计算。最好避免这种导致不能确定的现象的行为。
为了节约内存,轻量重复计算可以不使用 useMemo 优化。
2)解决 useEffect 中多轮请求数据造成数据不同步的问题
用户在交互界面可能会来回触发多个涉及到请求数据的功能,例如在视图选项卡之间反复点击切换。我们无法知道数据请求什么时候会返回,一个后发出请求的返回值可能会先于先发出请求的返回值到达。如果不做处理,视图就不同步了:第一次行为对应了第二次行为的返回值,第二次行为对应了第一次行为的返回值。为了解决这个问题,我们可以取消已经发送的请求,也可以采取一个比较简单的做法:直接忽略老的返回值。
useEffect(() => {
let doUpdate = true; // 是否用返回值做更新
asyncFunc(param).then(res => {
if (doUpdate) setVal(res);
})
return () = doUpdate = false; // 视图状态更新 进入下一轮渲染 把这轮请求的返回值标记为无效数据
}, [deps])
在这段代码中,判断每次请求后「是否用返回值做更新」的变量初始值为 true。在数据成功返回前,如果依赖项 deps 更新,意味着这次的请求返回值将成为旧值,不再被需要。在下一次运行这个 useEffect 前,React 将会调用这个 useEffect 的清理函数,也就是把「是否用返回值做更新」设为false。这样一来,即使这次请求的数据返回了,React 也不会用它去做更新。
When fetching data within a call to useEffect, combine a local variable and the cleanup function to match a data request with its response...If the component re-renders with a new url, the cleanup function for the previous render will set the previous render’s doUpdate variable to false, preventing the previous then method callback from performing updates with stale data.
同一个 useEffect 不同轮调用间的本地变量是独立的,新一轮 useEffect 中的 doUpdate 为true 不会影响旧一轮 useEffect 中读取到的 doUpdate 为false,所以使用本地变量标记能把不同 requests 区分开来。
第8章 Managing state with the Context API
1)Context API 搭配 hook 的用法
-
首先创建一个 context
import { createContext } from 'react'; const SharedContext = createContext(); export default SharedContext; -
引入创建好的 context ,用 Provider 包裹需要使用这个context的顶级组件
import SharedContext from 'xxx'; export function function App = () => { return ( <SharedContext.Provider value={xxx}> <Child /> </SharedContext.Provider> ); } -
在子组件里消费 context
import { useContext } from 'react'; import SharedContext from 'xxx'; const shared = useContext(SharedContext); console.log(shared.id) // 拓展:可以把获取context的逻辑抽象成一个hook useShared = () => { if (!SharedContext) { throw new Error('must be used in Shared Provider'); } const context = React.useContext(SharedContext); return context; }
2)当 context 更新 减少不必要的渲染
当子组件对 context 中的状态进行更新,负责管理 context 状态的父组件就会重新渲染,这会导致父组件所有的子组件都重新渲染——不管他们是否是 context 的consumer。
我们希望做到,子组件根据 provider 状态的改变重新渲染,而不是在由父组件引起的全局重新渲染中重新渲染。方法如下:
-
将 provider 从父组件中抽出来,单独包在一个新组件里,类似 SharedProvider。
新组件从prop 中结构出 children 属性,在返回的UI中渲染出来。
export const SharedProvider = ({ children }) => {
<SharedContext.Provider value={xxx}>
{ children }
</SharedContext.Provider>
}
- 用这个新组件包裹父组件。
<SharedProvider>
<APP />
</SharedProvider>
- 子组件更新 context 状态,负责管理 context 状态的 SharedProvider 重新渲染。SharedProvider 的子组件中,只有 context 的 consumer 会重新渲染,其他子组件不会重新渲染。
同样是管理 context 状态,为什么父组件管理时和 SharedProvider 管理时表现不同?
因为 SharedProvider 是通过 prop 拿到它的子组件的。SharedProvider 内部状态的变化不会影响到 prop。子组件改变 context 状态不会改变 prop 中的children,所以 React 认为 children 不需要重新渲染。
但是,React 认为消费了 context 的子组件需要重新渲染。context 的消费者总是会在 context 的值改变后重新渲染。这样我们就实现了消费了 context 的子组件是跟随 context 的改变重新渲染,而不是因为整个组件树重新渲染而跟着渲染,从而减少了整个组件树不必要的渲染。
3)数据传递方案的选择
- 只有单个组件使用的数据,定义在该组件内部的 state 里
- 兄弟组件通信,由共同父组件的 prop 传递
- 跨层级数据传递,考虑组件组合(component composition)
- 不经常改变且跨层级被需要的数据,使用 context
4)context 的值是一个对象时可能会有性能问题
当 context 的值是一个对象,
- 假设里面包含了 主题色、当前用户信息、数据列表等等,子组件A只需要主题色,子组件B只需要用户信息,子组件C只需要数据列表。那么当数据列表更新时,除了子组件C,A和B作为这个 context 的消费者也会重新渲染——这是不必要的。
- 假设 context 的管理者(例如 SharedProvider)重新渲染,那么即使对象里的值没有发生改变,这个对象本身会被重新创建,引发消费者更新。
一种解决方法是把这个 context 的对象值拆成几个 context。
第9章 Creating your own hooks
1)拆分hook的原则
根据目的或希望取得的值来拆分功能,形成不同的hooks。hook的返回值可以是什么都没有、原始类型、函数、对象或者数组——只取决于它的调用者希望得到什么。
第10章 Using third-party hooks
1)useParams:React Router 获取路由参数
-
带参路由的表示
// App <Routes> <Route path="father/*" element={<Father />} /> </Routes> // father <Routes> <Route path="/"> <Children /> </Route> <Route path="/:id/xx"> <Children /> </Route> </Routes> -
路由参数的获取
React Router的
useParams返回路由参数的对象:{ param1: xxx, param2: xxx } const { param1, param2 } = useParams()路由参数对象中的值为字符串,需要 Number 使用
parseInt
2)useNavigate: React Router跳转路由
React Router的useNavigate 返回一个函数。这个函数接收一个字符串,作为跳转路由的路径。
const navigate = useNavigate()
navigate('/new')
3)useSearchParams:React Router操作搜索参数
React Router的useSearchParams返回一个对象数组,一个对象拥有get方法,用来获取搜索参数;一个对象用来设置路由参数:
// url: xxx/yyy?a=1&b=2
const [searchParams, setSearchParams] = useSearchParams()
const a = searchParams.get('a')
let newParam = {a: 1, b: 3}
setSearchParams(newParam, {replace: true}) // 使用新参数更新路由
// replace: true 使得浏览器把历史记录里的当前路由替换为更新后的路由,而不是叠加一层
4)useQuery:请求数据
- React Query 的引入
有点类似 Context,有 provider 和 consumer 的概念。
import { QueryClient, QueryClientProvider } from "react-query"
const qc = new QueryClient() // 创建实例
export default APP = () => {
<QueryClientProvider client={ qc }>
{/*被包裹的组件树中使用React Query的hooks时都能获取到client对象*/}
</QueryClientProvider>
}
useQuery :接收一个 key 、一个异步函数和一个可选的配置项
const {data, status, error} = useQuery(key, () => fetch(url), config?)
// key 用来标识React Query缓存中的数据
// 读取时React Query会返回key对应的数据 然后在后台请求最新的数据
拓展:
key 可以是字符串、数组或对象。推荐总是使用数组作为 key。就算使用字符串,内部也是被转化为了数组。
useQuery('todos') => useQuery(['todos'])
在数组key中具体化请求详情:
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
5)useMutation:更新数据
useMutation 返回一个包含 mutate 函数和状态值的对象
const {mutate, status, error} = useMutation(asyncFunction, config)
使用 mutate 函数,React Query 会执行异步函数更新一系列状态值。
第二部分
React’s evolution encompasses more than just hooks...Concurrent Mode lets React work on multiple versions of your UI simultaneously...Chapter 11 shows how you can use the Suspense component and error boundaries to decouple fallback UI from components for lazy loading and error reporting and recovery. Chapters 12 and 13 then head into more experimental territory, exploring how data fetching and image loading might integrate with Suspense and how you can use two further hooks, useTransition and use- DeferredValue, to present the best UI to users as state changes in your apps.
第二部分介绍了除了hooks之外,React新增的特性:Concurrent Mode、 Suspense 和 Error Boundaries。
第11章 Code splitting with Suspense
1)组件懒加载
使用 lazy + suspense 的组合
Suspense components catch pending promises thrown by not-yet-loaded components.
import {lazy, Suspense} from "react";
const LazyCalendar = lazy(() => import("./Calendar.js"))
//相当于
const getPromise = () => import(modulePath)
const LazyComponent = lazy(getPromise);
// 如果只到这一步,会报错:A React component suspended while rendering, but no fallback UI was specified.
// 我们需要使用 Suspense组件定义兜底内容
<Suspense fallback={<div>Loading...</div>}>
<CalendarWrapper />
</Suspense>
Suspense的包裹方式不同,呈现的UI不同:
<Suspense fallback={<div>Loading...</div>}>
<CalendarWrapper />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<CalendarWrapper />
</Suspense>
当第一个区域的loading结束,点击第二个区域,组件会马上加载出来。因为组件已经被加载进来了。
<Suspense fallback={<div>Loading...</div>}>
<CalendarWrapper />
<CalendarWrapper />
</Suspense>
包含中的所有组件都加载完成后,才会移除Suspense组件。
2)捕获错误
React 没有提供遇到错误时的兜底组件,但提供了可以捕获错误的生命周期方法。如果一个类组件使用了这些方法,那么就可以称这个组件为 error boundary。
import {Component} from "react";
export default class ErrorBoundary extends Component {
constructor (props) {
super(props);
this.state = {hasError: false};
}
static getDerivedStateFromError () { return { hasError: true }; }
render() {
const { children, fallback = <h1>Something went wrong.</h1>} = this.props;
return this.state.hasError ? fallback : children;
}
}
<ErrorBoundary fallback={ <Fragment><h1>Something went wrong! + recovery strategies?</h1></Fragment> }>
<Suspense fallback={<PageSpinner/>}>
<Routes>
<Route path="/bookings" element={<BookingsPage/>}/>
<Route path="/bookables/*" element={<BookablesPage/>}/>
</Routes>
</Suspense>
</ErrorBoundary>
只用error boundary包裹必要的组件,以便用户可以在出现错误以后继续使用不受错误影响的功能。
第12章 Integrating data fetching with Suspense
1)用 Suspense + React Query 加载资源
function Img ({src, alt, ...props}) {
const {data: imgObject} = useQuery(
src,
() => new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = src;
}),
{suspense: true} // 抛出pending的promise和错误
);
return <img src={imgObject.src} alt={alt} {...props}/>
}
function Avatar ({src, alt, fallbackSrc, ...props}) {
return (
<div className="user-avatar">
<Suspense fallback={<img src={fallbackSrc} alt="Fallback Avatar"/>}>
<Img src={src} alt={alt} {...props}/>
</Suspense>
</div>
)
}
2)用 React Query 预加载
queryClient.prefetchQuery(
`http://localhost:3001/img/${nextUser.img}`,
() => new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = `http://localhost:3001/img/${nextUser.img}`;
})
);
第13章 Experimenting with useTransition, useDeferredValue, and SuspenseList
1)启用 Concurrent Mode
const root = document.getElementById('root');
ReactDOM.unstable_createRoot(root).render(<App />);
2)useTransition:保持旧状态直至过渡到新状态
import { unstable_useTransition as useTransition } from 'react'
const [startTransition, isPending] = useTransition()
// 包裹希望在过渡过程中保留旧状态的更新方法
startTransition(() => setSelectedUser(nextUser));
if (isPending) {
// 界面半透明或操作按钮loading代替全局loading
}
3)useDeferredValue:保持一致性
import { unstable_useDeferredValue as useDeferredValue } from 'react'
const deferredUser = useDeferredValue(user)
const isPending = deferredUser !== user;
// 使用 deferredUser 代替原生user
useDeferredValue 跟踪一个状态值。当这个值发生改变:1. 新UI成功渲染,没有子组件延迟,useDeferredValue返回新值;2. 等待过程中,useDeferredValue返回旧值。
4)Concurrent Mode的意义
With Concurrent Mode, React can work on rendering multiple versions of the UI in memory at the same time and update the DOM with only the most appropriate version for the current state
Concurrent Mode就像是往React的数据存储中引入了版本管理的概念。根据生产需要来决定展示哪个版本的数据,增加了应用开发更加顺滑的可能性。