React 的 logo 是一个原子图案, 原子组成了物质的表现。类似的, React 就像原子般构成了页面的表现; 而 Hooks 就如夸克, 其更接近 React 本质的样子, 但是直到 4 年后的今天才被真正设计出来。 —— Dan in React Conf(2018)
多个组件间逻辑复用
: 在 Class 中使用 React 不能将带有 state 的逻辑给单独抽离成 function, 其只能通过嵌套组件的方式来解决多个组件间逻辑复用的问题, 基于嵌套组件的思想存在 HOC 与 render props
两种设计模式。但是这两种设计模式是否存在缺陷呢?单个组件中的逻辑复用
: Class 中的生命周期 componentDidMount
、componentDidUpdate
甚至 componentWillUnMount
中的大多数逻辑基本是类似的, 必须拆散在不同生命周期中维护相同的逻辑对使用者是不友好的, 这样也造成了冗余的组件代码量。------- 2020.07.19 -------
组件重构
React Hooks 的常见陷阱
Hooks 不能做到?
[https://zhuanlan.zhihu.com/p/103150605], 分享一篇流畅风趣的 hooks 文章, 文风风趣不错, 可以借鉴。
---------- more ---------
原因是数组的解构比对象更加方便, 可以观察以下两种数据结构解构的差异。
返回数组时, 可以直接解构成任意名字。
[name, setName] = useState('路飞')[age, setAge] = useState(12)
返回对象时, 却需要多一层的命名。
{value: name, setValue: setName} = useState('路飞'){value: name, setValue: setName} = useState(12)
Hooks 是否可以设计成在组件中通过函数传参来使用? 比如进行如下调用?
const SomeContext = require('./SomeContext)function Example({ someProp }, hooks) {const contextValue = hooks.useContext(SomeContext)return <div>{someProp}{contextValue}</div>}
使用传递的劣势是会出现冗余的传递。(可以联想 Context 解决了什么)
Hooks 中的 setState 与 Class 中最大区别在于 Hooks 不会对多次 setState 进行合并操作。如果要执行合并操作, 可执行如下操作:
setState(prevState => {return { ...prevState, ...updateValues }})
此外可以对 class 与 Hooks 之间 setState
是异步还是同步的表现进行对比, 可以先对以下 4 种情形 render 输出的个数进行观察分析:
class 中的 setState:
export default class App extends React.Component {state = {name: '路飞',old: 12,gender: 'boy'}// 情形 ①: class 中异步调用 setStatecomponentDidMount() {this.setState({name: '娜美'})this.setState({old: 13})this.setState({gender: 'girl'})}// 情形 ②: class 中同步调用 setStatecomponentDidMount() {setTimeout(() => {this.setState({name: '娜美'})this.setState({old: 13})this.setState({gender: 'girl'})})}render() {console.log('render')const { name, old, gender } = this.statereturn (<>{name}{old}{gender}</>)}}
Hooks 中的 setState
export default function() {const [name, setName] = useState('路飞')const [old, setOld] = useState('12')const [gender, setGender] = useState('boy')// 情形③: Hooks 中异步调用 setStateuseEffect(() => {setName('娜美')setOld('13')setGender('girl')}, [])// 情形④: Hooks 中同步调用 setStateuseEffect(() => {setTimeout(() => {setName('娜美')setOld('13')setGender('girl')}, 0)}, [])console.log('render')return (<>{name}{old}{gender}</>)}
情形①、情形②、情形③、情形④ 中 render 输出的次数分别是 2, 4, 2, 4。可以看出在 React 中使用 class 写法和 hooks 写法是一一对应的。此外 setState 的执行是异步还是同步取决于其执行环境
。
场景: 在使用类模式的 React 中有时会使用 setState 的第二个参数来完成某些异步回调操作(比如接口请求), 在 Hooks 中如何对齐类模式中的这种用法呢?
使用 useRef 来控制一个标志符;
具体见 issue
在 timing-of-effects 中有提到 useEffect
的执行时机是在浏览器下一次 layout 与 paint 之后, 与之相对的 useLayoutEffect
的执行时机是在浏览器下一次 layout 与 paint 之前(同 componentDidMount
/componentDidUpdate
)。
useLayoutEffect 适用的场景为在 class 模式下在 componentDidMount/componentDidUpdate 中对样式进行调整的场景;
this-benchmark-is-indeed-flawed: 此文用数据比较了 useEffect 与 componentDidMount/componentDidUpdate 的执行时机。
思路: 借助 useRef
跳过头一次的执行。
function Demo() {const mounted = React.useRef(false)React.useEffect(() => {if (!mounted.current) {mounted.current = true} else {// do something mock componentDidUpdate}})}
React 官方在未来很可能会提供一个 usePrevious
的 hooks 来获取之前的 props 以及 state。
usePrevious
的核心思想是用 ref 来存储先前的值。
function usePrevous(value) {const ref = useRef()useEffect(() => {ref.current = value})return ref.current}
在 Hooks 中使用 useRef() 等价于在 Class 中使用 this.something。
/* in a function */const X = useRef()X.current // can read or write/* in a Class */this.X // can read or write
在 React 暗器百解 中提到了 getDerivedStateFromProps
是一种反模式, 但是极少数情况还是用得到该钩子, Hooks 没有该 api, 那其如何达到 getDerivedStateFromProps 的效果呢?
function ScrollView({row}) {const [isScrollingDown, setISScrollingDown] = setState(false)const [prevRow, setPrevRow] = setState(null)// 核心是创建一个 prevRow state 与函数调用方传进来的 row 进行比较if (row !== prevRow) {setISScrollingDown(prevRow !== null && row > prevRow)setPrevRow(row)}return `Scrolling down ${isScrollingDown}`}
可以使用 useReducer
来 hack forceUpdate
, 但是尽量避免 forceUpdate 的使用。
const useForceUpdate = () => {return useReducer(x => x + 1, 0)[1]}function handleClick() {const forceUpdate = useForceUpdate()forceUpdate()}
在 Hooks 中可以使用 useMemo
来作为 shouldComponentUpdate
的替代方案, 但 useMemo
只对 props 进行浅比较。
React.useMemo((props) => {// your component})
useMemo(() => <Component />) 等价于 useCallback(<Component />)
const fn = React.useCallback(() => {setOut(true)}, [])
const demo = React.useMemo(() => [...], [])
在 React 16.8 版本之后, 针对不是特别复杂
的业务场景, 可以使用 React 提供的 useContext
、useReducer
实现自定义简化版的 redux, 可见 todoList 中的运用。核心代码如下:
import React, { createContext, useContext, useReducer } from "react"// 创建 StoreContextconst StoreContext = createContext()// 构建 Provider 容器层export const StoreProvider = ({reducer, initialState, children}) => {return (<StoreContext.Provider value={useReducer(reducer, initialState)}>{children}</StoreContext.Provider>)}// 在子组件中调用 useStoreContext, 从而取得 Provider 中的 valueexport const useStoreContext = () => useContext(StoreContext)
但是针对特别复杂的场景目前不建议使用此模式, 因为 context 的机制会有性能问题。具体原因可见 react-redux v7 回退到订阅的原因
通常来说依赖列表中移除函数是不安全的。观察如下 demo
const { useState, useEffect } = Reactfunction Example({ someProp }) {function doSomething() {console.log(someProp) // 这里只输出 1, 点击按钮的 2 并没有输出。}useEffect(() => {doSomething()},[] // 🔴 这是不安全的, 因为在 doSomething 函数中使用了 someProps 属性)return <div>example</div>}export default function() {const [value, setValue] = useState(1)return (<><Example someProp={value} /><Button onClick={() => setValue(2)}>button</Button></>)}
在该 demo 中, 点击 button 按钮, 并没有打印出 2。解决上述问题有两种方法。
方法一: 将函数放入 useEffect
中, 同时将相关属性放入依赖项中。因为在依赖中改变的相关属性一目了然, 所以这也是首推的做法。
function Example({ someProp }) {useEffect(() => {function doSomething() {console.log(someProp)}doSomething()},[someProps] // 相关属性改变一目了然)return <div>example</div>}
方法二: 把函数加入依赖列表中
function Example({ someProp }) {function doSomething() {console.log(someProp)}useEffect(() => {doSomething()},[doSomething])return <div>example</div>}
方案二基本上不会单独使用, 它一般结合 useCallback
一起使用来处理某些函数计算量较大的函数。
function Example({ someProp }) {const doSomething = useCallback(() => {console.log(someProp)}, [someProp])useEffect(doSomething(),[doSomething])return <div>example</div>}
useState
的懒初始化, 用法如下const [value, setValue] = useState(() => createExpensiveObj)
function Image(props) {const ref = useRef(null)function getExpensiveObj() {if (ref.current === null) {ref.current = ExpensiveObj}return ref.current}// if need ExpensiveObj, call getExpensiveObj()}
关于竞态(race condition) 的解决方法:
方案一: 提供一个标志符, 在 clean effect
阶段中将其置空。代码如下:
function Article({ id }) {const [article, setArticle] = useState(null);useEffect(() => {let didCancel = false;async function fetchData() {const article = await API.fetchArticle(id);if (!didCancel) {setArticle(article);}}fetchData();return () => {didCancel = true;};}, [id]);// ...}
方案二: 使用 Suspense: Suspense 的机制能做到 render as fetch
。见 solving-race-conditions-with-suspense