发布于 2026-01-06 1 阅读
0

当一个简单的 React 上下文变得难以控制时。TL;DR:让我们从简单的开始。新需求!单一全局真理来源 Revelio 副作用 结论 参考资料

当一个简单的 React 上下文失控时。

TL;DR:

我们先从简单的开始。

新要求!

全球真理的单一来源

Revelio 副作用

结论

参考

TL;DR:

  • 有时你认为的简单有效的解决方案,最终却会变成弗兰肯斯坦式的怪物。
  • useEffect如果你发现自己想在 React 上下文中使用,请三思。
  • 更重要的是,要小心,useEffects这取决于全球状态。
  • Kent C Dodds 对如何设置React Context API有一些不错的想法。
  • useReducer从现在开始,我可能会在我的“应用程序”上下文中默认使用 a 。

我们先从简单的开始。

我的团队启动了一个新的 React 应用,我们想看看使用 React Context API 会是什么感觉,就这么简单 useState。我们还想把每个 context 都当作包含相似数据的“盒子”来处理。

假设我们的应用程序现在需要 2 个上下文:

  • 1 代表“Auth”
  • 1. 时间线(暂且这么称呼)
  const AuthContext = React.createContext();

  const AuthContextProvider = ({ children }) => {
    const [user, setUser] = useState();
    const [isLoggedIn, setIsLoggedIn] = useState();

    const state = { user, isLoggedIn };

    return (
      <AuthContext.Provider value={{ state, setUser, setIsLoggedIn }}>
        {children}
      </AuthContext.Provider>
    );
  };
Enter fullscreen mode Exit fullscreen mode

该对象AuthContext包含与身份验证相关的状态。当用户登录时,`setIsLoggedIn(true)` 和 `setUser({email, username})` 函数都会被调用。这将改变该对象的状态,AuthContext并且这种改变可能会影响整个应用程序。

const TimelineContext = React.createContext();

