Redux与Context API的区别

huangapple go评论70阅读模式
英文:

redux differentiation with context api

问题

1 - 可以确认,当Context API中的任何状态值更改时,它会导致整个组件重新渲染(full re-render)。

2 - 可以确认,Redux在内部使用Context API,根据提供的链接来自React Redux文档的解释。至于Redux Toolkit是否仍然在内部使用Context API,我无法提供确切信息。

3 - 据我了解,Context API的引入是为了成为Redux等工具的简化替代品。但如果Redux在内部使用Context API,那么可能是Context API首先存在,但可能没有直接暴露给开发者。

4 - 如果Redux确实在内部使用Context API,它可能使用某种映射层来避免完全组件重新渲染。关于Redux的具体实现细节,我无法提供详细信息。

英文:

My understanding is that React's Context API was essentially introduced for quick and dirty global state management, particularly before Redux Toolkit was introduced to simplify the overhead of implementing Redux.

My understanding is that one of the main downsides of Context API is that it any update to any property on it will re-render all component fields which are bound to Context API (full re-render).

I recently explained that downside to someone and he asked why that wouldn't be the case with Redux. He said Redux uses the Context API under the covers which appears to be the case based on some googling:

https://react-redux.js.org/using-react-redux/accessing-store#:~:text=Internally%2C%20React%20Redux%20uses%20React's,object%20instance%20generated%20by%20React.

A few questions:

1 - Can you confirm that Context API does a full component re-render when any state value is changed on it?

2 - Can you confirm that Redux uses Context API under the covers? And can you confirm if Redux Toolkit still uses Context API under the covers?

3 - I was under the impression that Context API was introduced to be a simpler alternative to tools like Redux. But if Redux uses Context API then did Context API come first but maybe it wasn't exposed directly to the developer?

4 - If Redux does use Context API under the covers then can you provide any insight into how Redux avoids a full component re-render? I would assume that there's somevtype of mapping layer but I'd be interested to get some more insight into the specific implementation details

答案1

得分: 4

> 我的理解是,React的Context API实际上是为了快速而简单的全局状态管理而引入的。

这是一个常见的误解。Context不是状态管理系统,就像props也不是状态管理系统。Context(就像props)是一种将数据从一个组件传递到另一个组件的方法。不同之处在于,props始终将数据传递给直接的子组件,而context使数据可用于子树中任何感兴趣的随机组件。

> 我的理解是,Context API的一个主要缺点是对其进行任何属性的更新都将重新渲染绑定到Context API的所有组件字段(完全重新渲染)。

这是正确的。同样,如果更改props,接收这些props的组件必须重新渲染。

> 1 - 你能确认Context API在更改其上的任何状态值时是否会进行完全的组件重新渲染吗?

对于正在监听该上下文的特定组件来说,是的。

> 2 - 你能确认Redux在内部使用Context API吗?你能确认Redux Toolkit是否仍在内部使用Context API吗?

React-redux确实使用了context。纯 reduxredux toolkit 不使用,因为它们是通用库,与React没有直接关系,但我想你指的是react-redux。

你必须在react-redux应用程序的顶部呈现的<Provider store={store}>是为了为其下面的组件提供上下文。调用useSelectoruseDispatch等钩子的组件然后使用上下文找到它们应该交互的存储。

> 3 - 我的印象是Context API被引入作为Redux等工具的简化替代方案。但如果Redux使用Context API,那么Context API是否首先出现,只是可能没有直接向开发人员公开?

Context已经存在很长时间,但过去它是一个非官方的功能。随着时间的推移,他们也使其更容易使用。

> 4 - 如果Redux确实在内部使用Context API,那么你能提供有关Redux如何避免完全重新渲染组件的任何见解吗?

上下文仅向子组件提供最少的内容,最重要的是存储对象的引用。存储对象的引用通常不会更改,因此由于上下文值不会更改,子组件不需要重新渲染。要查看它提供的内容,请参阅此代码:https://github.com/reduxjs/react-redux/blob/master/src/components/Provider.tsx#L33

存储的内容确实会更改,但这不是上下文提供的内容。要获取存储的内容,各个组件会订阅存储,使用redux的 store.subscribe,以及一个名为useSyncExternalStore的特殊钩子。基本上,当存储的状态更新时,redux会触发事件,然后各个组件根据自己的本地状态设置它们的状态,如果这是他们关心的变化。这种本地状态变化会导致重新渲染。

如果你正在编写使用上下文的代码,你很少需要做足够复杂的事情,需要useSyncExternalStore或自定义订阅系统。因此,你主要需要牢记以下几点:

  1. 将上下文集中在一个单一任务上。例如,如果你有一个主题对象来控制应用程序的颜色,还有一个描述当前登录用户的用户对象,将它们放在不同的上下文中。这样,只关心主题的组件在用户更改时不需要重新渲染,反之亦然。

  2. 如果你的上下文值是一个对象,请进行记忆化,以便它不会在每次渲染时更改(参见此文档)。

