hooks 出现的原因

  • side effects 处理
  • state 管理
  • wrapping hell

为函数组件新增了添加和处理自身状态的能力。在类组件和函数组件混用的情况下,若想对组件增加自身状态管理能力,就不得不将其重构成类组件,反之亦然。在类组件中,副作用的产生和处理都是由生命周期函数带来的。副作用可以是在 React 中请求数据或者与浏览器 API 交互。通常这些副总用都伴有创建和清除两个阶段。React Hooks 的出现解决了上述两个问题,并且让组件之间的交互更为流畅。使用 useEffect 来处理副作用,使用 useState 来为组件创建状态。

Q: React 清除 effect 的确切时间是什么时候?
当组件卸载时,React 会执行清除操作。但实际情况是,每次渲染的时候 effects 都会执行一次,同时清除操作也会在 effect 执行之前执行一遍。

Rules of hooks

只在最顶层调用 hooks。不要在循环结构,条件判断或嵌套函数中进行调用。
只在 React 函数中调用 hooks:

  • 从 React 函数式组件中调用
  • 从自定义 hooks 中调用

useInterval implementation

  1. import React, { useState, useRef } from 'react'
  2. function useInterval(callback, delay) {
  3. const [delay, setDelay] = useState(delay)
  4. const savedCallback = useRef()
  5. useEffect(() => {
  6. savedCallback.current = callback
  7. }, [callback])
  8. function tick() {
  9. savedCallback.current()
  10. }
  11. useEffect(() => {
  12. const id = setInterval(tick, delay)
  13. return () => clearInterval(id)
  14. }, [delay])
  15. }

useRef可以返回一个含有 current属性的对象,这个对象可以在渲染过程中进行共享。用此特性将最新的 callback进行存储,然后他就会一直执行第一个 setInterval函数,来实现数字的变更。

函数组件和类组件的区别

  1. // class
  2. class ProfilePage extends React.Component {
  3. showMessage = () => {
  4. alert('Followed ' + this.props.user);
  5. };
  6. handleClick = () => {
  7. setTimeout(this.showMessage, 3000);
  8. };
  9. render() {
  10. return <button onClick={this.handleClick}>Follow</button>;
  11. }
  12. }
  13. // inside render function
  14. class ProfilePage extends React.Component {
  15. render() {
  16. // Capture the props!
  17. const props = this.props;
  18. // Note: we are *inside render*.
  19. // These aren't class methods.
  20. const showMessage = () => {
  21. alert('Followed ' + props.user);
  22. };
  23. const handleClick = () => {
  24. setTimeout(showMessage, 3000);
  25. };
  26. return <button onClick={handleClick}>Follow</button>;
  27. }
  28. }
  29. // functional
  30. function ProfilePage(props) {
  31. const showMessage = () => {
  32. alert('Followed ' + props.user);
  33. };
  34. const handleClick = () => {
  35. setTimeout(showMessage, 3000);
  36. };
  37. return (
  38. <button onClick={handleClick}>Follow</button>
  39. );
  40. }

在 React 中,props是不可变的,而 this 却是可修改的。在上述的例子中,我们过了3秒才重新去调取 this.props.user这个值,此时我们取到的是一个当前值而不是点击按钮时的那个值。

以上的问题,我们可以通过 JS 的闭包来解决,在闭包中存的 props 值是不可变的,我们也没有用其他的方法对其进行修改。这样我们就获取到了在渲染的当前时间点的 props

Closures are often avoided because it’s hard to think about a value that can be mutated over time. But in React, props and state are immutable.

如果我们想要获得最新的值,在函数式组件中可以使用 useRef,它会创建一个包含 current属性的对象。而这个对象是可变的,我们可以手动的操作这个对象的值。

  1. function MessageThread() {
  2. const [message, setMessage] = useState('');
  3. // Keep track of the latest value.
  4. const latestMessage = useRef('');
  5. useEffect(() => {
  6. latestMessage.current = message;
  7. });
  8. const showMessage = () => {
  9. alert('You said: ' + latestMessage.current);
  10. };
  11. const handleSendClick = () => {
  12. setTimeout(showMessage, 3000);
  13. };
  14. const handleMessageChange = (e) => {
  15. setMessage(e.target.value);
  16. };
  17. return (
  18. <>
  19. <input value={message} onChange={handleMessageChange} />
  20. <button onClick={handleSendClick}>Send</button>
  21. </>
  22. );
  23. }