const TimelineContextProvider = ({ children }) => {
  const [posts, setPosts] = useState([]);
  // For the purposes of this blog, selectedPost will be used to display
  // the "show page"
  const [selectedPost, setSelectedPost] = useState(null);
  // And let's imagine we want to do the same thing for a comment.
  const [selectedComment, setSelectedComment] = useState(null);

  const state = { posts, selectedPost, selectedComment };

  return (
    <TimelineContext.Provider
      value={{ state, setPosts, setSelectedPost, setSelectedComment }}
    >
      {children}
    </TimelineContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

TimelineContext将维护我们时间线的状态,包括一个列表posts、一个selectedPost、一个selectedComment

这些都很简单,对吧?

这里立即出现的一个问题是每个上下文的返回值。目前我们可以看到,随着新状态的添加,返回值增长得非常快。

让我们继续解决这个问题TimelineContext

  const TimelineContextProvider = ({ children }) => {
    const [posts, setPosts] = useState([]);
    const [selectedPost, setSelectedPost] = useState(null)
    const [selectedComment, setSelectedComment] = useState(null)

    const state = { posts, selectedPost, selectedComment };
    const actions = { setPosts, setSelectedPost, setSelectedComment }

    return (
      <TimelineContext.Provider value={{ state, actions}}>
        {children}
      </TimelineContext.Provider>
    );
  };
Enter fullscreen mode Exit fullscreen mode

好的,这有点帮助。我们已经将返回对象限制为state& actions

真棒!

另一个令人头疼的问题是,如果这个上下文规模不断扩大,useStates管理起来就会越来越困难。这就是我们引入多个上下文的原因。这样我们就能清晰地划分不同的关注点。

新要求!

现在我们希望在应用程序中设置选定的帖子和评论。如果评论依赖于帖子,那么selectedComment当选择新帖子时,我们也需要将其置空。

这很简单。我们只要加上一个useEffect,就搞定了。

  const TimelineContextProvider = ({ children }) => {
    const [posts, setPosts] = useState([]);
    const [selectedPost, setSelectedPost] = useState(null)
    const [selectedComment, setSelectedComment] = useState(null)

    const state = { posts, selectedPost, selectedComment };
    const actions = { setPosts, setSelectedPost, setSelectedComment }

    useEffect(() => {
      setSelectedComment(null)
    }, [selectedPost])

    return (
      <TimelineContext.Provider value={{ state, actions}}>
        {children}
      </TimelineContext.Provider>
    );
  };
Enter fullscreen mode Exit fullscreen mode

更多修改!!!

现在假设出于测试目的,我们想要添加初始{SelectedPost 和 SelectedComment}。这很简单,对吗?

按照我们目前的设置,它useEffect会在第一次渲染时设置我们的initialSelectedCommentnull。哦哦哦,没有副作用!!!

因此,我们的语境就变成了:

const TimelineContextProvider = ({
  initialSelectedPost,
  initialSelectedComment,
  children
}) => {
  const [posts, setPosts] = useState([]);
  const [selectedPost, setSelectedPost] = useState(initialSelectedPost);
  const [selectedComment, setSelectedComment] = useState(
    initialSelectedComment
  );

  const state = { posts, selectedPost, selectedComment };
  const actions = { setPosts, setSelectedPost, setSelectedComment };

  useEffect(() => {
    if (initialSelectedPost != initialSelectedComment) {
      setSelectedComment(null);
    }
  }, [selectedPost]);

  return (
    <TimelineContext.Provider value={{ state, actions }}>
      {children}
    </TimelineContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

这或许不是什么大问题,但它会促使我们思考仅仅改变现状就可能产生的任何后果。

全球真理的单一来源

团队一直抱怨“组件中到底应该使用哪个 use{X}Context?”。由于 `use{X}Context`AuthContext和`use{X}Context` 都TimelineContext属于全局状态,因此一个解决方案是将它们合并,并在状态对象内部区分不同的域。我们先来解决这个问题。

const AppContextProvider = ({
  initialSelectedPost,
  initialSelectedComment,
  children
}) => {
  const [user, setUser] = useState();
  const [isLoggedIn, setIsLoggedIn] = useState();
  const [posts, setPosts] = useState([]);
  const [selectedPost, setSelectedPost] = useState(initialSelectedPost);
  const [selectedComment, setSelectedComment] = useState(
    initialSelectedComment
  );

  const state = {
    auth: { user, isLoggedIn },
    timeline: { posts, selectedPost, selectedComment }
  };

  const actions = {
    setUser,
    setIsLoggedIn,
    setPosts,
    setSelectedPost,
    setSelectedComment
  };

  useEffect(() => {
    if (initialSelectedPost != initialSelectedComment) {
      setSelectedComment(null);
    }
  }, [selectedPost]);

  return (
    <AppContext.Provider value={{ state, actions }}>
      {children}
    </AppContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

在我看来,这算不上什么巨大的胜利,但现在球队更开心了。

Revelio 副作用

使用 React Hooks 一年后,我得出的结论是,useEffect在上下文中使用它可能不是个好主意。(顺便说一句,我很想看看你们是如何成功运用这种方法的。)

我最终确定的一条更具体的规则是:我们useEffect的应用中不应该存在依赖全局状态的回调函数。我觉得这就像一把锋利的刀,很容易伤到眼睛。对于那些并非每天都从事前端开发的人来说,这无疑增加了项目开发的门槛。即使是负责代码库的开发人员,也必须时刻牢记这一点:“如果我修改了 {X},这个回调函数会运行吗?我需要修改它吗?”

我的解决方案是始终(大概 95% 的时间)使用useReducer全局状态,并且永远不要依赖useEffect全局状态的任何部分。

我们走吧!

初始状态

首先,我们将从应用程序的初始状态开始。

const initialState = {
  auth: { user: null, isLoggedIn: false },
  timeline: { posts: [], selectedPost: null, selectedComment: null }
};
Enter fullscreen mode Exit fullscreen mode

嗯,这很简单!定义初始状态后,我们就能一目了然地看到所有全局状态。任何时候我们想向全局状态添加内容,都可以先给对象添加一个合理的默认值initialState。例如,isLoggedIn初始值为 false,posts初始值为空数组。

减少,我亲爱的华生

我最喜欢 reducer 模式的一点是,你可以把 reducer 中的每个 action 都看作是与应用的一次交互。这些交互可以是网络请求,也可以是用户事件。设置一个 action 时,我会问自己“当 {X} 发生时,状态会发生什么变化”。然后,你只需分发带有正确 payload 的 action,就大功告成了!现在,如果同一个交互发生在两个地方,你无需打开另一个组件并记住逻辑;只需分发 action 即可。

auth我们所讨论的情况而言,我们有两种交互方式:登录和注销。

我们来看一下这段代码。

const ActionTypes = {
  SET_USER: "set-user",
  LOGOUT_USER: "logout-user",
}
const reducer = (state, action) => {
  switch (action.type) {
    case ActionTypes.SET_USER: {
      return {
        ...state,
        auth: { ...state.auth, user: action.payload, isLoggedIn: true }
      };
    }
    case ActionTypes.LOGOUT_USER: {
      return {
        ...state,
        auth: { ...state.auth, user: null, isLoggedIn: false }
      };
    }
    ...
  }
};
Enter fullscreen mode Exit fullscreen mode

哇,这才是KISS精神!:D

现在我们不必记住调用 `call`setUsersetIsLoggedIn`dispatch`,只需针对给定的交互分发相应的操作即可。

接下来,我们为该timeline状态添加操作。

const ActionTypes = {
  ...,
  ADD_POSTS: "add-posts",
  SELECT_POST: "select-post",
  SELECT_COMMENT: "select-comment"
};

const reducer = (state, action) => {
  switch (action.type) {
    ...,
    case ActionTypes.ADD_POSTS: {
      return {
        ...state,
        timeline: {
          ...state.timeline,
          posts: [...state.timeline.posts, ...action.payload]
        }
      };
    }
    case ActionTypes.SELECT_POST: {
      return {
        ...state,
        timeline: {
          ...state.timeline,
          selectedPost: action.payload,
          selectedComment: null
        }
      };
    }
    case ActionTypes.SELECT_COMMENT: {
      return {
        ...state,
        timeline: {
          ...state.timeline,
          selectedComment: action.payload
        }
      };
    }
    ...,
  }
};
Enter fullscreen mode Exit fullscreen mode

你可能没注意到,但这个SELECT_POST操作解决了 useEffect 的副作用问题!如果你还记得,useEffect在最初的上下文中,我们有一个 `&` 会在selectedComment条件改变时使 `&` 失效selectedPost。现在,我们可以设置一个 ` initialSelectedPost&`initialSelectedComment而不用担心useEffect触发 `&`;这样就无需if仅仅为了测试而维护一个状态了。

新背景

最后一步是通过 React Context 将我们新的 reducer 提供给我们的应用程序。

const AppProvider = ({ initialState, reducer, children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

这样简洁多了。我的团队使用Rails单体架构,所以我决定为 `<React>`initialState标签添加 ` reducer<React>` 属性AppProvider。这种方法允许我们为创建的任何 React 应用使用同一个 `<React>` 提供程序。

结论

目前,这是我最喜欢的在 React 应用中管理全局状态的方法(稍后我会写博客介绍一些额外的技巧)。

  • 无需添加任何依赖项。
  • 对全局状态没有需要记忆的副作用。
  • 每次迭代都映射到一个单独的封装动作。

把所有内容整合起来。

const initialState = {
  auth: { user: null, isLoggedIn: false },
  timeline: { posts: [], selectedPost: null, selectedComment: null }
};

const ActionTypes = {
  SET_USER: "set-user",
  LOGOUT_USER: "logout-user",
  ADD_POSTS: "add-posts",
  SELECT_POST: "select-post",
  SELECT_COMMENT: "select-comment"
};

const reducer = (state, action) => {
  switch (action.type) {
    case ActionTypes.SET_USER: {
      return {
        ...state,
        auth: { ...state.auth, user: action.payload, isLoggedIn: true }
      };
    }
    case ActionTypes.LOGOUT_USER: {
      return {
        ...state,
        auth: { ...state.auth, user: null, isLoggedIn: false }
      };
    }
    case ActionTypes.ADD_POSTS: {
      return {
        ...state,
        timeline: {
          ...state.timeline,
          posts: [...state.timeline.posts, ...action.payload]
        }
      };
    }
    case ActionTypes.SELECT_POST: {
      return {
        ...state,
        timeline: {
          ...state.timeline,
          selectedPost: action.payload,
          selectedComment: null
        }
      };
    }
    case ActionTypes.SELECT_COMMENT: {
      return {
        ...state,
        timeline: {
          ...state.timeline,
          selectedComment: action.payload
        }
      };
    }
    default:
      return state;
  }
};

const AppProvider = ({ initialState, reducer, children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

你可以在推特账号@basicbrogrammer上找到我随笔的技术杂谈。

参考

特别推荐 Kent Dodds。他的博客上有一些非常棒的 React 模式。快去看看吧。

userReducerReact 的文档

文章来源:https://dev.to/basicbrogrammer/when-a-simple-react-context-gets-out-of-hand-28k