Opinionated React: Use Context for Shared State Intro My Pattern for React Context Why Use Cases - Important! Inspiration Example Wrapping Up

2025-06-05

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>
  );
};

Enter fullscreen mode Exit fullscreen mode

让我们反向思考来找到这个解决方案。

首先,让我们开始定义上下文的状态以及我们将要创建的两个上下文。

interface UserState {
  user?: User;
  isLoading: boolean;
}

const UserStateContext = React.createContext<UserState | undefined>(undefined);
const UserDispatchContext = React.createContext<UserDispatch | undefined>(
  undefined
);
Enter fullscreen mode Exit fullscreen mode

我们创建了两个独立的上下文,因为并非所有组件都需要同时访问statedispatch。这样,组件可以只使用它需要的上下文。额外的好处是,如果一个组件只使用,它就不会因为没有使用该上下文而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");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我经常写这样的上下文。在应用启动时,我们希望获取当前登录用户的信息,并将这些数据全局可用。

您想要获取的用户可能由 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

在我的大多数应用程序中,我都使用钩子,因此我们将在这里定义钩子。

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;
};
Enter fullscreen mode Exit fullscreen mode

总结

以下是所有内容:

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;
};

Enter fullscreen mode Exit fullscreen mode

这是我即将发布的系列文章的第五篇。如果你喜欢这篇文章,请点赞并在下方留言。你还有什么想说的吗?

与往常一样,我愿意接受建议。

谢谢阅读。

文章来源:https://dev.to/farazamiruddin/opinionated-react-use-context-for-shared-state-4pmj
PREV
自以为是的 React - 使用状态枚举而不是布尔值简介为什么这是一个坏主意 - 一个例子更好的方法 - 使用枚举总结
NEXT
20+ 面向新人的 API 测试常见问题!(2025)🔥