useContext
如果我们希望在子孙组件中传递数据,我们需要通过 props
一层一层进行传递,其实还提供了更简便的数据传递方式: useContext
。
创建
使用 createContext
创建的 context
是一个包含 Provider
和 Consumer
组件的对象,可以用 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,所以通过 props
将 addToCart
方法传递了下去。这时我们点击 setCount
还是会触发 BigList
和 SingleProduct
的重新渲染。
原因
这是因为 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
的依赖数组设置为空数组,那么无论怎么改变 props
和 state
,addToCart
方法都不会重新创建,这样方法中的 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 };
};