拆分组件优化性能

  1. import { useState } from 'react';
  2. export default function App() {
  3. let [color, setColor] = useState('red');
  4. return (
  5. <div>
  6. <input value={color} onChange={(e) => setColor(e.target.value)} />
  7. <p style={{ color }}>Hello, world!</p>
  8. <ExpensiveTree />
  9. </div>
  10. );
  11. }
  12. function ExpensiveTree() {
  13. let now = performance.now();
  14. while (performance.now() - now < 100) {
  15. // Artificial delay -- do nothing for 100ms
  16. }
  17. return <p>I am a very slow component tree.</p>;
  18. }

我们通过认为的方式引入了一个计算量非常大的组件。为了对组件性能进行优化,我们可以有两种方式来进行优化。

方案一:将状态下放

  1. export default function App() {
  2. return (
  3. <>
  4. <Form />
  5. <ExpensiveTree />
  6. </>
  7. );
  8. }
  9. function Form() {
  10. let [color, setColor] = useState('red');
  11. return (
  12. <>
  13. <input value={color} onChange={(e) => setColor(e.target.value)} />
  14. <p style={{ color }}>Hello, world!</p>
  15. </>
  16. );
  17. }

通过拆分,将计算量大的组件作为单独的组件进行引入,不需要在 color 发生变更的时候进行重新计算。

方案二:将内容上移

若实际组件是下图这样的情况,我们就不能简单通过拆分的方式来进行优化

  1. export default function App() {
  2. let [color, setColor] = useState('red');
  3. return (
  4. <div style={{ color }}>
  5. <input value={color} onChange={(e) => setColor(e.target.value)} />
  6. <p>Hello, world!</p>
  7. <ExpensiveTree />
  8. </div>
  9. );
  10. }

我们可以将<Expensive />组件当作参数来进行传入:

  1. export default function App() {
  2. return (
  3. <ColorPicker>
  4. <p>Hello, world!</p>
  5. <ExpensiveTree />
  6. </ColorPicker>
  7. );
  8. }
  9. function ColorPicker({ children }) {
  10. let [color, setColor] = useState("red");
  11. return (
  12. <div style={{ color }}>
  13. <input value={color} onChange={(e) => setColor(e.target.value)} />
  14. {children}
  15. </div>
  16. );
  17. }

封装自定义网络请求 hooks

先来分析,要完成一个网络请求需要哪些步骤,哪些请求状态。
网络请求发起者、网络请求状态、错误处理

  1. import React, { useState, useEffect } from 'react'
  2. import axios from 'axios'
  3. function useFetch(initUrl, initData) {
  4. const [data, setData] = useState(initData)
  5. const [url, setUrl] = useState(initUrl)
  6. const [isLoading, setIsLoading] = useState(false)
  7. const [isError, setIsError] = useState(false)
  8. useEffect(() => {
  9. const fetchData = async () => {
  10. setIsError(false)
  11. setIsLoading(true)
  12. try {
  13. const result = await axios(url)
  14. setData(result.data)
  15. } catch (error) {
  16. setIsError(true)
  17. }
  18. setIsLoading(true)
  19. }
  20. fetchData()
  21. }, [url])
  22. return [{ data, isLoading, isError }, setUrl]
  23. }

函数 fetchData是作为数据流的一部分,要将其放置于 useEffect内部。
useFetch 的使用:

  1. function App() {
  2. const [query, setQuery] = useState('redux')
  3. const [{ data, isLoading, isError }, doFetch] = useFetch('https://github.com/search?q=react', { hits: [] })
  4. return (
  5. <>
  6. <form onSubmit={evt => {
  7. doFetch(`https://github.com/search?${query}`)
  8. evt.preventDefault()
  9. }}>
  10. <input type="text" value={query} onChange={event => setQuery(event.target.value)}></input>
  11. <button type="submit">Search</button>
  12. </form>
  13. {isError && <div>Something went wrong...</div>}
  14. {isLoading ? (
  15. <div>Loading...</div>
  16. ): (
  17. <ul>
  18. { data.hits.map(item => (
  19. <li key={item.objectID}>
  20. <a href={item.url}>{item.title}</a>
  21. </li>
  22. )) }
  23. </ul>
  24. )}
  25. </>
  26. )
  27. }

输入框 input 用于获取即时输入的 query 值的变化,而 setUrl 用于发起新的请求。将 url 作为 useEffect 的依赖项,仅当 url 发生变化的时候才会重新发起请求。而 isLoadingisError 用于表示发起请求过程中的中间状态。包括加载状态和是否加载成功。