英文:

> My understanding is that React's Context API was essentially introduced for quick and dirty global state management

That's a common misunderstanding. Context is not a state management system, any more than props is a state management system. Context (like props) is a way to get data from one component to another. The difference is that props always passes the data to direct children, while context makes the data available to whichever random components in a subtree are interested.

> My understanding is that one of the main downsides of Context API is that it any update to any property on it will re-render all component fields which are bound to Context API (full re-render).

This is true. Similarly, if you change props, the component that receives those props must rerender

> 1 - Can you confirm that Context API does a full component re-render when any state value is changed on it?

Of the specific components that are listening to that context, yes.

> 2 - Can you confirm that Redux uses Context API under the covers? And can you confirm if Redux Toolkit still uses Context API under the covers?

React-redux does use context, yes. Pure redux and redux toolkit don't, since they're general purpose libraries not directly related to react, but I think you meant react-redux.

That &lt;Provider store={store}&gt; that you must render at the top of a react-redux app is there to provide a context to the components underneath it. Components that call hooks like useSelector or useDispatch then use the context to find the store that they should interact with.

> 3 - I was under the impression that Context API was introduced to be a simpler alternative to tools like Redux. But if Redux uses Context API then did Context API come first but maybe it wasn't exposed directly to the developer?

Context has existed for a long time, but it used to be an unofficial feature. They've also made it easier to use over time.

> 4 - If Redux does use Context API under the covers then can you provide any insight into how Redux avoids a full component re-render?

The context is only providing a minimal amount of things to the child components, most importantly the store object. The reference to the store object does not typically change, so since the context value does not change, the child components do not need to render. To see exactly what it's providing, see this code: https://github.com/reduxjs/react-redux/blob/master/src/components/Provider.tsx#L33

The contents of the store does change, but that's not what the context is providing. To get the contents of the store, the individual components subscribe to the store, using redux's store.subscribe, plus a special hook called useSyncExternalStore. Basically, redux fires an event when the store's state is updated, and then the individual components set their own local state if it's a change they care about. This local state change is what causes the rerender.


If you're writing code that uses context, you're rarely going to be doing things fancy enough to require useSyncExternalStore or a custom subscription system. So the main things you'll want to keep in mind are:

  1. Keep the context focused on a single task. For example, if you have a theme object to control your app's colors, and also a user object which describes who is currently logged in, put these in different contexts. That way a component that just cares about the theme doesn't need to rerender when the user changes, and vice versa.

  2. If your context value is an object, memoize it so it's not changing on every render (see this documentation)

答案2

得分: 3

我是Redux的维护者。 @NicholasTower 给出了一个很好的答案,但为了提供更多细节:

Context 和 Redux 是解决不同问题的非常不同的工具,它们有一些重叠之处。

Context 不是一个“状态管理”工具。它是一种依赖注入机制,其唯一目的是使一个单一值可在嵌套的 React 组件树中访问。决定该值是什么以及如何创建它完全由您决定。通常,可以使用来自 React 组件状态的数据,例如 useStateuseReducer。因此,实际上是您自己在进行“状态管理” - Context 只是提供了一种将其传递到树中的方法。

Redux 是一个库和一种模式,用于将状态更新逻辑与应用程序的其余部分分离,并使跟踪状态何时/在何处/为何/如何更改变得容易。它还使整个应用程序能够访问任何组件中的任何状态片段。

此外,Context 和(React-)Redux 在如何传递更新方面存在一些明显的差异。Context 存在一些重大性能限制 - 特别是,任何使用上下文的组件都将被迫重新渲染,即使它只关心上下文值的一部分。

Context 本身是一个很好的工具,我经常在自己的应用程序中使用它。但是,Context 不能“替代 Redux”。当然,您可以同时使用它们来传递数据,但它们并不是相同的东西。这就像问“我能用螺丝刀代替锤子吗?”不,它们是不同的工具,您使用它们来解决不同的问题。

由于这是一个常见问题,我写了一篇详细的文章来详细介绍它们之间的区别:

为什么 React Context 不是“状态管理”工具(以及为什么它不能替代 Redux)

