react笔记03-hooks2

22 阅读4分钟

useContext

如果我们希望在子孙组件中传递数据,我们需要通过 props 一层一层进行传递,其实还提供了更简便的数据传递方式: useContext

创建

使用 createContext 创建的 context 是一个包含 ProviderConsumer 组件的对象,可以用 Provider 包裹最外层组件,这样可以将 value 传递到内部的每一个组件中。

const PersonContext = React.createContext();
<PersonContext.Provider value={{ removePerson, person }}>
  ...
</PersonContext.Provider>

使用

可以用 Consumer 组件接收 value,但在 hooks 中,更常用 useContext 来接收某个 context 传递下来的数据。

const { removePerson } = useContext(PersonContext);

自定义 hooks

一般自定义 hooks 都以 use 开头,重复使用的代码我们就可以想着用自定义 hooks 处理。

React.memo

每次 props 或者 state 改变时,组件会重新渲染。

问题

以下面的代码为例,每次我点击按钮 setCount 的时候,由于改变了 count,所以 Index 组件会重新渲染,同时其子组件 BigList 也会重新渲染。但是 BigList 其实和 count 无关,我们不希望改变 count 的时候,BigList 也重新渲染,这时我们就可以使用 React.memo 这个方法。

// every time props or state changes, component re-renders
const Index = () => {
  const { products } = useFetch(url)
  const [count, setCount] = useState(0)

  return (
    <>
      <h1>Count : {count}</h1>
      <button onClick={() => setCount(count + 1)}>click me </button>
      <BigList products={products} />
    </>
  )
}

const BigList = React.memo(({ products }) => {
  return (...)
})

解决方法

当我们使用 React.memo 包裹组件的时候,当父级组件重新渲染时,会检查传入的 products 这个 props 有没有改变,没改变就不会触发 BigList 的重新渲染。

useCallback

问题

products 是一个数组,每个元素都有一个 id 和一个 fields 对象,fields 对象中包含这个产品的信息。我们希望点击每个产品,都在购物车上加1,所以通过 propsaddToCart 方法传递了下去。这时我们点击 setCount 还是会触发 BigListSingleProduct 的重新渲染。

原因

这是因为 setCount 改变了 count 这个 state 的值,所以触发了 Index 组件的重新渲染,也就会重新创建 addToCart 方法,而这个方法是作为 props 传递给下级组件的,所以即使下级组件使用了 memo 也会重新渲染。

const Index = () => {
  const { products } = useFetch(url)
  const [count, setCount] = useState(0)
  const [cart, setCart] = useState(0)

  const addToCart = useCallback(() => {
    setCart(cart + 1)
  }, [cart])

  return (
    <>
      <h1>Count : {count}</h1>
      <button onClick={() => setCount(count + 1)}> click me </button>
      <h1>cart : {cart}</h1>
      <BigList products={products} addToCart={addToCart} />
    </>
  )
}

const BigList = React.memo(({ products, addToCart }) => {
  return (
    <section>
      {products.map((product) => {
        return (
          <SingleProduct key={product.id} {...product} addToCart={addToCart}></SingleProduct>
        )
      })}
    </section>
  )
})

const SingleProduct = ({ fields, addToCart }) => {
  return (
    <article>
      ...
      <button onClick={addToCart}>add to cart</button>
    </article>
  )
}

解决方法

addToCart 方法使用 useCallback 这个 hook 包裹起来,并设置依赖值:cart。这样当 count 变化时,就不会重新创建 addToCart 方法。只有当 cart 这个依赖值产生变化,也就是每次点击加入购物车时,才会重新创建 addToCart 方法,并将其中的 cart 设置为最新的值。

依赖数组

如果将 useCallback 的依赖数组设置为空数组,那么无论怎么改变 propsstateaddToCart 方法都不会重新创建,这样方法中的 cart 值就一直是初始值0,每次加入购物车,cart 的值一直为1。

useMemo

问题

当我们希望在一个页面展示列表中最高价的时候,如果像注释掉的一行那样,直接调用函数,那么当我们点击改变 count 的时候,都会重新执行 calculateMostExpensive 计算最高价。

const calculateMostExpensive = (data) => {
  return (
    data.reduce((total, item) => {
      const price = item.fields.price
      if (price >= total) total = price
      return total
    }, 0) / 100
  )
}
const Index = () => {
  const { products } = useFetch(url)
  const [count, setCount] = useState(0)

  const mostExpensive = useMemo(() => calculateMostExpensive(products), [ products ])
  
  return (
    <>
      <h1>Count : {count}</h1>
      <button onClick={() => setCount(count + 1)}> click me </button>
      
      {/* <h1>Most Expensive : ${calculateMostExpensive(products)}</h1> */}
      <h1>Most Expensive : ${mostExpensive}</h1>
    </>
  )
}

解决方法

使用 useMemo 这个 hook 传递进 calculateMostExpensive 方法,并设置依赖数组,这样当 count 改变时,由于 products 没有改变,不会再重新计算。

补充 useFetch

useFetch 其实是一个自定义 hook,它对于请求做了一些封装,来看看它现在的写法:

export const useFetch = (url) => {
  const [loading, setLoading] = useState(true);
  const [products, setProducts] = useState([]);

  const getProducts = async () => {
    const response = await fetch(url);
    const products = await response.json();
    setProducts(products);
    setLoading(false);
  };

  useEffect(() => {
    getProducts();
  }, [url]);

  return { loading, products };
};

问题1

当你这样写的时候,你会在终端看到这样一个报错:

React Hook useEffect has a missing dependency: 'getProducts'. Either include it or remove the dependency

这个报错是因为当我们在 useEffect 中添加了 getProducts 这个外部变量,却没有在依赖数组中加上它,这是 ESlint 给我们增加的校验。

问题2

如果我们直接在 useEffect 的依赖数组中加上 getProducts,前一个问题可以解决,但是又会产生一个新的问题:

The 'getProducts' function makes the dependencies of useEffect Hook (at line 27) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'getProducts' definition into its own useCallback() Hook

这是因为在 useEffect 中添加了 getProducts 依赖后会造成无限循环,当页面渲染的时候,触发 useEffect 从而执行 getProducts 方法,在 getProducts 方法中使用 setProducts 改变了 state 值,从而触发组件重新渲染,重新生成 getProducts 方法useEffect 监听到依赖数组变化,执行 setProducts 方法 。。。

解决

我们可以通过 useCallback 包裹 getProducts 方法,从而避免重新渲染的时候重新生成 getProducts 方法。

export const useFetch = (url) => {
  const [loading, setLoading] = useState(true);
  const [products, setProducts] = useState([]);

  const getProducts = useCallback(async () => {
    const response = await fetch(url);
    const products = await response.json();
    setProducts(products);
    setLoading(false);
  }, [url]);

  useEffect(() => {
    getProducts();
  }, [url, getProducts]);

  return { loading, products };
};