React-Redux 与 Context 之个人浅见
引子
最近面试被问到了react-redux跟Context之间的区别,答:
它们两者都解决了跨层级组件间数据共享的问题; react-redux 比较适合在 大型项目中使用, context 比较适合在小型项目中使用;
面试官:...
通过面试,我发现确实有些问题想的不够深入,听别人一说,觉得有道理,然后认为就是这样了,没有深究其深层次原因;或者工作中遇到了,直接觉得这个方案不行,立马换一个方案,没有及时记录;
发现了问题,就来解决它,于是我查了一些资料,整理了一下,谈一下当下自己的理解,后面会随着技术广度提升后,再来更新修饰,如果有大佬有更好的解释,也希望能够在评论区留下宝贵见解;
历史背景
状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去
总体就是一个组件内的State会影响其他组件的State,那么我们就应该将它提升到这些组件的父组件中去统一管理;
具体可以查看React官网的例子:
后面遇到了再补充一个例子,现实中这种场景应该还是不少的。
属性跨层传递;
前面说了状态提升的场景,这种方式在某些场景下会导致一定的问题的,比如下图:
比如ComponentA 与 ComponentB 之间需要沟通,那么就需要将数据与方法提升到SubRoot中;如果ComponentB出现了某个问题,我们要往上查找多层,如果用的js, 并且属性传递的时候是用拓展运算符(好处是后面底层组件中添加从顶层组件获取的某个属性,可以不动中间组件,坏处是如果底层组件看它的某个属性哪来的不是很方便)传递下去,那么维护这段代码的人观感可能不太好(如果不是他写的😀)。
还有一种情况是单纯的顶层组件保存着数据,某个底层组件需要使用该数据,如下图App 组件中有用户信息,Message组件使用用户信息(显示欢迎语);
这种方式容易污染中间组件,使中间组件的复用困难,因为它不干净了,承担了数据传递的职责。
问题总结;
- 状态方法都提升到父级组件,容易使父级组件逻辑臃肿;
- 一个组件更新,会导致父组件更新,进而导致所有组件更新(虽然可以通过一些方式打断,但是这些中间组件本可以不做);
- 组件重用困难,处于中间层的组件,有了一些它并不需要的属性;
Context 与 React-Redux的 使用方式
知道了问题,看下react-redux 和 Context 的解决方式:
它们都可以跨组件直传属性;分别看一下它们的使用方式:
Context使用
为了方便,写在了一个文件里:
const UserContext = createContext();
export const App = () => {
const [user, setUser] = useState({});
useEffect(() => {
setTimeout(() => {
setUser({ username: "Bob", userId: 0 });
}, 500);
}, []);
console.log("app render");
return (
<div>
<UserContext.Provider value={{ user, setUser }}>
{useMemo(
() => (
<>
<NavBar />
<MainPage />
</>
),
[]
)}
</UserContext.Provider>
</div>
);
};
const NavBar = () => {
const { setUser } = useContext(UserContext);
console.log("nav-bar:render");
return (
<div>
this is NavBar!!{" "}
<button
onClick={() => {
setUser((user) => ({ ...user, username: "Tom" }));
}}
>
修改用户名
</button>
</div>
);
};
const MainPage = () => {
console.log("main-page:render");
return (
<div>
<Content />
</div>
);
};
const Content = () => {
console.log("content:render");
return (
<div>
this is Content
<div>
<Message />
</div>
</div>
);
};
const Message = () => {
console.log("message:render");
const { user } = useContext(UserContext);
return <div>hello {user.username}</div>;
};
react-redux的使用
使用 react-redux(也是为了方便写在一个文件里,实际开发还是得分开):下面是最原始的写法,需要定义较多变量和模板代码(现在redux 官方推荐使用Redux Toolkit,本例未使用)
// service
const getUser = (params: unknown) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
username: "Tom",
userId: 0,
});
}, 500);
});
};
// action type
type UserAction = Action<ValueOf<typeof USER_MODIFY_ACTION>> & {
payload: any;
};
const USER_MODIFY_ACTION = {
SET_USER: "setUser",
SET_NAME: "setName",
} as const;
// reducer
const userReducer: Reducer<User, UserAction> = (
// @ts-ignore
state = { username: "", userId: undefined },
action
) => {
// 这里可以使用对象来优化switch case;
switch (action.type) {
case USER_MODIFY_ACTION.SET_USER:
return action.payload as User;
case USER_MODIFY_ACTION.SET_NAME:
return { ...state, username: action.payload as string };
default:
return state;
}
};
// actions
const getUserAction = (params: unknown) => {
return (dispatch) =>
getUser(params).then((userInfo) => {
dispatch({
type: USER_MODIFY_ACTION.SET_USER,
payload: userInfo,
});
});
};
const updateUserNameAction = (username: string) => {
return {
type: USER_MODIFY_ACTION.SET_NAME,
payload: username,
};
};
// createStore
const reducers = combineReducers({ user: userReducer });
const enhancers = applyMiddleware(thunk);
const store = createStore(reducers, enhancers);
// 组件部分
export const App = () => {
return (
<Provider store={store}>
<InnerApp />
</Provider>
);
};
const InnerApp = () => {
const dispatch = useDispatch<ThunkDispatch<State, never, UserAction>>();
useEffect(() => {
dispatch(getUserAction({}));
}, []);
console.log("app render");
return (
<div>
<NavBar />
<MainPage />
</div>
);
};
const NavBar = () => {
const dispatch = useDispatch();
console.log("nav-bar:render");
return (
<div>
this is NavBar!!{" "}
<button
onClick={() => {
dispatch(updateUserNameAction("Bob"));
}}
>
修改用户名
</button>
</div>
);
};
const MainPage = () => {
console.log("main-page:render");
return (
<div>
<Content />
</div>
);
};
const Content = () => {
console.log("content:render");
return (
<div>
this is Content
<div>
<Message />
</div>
</div>
);
};
const Message = () => {
const user = useSelector<State, State["user"]>((state) => state.user);
console.log("message:render");
return <div>hello {user.username}</div>;
};
使用对比
现在可以看一下这两个例子从以下几个维度,试着回答开篇的问题:
程序性能
我们观察控制台的打印,发现redux在数据更新的时候渲染的组件更少,并且Context 组件需要额外的优化,总的来说,redux 性能更好,context更适合用静态数据的场景,例如主题信息,语言信息;
左侧是使用context的,右侧是使用redux的;
职责单一
Context Provider 顶层组件用户交互的逻辑与数据管理逻辑耦合在了一起;
注意context的数据是存在对应的组件上的,该组件销毁后,也就一同销毁了,但是redux是全局独立于组件树的,需要自己手动销毁(如果不销毁,大型应用里可能会导致内存爆炸),它的数据存储独立于组件外,代码组织上更灵活;
可重用性
context 有隐式的状态依赖,例如如果Message脱离了提供者组件的包裹,程序会抛出错误; redux 严格说也有这个问题,但是项目中一般在顶层使用,出现这个问题的几率较小;
这个地方只是一个点,其实这两者都有解决办法;context 用 Consumer;redux 用connect;来包裹复用组件即可;
可维护性
调试工具: react-redux 提供了redux dev tools; context 基于react-dev-tool, redux dev tools 更详细些;
总结
redux底层也是用的context来实现的;它们有各自的应用场景,如果是静态的全局数据共享场景或者数据量小,可以使用它,引入简单;如果是一个页面有很多组件,组件间交互多且复杂,那么react-redux是不错的一个选择,因为它从性能和代码可维护性都比context要好些;如果数据层级跟组件层级是对应的,那么使用props分发下去就可以(个人认为)
贴一个chatGpt 给出的答案:
参考链接
What is redux and what problem does it solve?
# The Problem with React's Context API
# When to Use Context API vs Redux
# A better way of solving prop drilling in React apps
# Idiomatic Redux: The History and Implementation of React-Redux
转载自:https://juejin.cn/post/7217305951552815159