具体回答您的问题:

  1. 是的,更新 Context 值将强制重新渲染所有使用该上下文的组件... 但实际上,它们有很大可能本来会被重新渲染,因为默认情况下 React 会递归渲染,并且在父组件中设置状态会导致该父组件内的所有组件重新渲染,除非您明确尝试避免。请查看我的文章 React 渲染行为(大部分)完全指南,该文章解释了所有这些工作原理。
  2. 是的,React-Redux 内部确实使用 Context... 但只是为了传递 Redux 存储实例,而不是当前状态值。这导致了非常不同的更新特性。而 Redux Toolkit 仅关于 Redux 逻辑,不特定于任何 UI 框架。
  3. Context 并不是作为替代 Redux 而引入的。在创建 Redux 之前,React 中存在一个“遗留上下文” API,React-Redux 使用了这个 API 直到 v5。然而,这个遗留上下文 API 在某些关键方面存在问题。当前的 React Context API 是在 React 16.3 中引入的,用于修复遗留 Context 中的问题,而不是专门用于替代 Redux。
  4. React-Redux 在每个组件实例中使用存储订阅和选择器,这是与 Context 运行机制完全不同的机制。

我强烈建议阅读我上面链接的文章,以及这些其他相关文章:

英文:

I'm a Redux maintainer. @NicholasTower gave a great answer, but to give some more details:

Context and Redux are very different tools that solve different problems, with some overlap.

Context is not a "state management" tool. It's a Dependency Injection mechanism, whose only purpose is to make a single value accessible to a nested tree of React components. It's up to you to decide what that value is, and how it's created. Typically, that's done using data from React component state, ie, useState and useReducer. So, you're actually doing all the "state management" yourself - Context just gives you a way to pass it down the tree.

Redux is a library and a pattern for separating your state update logic from the rest of your app, and making it easy to trace when/where/why/how your state has changed. It also gives your whole app the ability to access any piece of state in any component.

In addition, there are some distinct differences between how Context and (React-)Redux pass along updates. Context has some major perf limitations - in particular, any component that consumes a context will be forced to re-render, even if it only cares about part of the context value.

Context is a great tool by itself, and I use it frequently in my own apps. But, Context doesn't "replace Redux". Sure, you can use both of them to pass data down, but they're not the same thing. It's like asking "Can I replace a hammer with a screwdriver?". No, they're different tools, and you use them to solve different problems.

Because this is such a common question, I wrote an extensive post detailing the differences:

Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)

To answer your questions specifically:

  1. Yes, updating a Context value forces all components consuming that context to re-render... but there's actually a good chance that they would be re-rendering anyway because React renders recursively by default, and setting state in a parent component causes all components inside of that parent to re-render unless you specifically try to avoid it. See my post A (Mostly) Complete Guide to React Rendering Behavior, which explains how all this works.
  2. Yes, React-Redux does use Context internally... but only to pass down the Redux store instance, and not the current state value. This leads to very different update characteristics. Redux Toolkit, on the other hand, is just about the Redux logic and not related to any UI framework specifically.
  3. Context was not introduced to be an alternative to Redux. There was a "legacy Context" API that existed in React well before Redux itself was created, and React-Redux used that up through v5. However, that legacy context API was broken in some key ways. The current React Context API was introduced in React 16.3 to fix the problems in legacy Context, not specifically to replace Redux.
  4. React-Redux uses store subscriptions and selectors in each component instance, which is a completely different mechanism than how Context operates.

I'd definitely suggest reading the posts I linked above, as well as these other related posts:

答案3

得分: 2

以下是Mark Erikson的原始博客文章的翻译:

原始博客文章由Mark Erikson编写:

我将只是复制粘贴一些信息,但这是原始来源,我建议直接查看这里:https://blog.isquaredsoftware.com/2020/01/blogged-answers-react-redux-and-context-behavior/

更多链接在这里:

关于React上下文行为以及React-Redux如何在内部使用上下文的解释

有一些假设一再出现:

  • React-Redux只是“React上下文的包装器”
  • 如果解构上下文值,就可以避免由React上下文引起的重新渲染

这两种假设都是不正确的,我想澄清它们的实际工作原理,以便您将来可以避免错误使用它们。

关于上下文行为,假设我们有这个初始设置:

function ProviderComponent() {
    const [contextValue, setContextValue] = useState({a: 1, b: 2});

    return (
        <MyContext.Provider value={contextValue}>
            <SomeLargeComponentTree />
        </MyContext.Provider>
    )
}

function ChildComponent() {
    const {a} = useContext(MyContext);
    return <div>{a}</div>
}

如果ProviderComponent然后调用setContextValue({a: 1, b: 3})ChildComponent将重新渲染,即使它只关心解构的a字段。它也不管包装useContext(MyContext)调用的层次有多深。由于提供者传递了新引用,因此所有使用者都会重新渲染。实际上,如果我明确地重新渲染<MyContext.Provider value={{a: 1, b: 2}}> ChildComponent 仍然会重新渲染,因为提供者传递了新对象引用!(请注意,这就是为什么您不应该直接将对象字面量传递给上下文提供者,而应该将数据保存在状态中或者将上下文值的创建进行记忆化处理。)

对于React-Redux:是的,它在内部使用上下文,但只是将Redux存储实例传递给子组件 - 它不使用上下文传递存储状态!如果查看实际实现,它大致如下,但更复杂:

function useSelector(selector) {
    const [, forceRender] = useReducer( counter => counter + 1, 0);
    const {store} = useContext(ReactReduxContext);
        
    const selectedValueRef = useRef(selector(store.getState()));

    useLayoutEffect(() => {
        const unsubscribe = store.subscribe(() => {
            const storeState = store.getState();
            const latestSelectedValue = selector(storeState);

            if(latestSelectedValue !== selectedValueRef.current) {
                 selectedValueRef.current = latestSelectedValue;
                 forceRender();
            }
        })
        
        return unsubscribe;
    }, [store])

    return selectedValueRef.current;
}

因此,React-Redux只使用上下文传递存储本身,然后使用store.subscribe()来通知存储状态已更改。这导致性能行为与使用上下文传递数据大不相同。

有关上下文行为的详尽讨论在React问题#14110:提供更多退出挂钩的方法中有。在该讨论中,Sebastian Markbage明确表示

我个人的总结是,新上下文准备好用于低频率不太可能的更新(如语言/主题)。它也适用于与旧上下文相同的方式。即用于静态值,然后通过订阅传播更新。它还没有准备好用作所有Flux样式状态传播的替代品。

事实上,我们曾试图在React-Redux v6中将存储状态传递给上下文,但发现对我们的需求性能不足,这就是为什么我们不得不在React-Redux v7中重新编写内部实现,以再次使用直接订阅的原因

有关React-Redux实际工作原理的完整详细信息,请阅读我的帖子React-Redux的历史和实现,其中涵盖了随时间对内部实现的更改以及我们实际如何使用上下文。

英文:

Original blog post by Mark Erikson:

I'll just copy paste some info, but here's the original source and I recommend going directly here: https://blog.isquaredsoftware.com/2020/01/blogged-answers-react-redux-and-context-behavior/

More links here:

An explanation of how React Context behaves, and how React-Redux uses Context internally

There's a couple assumptions that I've seen pop up repeatedly:

  • React-Redux is "just a wrapper around React context"
  • You can avoid re-renders caused by React context if you destructure the context value

Both of these assumptions are incorrect, and I want to clarify how they actually work so that you can avoid mis-using them in the future.

For context behavior, say we have this initial setup:

function ProviderComponent() {
    const [contextValue, setContextValue] = useState({a: 1, b: 2});

    return (
        &lt;MyContext.Provider value={contextValue}&gt;
            &lt;SomeLargeComponentTree /&gt;
        &lt;/MyContext.Provider&gt;
    )
}

function ChildComponent() {
    const {a} = useContext(MyContext);
    return &lt;div&gt;{a}&lt;/div&gt;
}

If the ProviderComponent were to then call setContextValue({a: 1, b: 3}), the ChildComponent would re-render, even though it only cares about the a field based on destructuring. It also doesn't matter how many levels of hooks are wrapping that useContext(MyContext) call. A new reference was passed into the provider, so all consumers will re-render. In fact, if I were to explicitly re-render with &lt;MyContext.Provider value={{a: 1, b: 2}}&gt;, ChildComponent would still re-render because a new object reference has been passed into the provider! (Note that this is why you should never pass object literals directly into context providers, but rather either keep the data in state or memoize the creation of the context value.)

For React-Redux: yes, it uses context internally, but only to pass the Redux store instance down to child components - it doesn't pass the store state using context!. If you look at the actual implementation, it's roughly this but with more complexity:

function useSelector(selector) {
    const [, forceRender] = useReducer( counter =&gt; counter + 1, 0);
    const {store} = useContext(ReactReduxContext);
        
    const selectedValueRef = useRef(selector(store.getState()));

    useLayoutEffect(() =&gt; {
        const unsubscribe = store.subscribe(() =&gt; {
            const storeState = store.getState();
            const latestSelectedValue = selector(storeState);

            if(latestSelectedValue !== selectedValueRef.current) {
                 selectedValueRef.current = latestSelectedValue;
                 forceRender();
            }
        })
        
        return unsubscribe;
    }, [store])

    return selectedValueRef.current;
}

So, React-Redux only uses context to pass the store itself down, and then uses store.subscribe() to be notified when the store state has changed. This results in very different performance behavior than using context to pass data.

There was an extensive discussion of context behavior in React issue #14110: Provide more ways to bail out of hooks. In that thread, Sebastian Markbage specifically said:

> My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

In fact, we did try to pass the store state in context in React-Redux v6, and it turned out to be insufficiently performant for our needs, which is why we had to rewrite the internal implementation to use direct subscriptions again in React-Redux v7.

For complete detail on how React-Redux actually works, read my post The History and Implementation of React-Redux, which covers the changes to the internal implementation over time, and how we actually use context.

huangapple
  • 本文由 发表于 2023年2月19日 14:43:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/75498437.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定