我们可以进一步使用 useReducer 来对不同的状态来进行抽象。

  1. const dataFetchReducer = (state, action) => {
  2. switch (action.type) {
  3. case 'FETCH_INIT':
  4. return { ...state, isLoading: true, isError: false };
  5. case 'FETCH_SUCCESS':
  6. return {
  7. ...state,
  8. isLoading: false,
  9. isError: false,
  10. data: action.payload,
  11. };
  12. case 'FETCH_INIT':
  13. return { ...state, isLoading: false, isError: true };
  14. default:
  15. throw new Error('fetch error')
  16. }
  17. };
  18. function useFetch(initUrl, initData) {
  19. const [url, setUrl] = useState(initUrl)
  20. const [state, dispatch] = useReducer(dataFetchReducer, {
  21. isLoading: false,
  22. isError: false,
  23. data: initData,
  24. });
  25. useEffect(() => {
  26. const fetchData = async () => {
  27. dispatch({ type: 'FETCH_INIT' });
  28. try {
  29. const result = await axios(url);
  30. dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
  31. } catch (error) {
  32. dispatch({ type: 'FETCH_FAILURE' });
  33. }
  34. };
  35. fetchData();
  36. }, [url]);
  37. return [state, setUrl];
  38. }

添加 abort data 的情况

  1. function useFetch(initUrl, initData) {
  2. const [url, setUrl] = useState(initUrl)
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initData,
  7. });
  8. useEffect(() => {
  9. let didCancel = false
  10. const fetchData = async () => {
  11. dispatch({ type: 'FETCH_INIT' })
  12. try {
  13. const result = await axios(url);
  14. if (!didCancel) {
  15. dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
  16. }
  17. } catch (error) {
  18. if (!didCancel) {
  19. dispatch({ type: 'FETCH_FAILURE' })
  20. }
  21. }
  22. };
  23. fetchData()
  24. return () => {
  25. didCancel = true
  26. }
  27. }, [url])
  28. return [state, setUrl]
  29. }

url 再次发生变化的时候,使用 useEffect 清除上一次发起的请求,通过一个 didCancel 标识变量来进行控制。

useEffect

Functions are part of the data flow.

  1. function Example({ someProp }) {
  2. function doSomething() {
  3. console.log(someProp)
  4. }
  5. useEffect(() => {
  6. doSomething()
  7. }, []) // 这是非常不安全的 🔐
  8. }

如上例,在 useEffect 中调用 doSomething 函数,而在其中调用了 someProp。在 effect 的外部是很难去记忆哪个 props 或 state 是被函数调用的。所以需要把在 effect 内部用到的函数要定义到 effect 的内部。这样书写的话很容易看出 effect 依赖了组件作用域中的哪个值。

  1. function Example({ someProp }) {
  2. useEffect(() => {
  3. function doSomething() {
  4. console.log(someProp);
  5. }
  6. doSomething();
  7. }, [someProp]); // ✅ OK (our effect only uses `someProp`)
  8. }

如果我们不依赖组件作用域中的任何值,那么将依赖项留空就是恰当的:

  1. useEffect(() => {
  2. function doSomething() {
  3. console.log('hello');
  4. }
  5. doSomething();
  6. }, []); // ✅ OK in this example because we don't use *any* values from component scope

如果在 useEffect 中设置一列依赖项,那么这些依赖项必须包括所有在回调函数中使用过和参与到 React 数据流的值。包括 propsstate 和任意由其衍生出来的任何东西。

只有当一个函数没有引用任何 propsstate 或衍生值的时候,忽略此依赖才是合理的。

  1. function ProductPage({ productId }) {
  2. const [product, setProduct] = useState(null);
  3. useEffect(() => {
  4. // By moving this function inside the effect, we can clearly see the values it uses.
  5. async function fetchProduct() {
  6. const response = await fetch('http://myapi/product/' + productId);
  7. const json = await response.json();
  8. setProduct(json);
  9. }
  10. fetchProduct();
  11. }, [productId]); // ✅ Valid because our effect only uses productId
  12. // ...
  13. }

若使用了任何数据,则需要在 dependency list 中标明。我们将函数写入了 effect 内部,所以不需要将其写进依赖表中

若无法将函数移入 effect 中

