1. prop-drilling
假设在如下的组件树中,B组件和F组件都需要使用count这个React state,传统的解决方案是,在App组件中定义count,然后将count作为组件的props一级一级往下传,直至传到B组件和F组件。
以B组件为例,传递count的代码应该是这样:
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h1>This is App component</h1>
<A count={count} />
</div>
);
}
const A = ({ count }) => {
return <B count={count} />;
};
const B = ({ count }) => {
return <p>{count}</p>;
};
可以看出,组件A其实并不需要count,它接受count作为props的作用仅仅是用于将它传递给子组件B。当组件树层级过多时,不必要的层层传递会造成prop-drilling问题,使代码繁杂、可读性下降。
2. Context API
React提供了Context API解决了上述问题,允许组件树上的任何一个组件读取state。
Context API是一个在整个应用程序内部传递数据的系统,避免了沿着组件树手动传递props的问题。可以说它允许我们向整个应用程序“广播”全局状态。
Context API有三个组成部分,分别是:
- Provider:一个特殊的React组件,允许所有子组件访问,一般放在根组件App中
- value:提供其他组件访问的数据,通常是state和function
- Consumer:所有需要读取Provider提供的value的组件都称为Consumer
一般来说,一个application中往往有一个Provider,多个value和多个Consumer。在上面的例子中,Provider是App组件,value是要传递的值count,Consumer是需要使用count的组件B和F。
3. Context API的re-render功能
当Provider提供的value更新时,注意:所有的Consumer将自动re-render!
也就是说,除了setState之外,value的更新同样会触发组件的重新渲染。
4. 使用Context API
使用Context API仅需三步:
- 使用React提供的createContext方法创建context,此方法返回两个组件Provider和Consumer。
const PostContext = createContext();
- 用Provider组件包裹需要传值的组件(往往是根组件)
function App() {
//需要传递给子组件的数据,包括state和function
const [posts, setPosts] = useState([]);
const [searchQuery, setSearchQuery] = useState("");
function handleAddPost(post) {...}
function handleClearPosts() {...}
return (
//PostContext.Provider组件包裹子组件,并在value中以对象的方式定义要传递的数据
<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery,
setSearchQuery,
}}
>
<section>
...
</section>
</PostContext.Provider>
);
}
也就是说,在这个应用程序任何子组件中,都可以访问App组件提供的value:posts、onAddPost、onClearQuery、 searchQuery、setSearchQuery,而不需要使用prop传递造成代码臃肿。
- 在需要值的组件内部使用useContext钩子接收值(常用对象解构接收)
function Results() {
const { posts } = useContext(PostContext);
return <p>🚀 {posts.length} atomic posts found</p>;
}
5. 自定义组件和钩子
value过多时,可以将step1和step2定义为自定义组件。
useContext(PostContext)也可以定义为自定义钩子。
由于EsLint新规定,禁止从同一模块导出 React 组件和非组件,因此自定义组件和钩子不能放在一个文件夹中定义并导出,但是可以导出Context组件和Provider组件。
//自定义组件PostProvider
import React, { createContext, useContext, useState } from "react";
const PostContext = createContext();
const PostProvider = ({ children }) => {
const [posts, setPosts] = useState();
const [searchQuery, setSearchQuery] = useState("");
const searchedPosts = ...
function handleAddPost(post) {...}
function handleClearPosts() {...}
return (
<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery,
setSearchQuery,
}}
>
{children}
</PostContext.Provider>
);
};
export { PostProvider, PostContext };
//自定义钩子usePosts
import PostContext from "./PostContext";
const usePosts = () => {
const context = useContext(PostContext);
return context;
};
export default usePosts;