当一个简单的 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>
);
};
该对象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>
);
};
这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>
);
};
好的,这有点帮助。我们已经将返回对象限制为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>
);
};
更多修改!!!
现在假设出于测试目的,我们想要添加初始{SelectedPost 和 SelectedComment}。这很简单,对吗?
按照我们目前的设置,它useEffect会在第一次渲染时设置我们的initialSelectedComment值null。哦哦哦,没有副作用!!!
因此,我们的语境就变成了:
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>
);
};
这或许不是什么大问题,但它会促使我们思考仅仅改变现状就可能产生的任何后果。
全球真理的单一来源
团队一直抱怨“组件中到底应该使用哪个 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>
);
};
在我看来,这算不上什么巨大的胜利,但现在球队更开心了。
Revelio 副作用
使用 React Hooks 一年后,我得出的结论是,useEffect在上下文中使用它可能不是个好主意。(顺便说一句,我很想看看你们是如何成功运用这种方法的。)
我最终确定的一条更具体的规则是:我们useEffect的应用中不应该存在依赖全局状态的回调函数。我觉得这就像一把锋利的刀,很容易伤到眼睛。对于那些并非每天都从事前端开发的人来说,这无疑增加了项目开发的门槛。即使是负责代码库的开发人员,也必须时刻牢记这一点:“如果我修改了 {X},这个回调函数会运行吗?我需要修改它吗?”
我的解决方案是始终(大概 95% 的时间)使用useReducer全局状态,并且永远不要依赖useEffect全局状态的任何部分。
我们走吧!
初始状态
首先,我们将从应用程序的初始状态开始。
const initialState = {
auth: { user: null, isLoggedIn: false },
timeline: { posts: [], selectedPost: null, selectedComment: null }
};
嗯,这很简单!定义初始状态后,我们就能一目了然地看到所有全局状态。任何时候我们想向全局状态添加内容,都可以先给对象添加一个合理的默认值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 }
};
}
...
}
};
哇,这才是KISS精神!:D
现在我们不必记住调用 `call`setUser和setIsLoggedIn`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
}
};
}
...,
}
};
你可能没注意到,但这个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>
);
};
这样简洁多了。我的团队使用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>
);
};
你可以在推特账号@basicbrogrammer上找到我随笔的技术杂谈。
参考
特别推荐 Kent Dodds。他的博客上有一些非常棒的 React 模式。快去看看吧。
文章来源:https://dev.to/basicbrogrammer/when-a-simple-react-context-gets-out-of-hand-28k
