Redux 是模式的一半(2/2)
我曾经写过一个表单库。
一次。
它被称为React Redux Form,当时用 Redux 来做表单是个好主意(别用它)。事实上,我的库就是为了回应Redux Form而写的,这两个库很快都发现,用一个全局的 store来存储所有应用状态的想法非常非常糟糕。
当所有表单都放在一个 store 中时,状态一开始很容易管理。但之后,每次按键都会开始卡顿。用户体验非常糟糕。
那么你该怎么做呢?
- 模糊输入
- 添加去抖动更新
- 记住一切
- 全面优化选择器
- 使受控组件不受控制
React.memo()
在组件上使用PureComponent
充分使用- 使用悬念(??)
- 等等等等
简而言之,您进入恐慌模式并尝试控制影响每个连接组件的全局更新的传播,即使这些组件不需要重新渲染。
你们中的一些人已经非常擅长解决这个问题,并且已经成为“选择器、缓存和记忆化”方面的专家级开发者。这太棒了。
但让我们来检验一下这些策略是否有必要。如果所有状态都不是全局的呢?
本地状态与全局状态
Redux 的三个原则中的第一个是整个应用程序状态本质上只有一个事实来源:
整个应用程序的状态存储在单个存储内的对象树中。
这样做的主要原因是它让很多事情变得更容易,比如共享数据、恢复状态、“时间旅行调试”等等。但它存在一个根本性的缺陷:在任何重要的应用程序中,都不存在单一的可信来源。所有应用程序,即使是前端应用程序,在某种程度上都是分布式的:
而且,矛盾的是,甚至 Redux 样式指南也建议不要将应用程序的整个状态放在单个存储中:
[...] 相反,应该有一个地方来查找所有你认为是全局的、应用范围的值。“本地”的值通常应该保存在最近的 UI 组件中。
来源:Redux 样式指南
每当为了简化某件事而做某事时,它几乎总是会使其他用例变得更加困难。Redux 及其单一事实来源也不例外,因为与前端应用程序的“分布式”本质(而不是理想的原子全局单元)作斗争会产生许多问题:
- 需要以某种方式在状态中表示多个正交关注点。
这是通过使用“解决”的combineReducers
。
- 多个独立的关注点需要共享数据、相互沟通或以其他方式间接相关。
这是通过更复杂、自定义的减速器“解决”的,这些减速器通过这些原本独立的减速器来协调事件。
- 不相关的状态更新:当单独的关注点被组合(使用
combineReducers
或类似)到单个存储中时,每当状态的任何部分更新时,整个状态都会更新,并且每个“连接”的组件(Redux 存储的每个订阅者)都会收到通知。
这可以通过使用选择器来“解决” ,或者也可以通过使用另一个库(例如reselect
用于记忆选择器)来“解决”。
我把“已解决”加上引号,是因为这些解决方案几乎都是必要的,因为它们仅仅因为使用全局原子存储而导致了一些问题。简而言之,即使对于已经使用全局存储的应用来说,拥有单一的全局存储也是不现实的。每当你使用第三方组件、本地状态、本地存储、查询参数或路由器等时,你就已经打破了单一全局存储的幻想。应用数据总是在某种程度上是分布式的,因此自然的解决方案应该是拥抱这种分布式(通过使用本地状态),而不是仅仅为了在短期内让某些用例更容易开发而与之对抗。
行为方式不同
那么,我们该如何解决这个全局状态问题呢?为了回答这个问题,我们需要回顾一下过去,并从另一个古老而成熟的模型中汲取一些灵感:Actor 模型。
Actor 模型出奇地简单,可以稍微扩展一下,使其超越其最初的用途(并发计算)。简而言之,Actor 是一个可以做三件事的实体:
- 它可以接收消息(事件)
- 它可以改变其状态/行为作为对收到的消息的反应,包括产生其他参与者
- 它可以向其他参与者发送消息
如果你觉得“嗯……Redux store 有点像 Actor”,那么恭喜你,你已经对这个模型有了基本的了解!Redux store 是基于某个单一的组合 Reducer 的:
- ✅ 可以接收事件
- ✅作为对这些事件的反应,改变其状态(如果你操作正确,则改变其行为)
- ❌ 无法向其他商店(只有一个商店)或在减速器之间发送消息(调度仅从外向内进行)。
它也不能真正产生其他“参与者”,这使官方 Redux 高级教程中的 Reddit 示例比它需要的更加尴尬:
function postsBySubreddit(state = {}, action) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
让我们分析一下这里发生的事情:
- 我们只获取我们需要的相关状态片段(
state[action.subreddit]
),理想情况下它应该是自己的实体 - 我们正在确定只有这个切片的下一个状态应该是什么,通过
posts(state[action.subreddit], action)
- 我们正在通过 精确地用更新的切片替换该切片
Object.assign(...)
。
换句话说,我们无法将事件直接分派或转发给特定的“实体”(或Actor);我们只有一个 Actor,并且必须手动更新其相关部分。此外,所有其他 ReducercombineReducers(...)
都会获取特定于实体的事件,即使它们不更新,每个 Reducer 仍会针对每个事件被调用。没有简单的方法可以优化这一点。一个未被调用的函数仍然比一个被调用但最终不执行任何操作(即返回相同状态)的函数更优,而后者在 Redux 中大多数情况下都会发生。
Reducer 和 Actor
那么 Reducer 和 Actor 是如何结合在一起的呢?简单来说,一个 Reducer 描述了一个 Actor 的行为:
- 事件被发送到 reducer
- Reducer 的状态/行为可能会因接收到的事件而改变
- Reducer 可以生成 Actor 和/或向其他 Actor 发送消息(通过执行声明性操作)
这并非什么尖端的、突破性的模型;事实上,你可能已经(在某种程度上)使用过 Actor 模型,只是你还不知道而已!考虑一个简单的输入组件:
const MyInput = ({ onChange, disabled }) => {
const [value, setValue] = useState('');
return (
<input
disabled={disabled}
value={value}
onChange={e => setValue(e.target.value)}
onBlur={() => onChange(value)}
/>
);
}
这个组件以一种隐式的方式有点像一个演员!
- 它使用 React 略显笨拙的父子通信机制来“接收事件”——prop 更新
- 当“接收”事件时,它会改变状态/行为,例如当
disabled
道具改变时true
(您可以将其解释为某个事件) - 它可以将事件发送给其他“参与者”,例如通过调用
onChange
回调向父级发送“更改”事件(再次使用 React 略显尴尬的子级到父级通信机制) - 理论上,它可以通过渲染不同的组件来“产生”其他“参与者”,每个组件都有自己的本地状态。
Reducer 使行为和业务逻辑更加明确,特别是当“隐式事件”变得具体、分派事件时:
const inputReducer = (state, event) => {
/* ... */
};
const MyInput = ({ onChange, disabled }) => {
const [state, dispatch] = useReducer(inputReducer, {
value: '',
effects: []
});
// Transform prop changes into events
useEffect(() => {
dispatch({ type: 'DISABLED', value: disabled });
}, [disabled]);
// Execute declarative effects
useEffect(() => {
state.effects.forEach(effect => {
if (effect.type === 'notifyChange') {
// "Send" a message back up to the parent "actor"
onChange(state.value);
}
});
}, [state.effects]);
return (
<input
disabled={disabled}
value={state.value}
onChange={e => dispatch({
type: 'CHANGE', value: e.target.value
})}
onBlur={() => dispatch({ type: 'BLUR' })}
/>
);
}
多 Redux?
再次强调,Redux 的三大原则之一是 Redux 存在于一个单一、全局、原子的数据源中。所有事件都通过该 store 路由,单一巨大的状态对象会被更新,并渗透到所有连接的组件中。这些组件使用它们的选择器、记忆机制和其他技巧来确保它们只在需要时更新,尤其是在处理过多、不相关的状态更新时。
在使用 Redux 时,使用单个全局 store 效果很好,对吧?嗯……并非完全如此,因为有专门的库提供在更分布式的层面上使用 Redux 的能力,例如用于组件状态和封装。虽然可以在本地组件级别使用 Redux,但这并不是它的主要目的,官方react-redux
集成也不会提供这种能力。
没有 Redux?
还有一些库也采用了“状态局部性”的理念,比如MobX和XState。对于 React,有用于“分布式”状态的Recoil以及内置的useReducer
hook,它感觉很像本地 Redux,专门用于你的组件。对于声明式效果,我创建了useEffectReducer
一个外观和感觉都与 类似的useReducer
,但也提供了一种管理效果的方法。
对于需要共享(非全局)的状态,您可以使用与 React-Redux 已经使用的非常相似的模式,即创建一个可以订阅(即“监听”)并通过 context 传递的对象:
这将为您提供最佳性能,因为“可订阅”对象很少/永远不会改变。如果您觉得这有点样板,并且性能不是主要考虑因素,您可以轻松组合useContext
和useReducer
:
const CartContext = createContext();
const cartReducer = (state, event) => {
// reducer logic
// try using a state machine here! they're pretty neat
return state;
};
const initialCartState = {
// ...
};
const CartContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialCartState);
return <CartContext.Provider value={[state, dispatch]}>
{children}
</CartContext.Provider>;
};
export const useCartContext = () => {
return useContext(CartContext);
};
然后在你的组件中使用它:
const CartView = () => {
const [state, dispatch] = useCartContext();
// ...
};
还不错吧?总的来说,这个问题在 Redux 中无法解决,除非另辟蹊径,因为 Redux 本质上是一个单一的、原子的全局 store。
其他人怎么想?
我在 Twitter 上进行了一项非科学性的民意调查,以了解大多数应用程序状态位于何处,以及开发人员对此有何感受:
由此我得出了两点结论:
- 无论您是在本地分发状态,还是将所有状态包含在单个存储中,您都能够成功完成应用程序状态要求。
- 然而,更多的开发人员对大多数应用程序状态是全局的而不是本地的感到不满,这也可能暗示了为什么大多数开发人员乐于使用本地状态。
你怎么看?在评论区分享你的想法吧!
结论
以“Actor”的视角思考,你的应用程序由许多较小的Actor组织起来,它们通过传递消息/事件来相互通信,这可以促进关注点分离,并让你以不同的方式思考状态应该如何本地化(分布式)和连接。我写这篇文章的目的是帮助你认识到并非所有状态都需要是全局的,并且存在其他模式(例如Actor模型)用于建模分布式状态和通信流。
然而,Actor 模型并非万能。如果不小心,最终可能会陷入面条式的状态管理问题,完全无法追踪哪个 Actor 正在与另一个 Actor 通信。任何解决方案中都可能存在反模式,因此在开始编码之前,研究最佳实践并实际建模你的应用会很有帮助。
如果你想了解有关 Actor 模型的更多信息,请查看Brian Storti的《10 分钟了解 Actor 模型》,或观看以下任何视频:
请记住,这篇文章仅代表我基于研究得出的观点,并非旨在指导你如何做事。我希望引导你思考,并希望这篇文章能够达到这个目的。感谢阅读!
如果您喜欢这篇文章(或者即使您不喜欢,只是想听听我的更多州管理漫谈),请订阅 Stately Newsletter以获取更多内容、想法和讨论📬
鏂囩珷鏉ユ簮锛�https://dev.to/davidkpiano/redux-is-half-of-a-pattern-2-2-4jo3