你不知道的React系列(十八)useMemo(掌握)

93 阅读3分钟

本文正在参加「金石计划」

const cachedValue = useMemo(calculateValue, dependencies)
  • 在重新渲染之间(组件里面)缓存计算结果

  • calculateValue 纯函数,没有参数,返回你想缓存的数据

  • 初始渲染调用,dependencies 没有改变下次渲染会返回缓存的数据, 改变了重新执行 calculateValue 返回数据

  • 首次渲染是不会缓存数据的

使用

除了使用 useMemo,一些其他建议

  • 组件作为 children 传递时

  • 减少 state 使用和提升

  • 渲染逻辑保持纯净

  • 避免在 Effect 中更新 state

  • 尽量减少 Effect dependencies

子组件跳过重新渲染

  • 父组件更新一些 state,子组件没有用到这些 state

    export default function TodoList({ todos, tab, theme }) {
      // ...
      return (
        <div className={theme}>
          <List items={visibleTodos} />
        </div>
      );
    }
    
    import { memo } from 'react';
    
    const List = memo(function List({ items }) {
      // ...
    });
    
  • 子组件 props 没有改变,不会重新渲染

    export default function TodoList({ todos, tab, theme }) {
      // Every time the theme changes, this will be a different array...
      const visibleTodos = filterTodos(todos, tab);
      return (
        <div className={theme}>
          {/* ... so List's props will never be the same, and it will re-render every time */}
          <List items={visibleTodos} />
        </div>
      );
    }
    
    export default function TodoList({ todos, tab, theme }) {
      // Tell React to cache your calculation between re-renders...
      const visibleTodos = useMemo(
        () => filterTodos(todos, tab),
        [todos, tab] // ...so as long as these dependencies don't change...
      );
      return (
        <div className={theme}>
          {/* ...List will receive the same props and can skip re-rendering */}
          <List items={visibleTodos} />
        </div>
      );
    }
    

缓存依赖组件里面定义的对象

useMemo 依赖组件内部一个对象时,也要把这个对象缓存或者把这个对象放在同一个 useMemo 里面

```JavaScript
function Dropdown({ allItems, text }) {
  const searchOptions = { matchMode: 'whole-word', text };

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
  // ...

function Dropdown({ allItems, text }) {
  const searchOptions = useMemo(() => {
    return { matchMode: 'whole-word', text };
  }, [text]); // ✅ Only changes when text changes

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
  // ...

function Dropdown({ allItems, text }) {
  const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
  }, [allItems, text]); // ✅ Only changes when allItems or text changes
  // ...
```

缓存函数

  • useMemo 记忆一个函数,算函数必须返回另一个函数

    export default function Page({ productId, referrer }) {
      const handleSubmit = useMemo(() => {
        return (orderDetails) => {
          post('/product/' + productId + '/buy', {
            referrer,
            orderDetails
          });
        };
      }, [productId, referrer]);
    
      return <Form onSubmit={handleSubmit} />;
    }
    
  • 使用 useCallback

    export default function Page({ productId, referrer }) {
      const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails
        });
      }, [productId, referrer]);
    
      return <Form onSubmit={handleSubmit} />;
    }
    

问答

  • 每次渲染 calculation 运行两次

    calculation 不是纯函数

  • useMemo 应该返回对象,但是返回 undefined

    calculation 写错了

  • 每次渲染组件 calculation 都会执行

    没有指定 dependencies,或者 dependencies 返回了一个新的数据

  • 循环中使用 useMemo

    提取循环内容为一个组件,在组件内部使用 useMemo, 可以缓存某个数据或者整个组件

    function ReportList({ items }) {
      return (
        <article>
          {items.map(item => {
            // 🔴 You can't call useMemo in a loop like this:
            const data = useMemo(() => calculateReport(item), [item]);
            return (
              <figure key={item.id}>
                <Chart data={data} />
              </figure>
            );
          })}
        </article>
      );
    }
    
    function ReportList({ items }) {
      return (
        <article>
          {items.map(item =>
            <Report key={item.id} item={item} />
          )}
        </article>
      );
    }
    
    function Report({ item }) {
      // ✅ Call useMemo at the top level:
      const data = useMemo(() => calculateReport(item), [item]);
      return (
        <figure>
          <Chart data={data} />
        </figure>
      );
    }
    
    function ReportList({ items }) {
      // ...
    }
    
    const Report = memo(function Report({ item }) {
      const data = calculateReport(item);
      return (
        <figure>
          <Chart data={data} />
        </figure>
      );
    });
    

Memo

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
  • props 没有改变跳过重新渲染
  • 组件内部 state 改变还是会重新渲染
  • context 改变还是会重新渲染
// name 没有变化,Greeting 就不会渲染
// greeting 变化,Greeting 还是重新渲染
// theme 变化,Greeting 还是重新渲染

import { memo, createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);

export default function MyApp() {
  const [name, setName] = useState("");
  const [address, setAddress] = useState("");
  const [theme, setTheme] = useState('dark');
  function handleClick() {
    setTheme(theme === 'dark' ? 'light' : 'dark'); 
  }
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={handleClick}>Switch theme</button>
      <label>
        Name{": "}
        <input value={name} onChange={(e) => setName(e.target.value)} />
      </label>
      <label>
        Address{": "}
        <input value={address} onChange={(e) => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </ThemeContext.Provider>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  const theme = useContext(ThemeContext);
  const [greeting, setGreeting] = useState("Hello");
  return (
    <>
      <h3 className={theme}>
        {greeting}
        {name && ", "}
        {name}!
      </h3>
      <GreetingSelector value={greeting} onChange={setGreeting} />
    </>
  );
});

function GreetingSelector({ value, onChange }) {
  return (
    <>
      <label>
        <input
          type="radio"
          checked={value === "Hello"}
          onChange={(e) => onChange("Hello")}
        />
        Regular greeting
      </label>
      <label>
        <input
          type="radio"
          checked={value === "Hello and welcome"}
          onChange={(e) => onChange("Hello and welcome")}
        />
        Enthusiastic greeting
      </label>
    </>
  );
}
  • 最小化 props 改变

    • props 是对象父组件传递的时候使用 useMemo 包裹
    • 传递最小化需要的数据
    • 使用能够推导出来的 props 而不是整个对象传递
    • 传递函数把它放在组件外部或者使用 useCallback
  • 自定义比较函数方法

    影响性能而且并不一定正确

    const Chart = memo(function Chart({ dataPoints }) {
      // ...
    }, arePropsEqual);
    
    function arePropsEqual(oldProps, newProps) {
      return (
        oldProps.dataPoints.length === newProps.dataPoints.length &&
        oldProps.dataPoints.every((oldPoint, index) => {
          const newPoint = newProps.dataPoints[index];
          return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
        })
      );
    }