function SearchResults() { useEffect(() => { const url = getFetchUrl(‘react’) }, []) }

  1. 如果一个函数不使用组件作用域中的任何东西,那么你可以将它移出组件,然后就可以在 effets 中进行自由的使用了。不需要将其放置于 deps 中,因为它并不在渲染作用域而且不会被数据流所影响。它不会突然依赖 porps 或者 state.
  2. ```javascript
  3. function ProductPage({ productId }) {
  4. // ✅ Wrap with useCallback to avoid change on every render
  5. const fetchProduct = useCallback(() => {
  6. // ... Does something with productId ...
  7. }, [productId]); // ✅ All useCallback dependencies are specified
  8. return <ProductDetails fetchProduct={fetchProduct} />;
  9. }
  10. function ProductDetails({ fetchProduct }) {
  11. useEffect(() => {
  12. fetchProduct();
  13. }, [fetchProduct]); // ✅ All useEffect dependencies are specified
  14. // ...
  15. }

在上面的例子中,我们需要将函数写在依赖项中。这样可以确保当 productId 发生变化的时候,ProductPage 会自动触发一个在 ProductDetails 中的重取(refetch)操作。

使用 **useCallback** ,函数可以完全参与到整个数据流当中。如果函数输入发生改变,那么函数本身也发生了改变,反之,它保持不变。

useReducer is a cheat mode

dispatch identity is guaranteed to be stable between re-renders.

所以可以将 dispatch 从依赖项中排除,因为它不会导致重渲染。

You may be wondering: how can this possibly work? How can the reducer “know” props when called from inside an effect that belongs to another render? The answer is that when you dispatch, React just remembers the action — but it will call your reducer during the next render. At that point the fresh props will be in scope, and you won’t be inside an effect.

This is why I like to think of useReducer as the “cheat mode” of Hooks. It lets me decouple the update logic from describing what happened. This, in turn, helps me remove unnecessary dependencies from my effects and avoid re-running them more often than necessary.

React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

  • 当你有包含多个子值的复杂状态逻辑的时候,或者下一个状态依赖于上一个状态的值,这时候使用 useReducer 是优于 useState 的。
  • 当触发深更新(deep update)的时候,使用 useReducer 也可以帮助你优化性能,因为你可以将 dispach 向下传导而非 callback

useState, useReduceruseContext 的异同

  • useState: 简单状态
  • useReducer: 复杂状态
  • useContext: 全局状态

useEffect vs useLayoutEffect

两者的区别主要是在于运行时间。
useEffect 异步执行,在渲染被绘制到屏幕上之后才会执行。

  1. 触发渲染(修改状态或者父级重渲染)
  2. React 渲染你的组件(对其进行调用)
  3. 屏幕视觉上发生了更新
  4. useEffect 执行

与之相反的是,useLayout 是同步执行的,在渲染之后和屏幕更新之前

  1. 触发渲染(修改状态或者父级重渲染)
  2. React 渲染你的组件(对其进行调用)
  3. useLayoutEffect 执行,React 等候他执行完
  4. 屏幕视觉上发生更新 ```javascript import React, { useState, useEffect } from “react”; import ReactDOM from “react-dom”; import “./styles.css”;

const BlinkyRender = () => { const [value, setValue] = useState(0);

useEffect(() => { if (value === 0) { setValue(10 + Math.random() * 200); } }, [value]);

console.log(“render”, value);

return (

setValue(0)}>value: {value}
); };

ReactDOM.render( , document.querySelector(“#root”) );

  1. 在上述的例子中执行顺序是这样的:
  2. 1. value 置为 0
  3. 2. 触发组件渲染
  4. 3. 页面显示为 0
  5. 4. useEffect 执行,value 置为 random 值,重新执行1-4步骤
  6. ```javascript
  7. import React, { useState, useEffect } from "react";
  8. import ReactDOM from "react-dom";
  9. import "./styles.css";
  10. const BlinkyRender = () => {
  11. const [value, setValue] = useState(0);
  12. useLayoutEffect(() => {
  13. if (value === 0) {
  14. setValue(10 + Math.random() * 200);
  15. }
  16. }, [value]);
  17. console.log("render", value);
  18. return (
  19. <div onClick={() => setValue(0)}>value: {value}</div>
  20. );
  21. };
  22. ReactDOM.render(
  23. <BlinkyRender />,
  24. document.querySelector("#root")
  25. );

而上面的例子的执行是这样的:

  1. value 置 0
  2. 触发渲染,React 渲染组件
  3. useLayoutEffect 执行,value 置为 random 值
  4. 页面绘制此值

在绝大多数的场景中,使用 useEffect 是正确的选择。如果你的代码导致页面闪烁,然后再换到 useLayoutEffect 。由于 useLayoutEffect 是同步的,所以它存在阻塞页面的可能,会导致性能问题。