React 的自以为是:使用 Context 实现共享状态
简介
我的 React Context 模式
为什么
用例——重要!
灵感
例子
总结
简介
我使用 React 已经四年多了。在此期间,我对应用程序应该是什么样子形成了自己的看法。这是系列文章的第五部分。
我的 React Context 模式
我的朋友Nader问我如何在我的应用中使用 React Context。我答应过我会写一篇相关文章,所以就写到这里了。
为什么
你的应用程序中有一些实例的状态会被多个组件使用。如果这些共享状态需要大量的 prop 钻取,我会使用 context。过去,Redux 是避免 prop 钻取的流行解决方案。然而,我认为 Redux 已经不再需要了。React 的 context API 非常适合这种情况。
用例——重要!
-
你应该使用 React 上下文来保存全局状态。话虽如此,全局状态其实并不多。一些很好的例子是当前用户、当前语言设置或功能开关映射。
-
您不需要仅将上下文用于全局状态。上下文可以应用于应用程序的特定子树。
-
拥有多个子树特定的上下文是很常见的。
灵感
我最初是从Kent C. Dodd 的精彩文章《如何有效使用 React Context》中了解到这种模式的,推荐阅读。Tanner Linsley 在他的演讲《React 中的自定义 Hooks:终极 UI 抽象层》中也介绍了类似的概念。
例子
最终目标是拥有一个看起来像这样的 API。
export const App = ({ userId }) => {
return (
<UserProvider id={userId}>
<Dashboard />
</UserProvider>
);
};
const Dashboard = () => {
const { isLoading, user } = useUserState();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<div>Hello {user.displayName}!</div>
</div>
);
};
让我们反向思考来找到这个解决方案。
首先,让我们开始定义上下文的状态以及我们将要创建的两个上下文。
interface UserState {
user?: User;
isLoading: boolean;
}
const UserStateContext = React.createContext<UserState | undefined>(undefined);
const UserDispatchContext = React.createContext<UserDispatch | undefined>(
undefined
);
我们创建了两个独立的上下文,因为并非所有组件都需要同时访问state
和dispatch
。这样,组件可以只使用它需要的上下文。额外的好处是,如果一个组件只使用,它就不会因为没有使用该上下文而dispatch
因更改而重新渲染。state
对于上下文中的状态管理,我们将使用useReducer
。
// omitted rest of the file
enum UserActionTypes {
LOADING = "loading",
SUCCESS = "success"
}
type UserAction =
| { type: UserActionTypes.LOADING }
| { type: UserActionTypes.SUCCESS; payload: User };
type UserDispatch = (action: UserAction) => void;
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case UserActionTypes.LOADING: {
return { isLoading: true };
}
case UserActionTypes.SUCCESS: {
return { isLoading: false, user: action.payload };
}
default: {
throw new Error("Invalid action type");
}
}
}
我经常写这样的上下文。在应用启动时,我们希望获取当前登录用户的信息,并将这些数据全局可用。
您想要获取的用户可能由 id 决定,并且由于 Provider 组件可以接受 props,我们可以简单地传入一个,id
这样当我们的 Context 安装时,我们就可以获取用户。
提供者组件如下所示。
export const UserProvider: React.FC<{ id: string }> = ({ id, children }) => {
const [state, dispatch] = React.useReducer(userReducer, { isLoading: true });
React.useEffect(() => {
const handleGetUser = async id => {
dispatch({ type: UserActionTypes.LOADING });
const user = await getUserById(id);
dispatch({ type: UserActionTypes.SUCCESS, payload: user });
return;
};
handleGetUser(id);
return;
}, [id]);
return (
<UserStateContext.Provider value={state}>
<UserDispatchContext.Provider value={dispatch}>
{children}
</UserDispatchContext.Provider>
</UserStateContext.Provider>
);
};
在我的大多数应用程序中,我都使用钩子,因此我们将在这里定义钩子。
export const useUserState = () => {
const userStateContext = React.useContext(UserStateContext);
if (userStateContext === undefined) {
throw new Error("useUserState must be used within a UserProvider");
}
return userStateContext;
};
export const useUserDispatch = () => {
const userDispatchContext = React.useContext(UserDispatchContext);
if (userDispatchContext === undefined) {
throw new Error("useUserDispatch must be used within a UserProvider");
}
return userDispatchContext;
};
总结
以下是所有内容:
import * as React from "react";
import { getUserById } from "../services/user-service";
import { User } from "../types/user";
interface UserState {
user?: User;
isLoading: boolean;
}
enum UserActionTypes {
LOADING = "loading",
SUCCESS = "success"
}
type UserAction =
| { type: UserActionTypes.LOADING }
| { type: UserActionTypes.SUCCESS; payload: User };
type UserDispatch = (action: UserAction) => void;
const UserStateContext = React.createContext<UserState | undefined>(undefined);
const UserDispatchContext = React.createContext<UserDispatch | undefined>(
undefined
);
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case UserActionTypes.LOADING: {
return { isLoading: true };
}
case UserActionTypes.SUCCESS: {
return { isLoading: false, user: action.payload };
}
default: {
throw new Error("Invalid action type");
}
}
}
export const UserProvider: React.FC<{ id: string }> = ({ id, children }) => {
const [state, dispatch] = React.useReducer(userReducer, { isLoading: true });
React.useEffect(() => {
const handleGetUser = async id => {
dispatch({ type: UserActionTypes.LOADING });
const user = await getUserById(id);
dispatch({ type: UserActionTypes.SUCCESS, payload: user });
return;
};
handleGetUser(id);
return;
}, [id]);
return (
<UserStateContext.Provider value={state}>
<UserDispatchContext.Provider value={dispatch}>
{children}
</UserDispatchContext.Provider>
</UserStateContext.Provider>
);
};
export const useUserState = () => {
const userStateContext = React.useContext(UserStateContext);
if (userStateContext === undefined) {
throw new Error("useUserState must be used within a UserProvider");
}
return userStateContext;
};
export const useUserDispatch = () => {
const userDispatchContext = React.useContext(UserDispatchContext);
if (userDispatchContext === undefined) {
throw new Error("useUserDispatch must be used within a UserProvider");
}
return userDispatchContext;
};
这是我即将发布的系列文章的第五篇。如果你喜欢这篇文章,请点赞并在下方留言。你还有什么想说的吗?
与往常一样,我愿意接受建议。
谢谢阅读。
文章来源:https://dev.to/farazamiruddin/opinionated-react-use-context-for-shared-state-4pmj