React context with object value - Alternative? - reactjs

I've been working with a React context that takes in a value that is an object, and facing rerendering issues that I didn't understand fully until I saw this statement:
If you are passing down an object on your React context provider and any property on it updates, what happens? Any component which consumes that context will re-render.
My question is... Is there an alternative to using a context when it comes to holding global state stuff in an object that will be updated?

Related

redux differentiation with context api

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
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 <Provider store={store}> 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:
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.
If your context value is an object, memoize it so it's not changing on every render (see this documentation)
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:
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.
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.
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.
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:
Redux - Not Dead Yet!
When (and when not) to Reach for Redux
React, Redux, and Context Behavior.
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:
https://github.com/markerikson/react-redux-links
https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/
https://medium.com/async/how-useselector-can-trigger-an-update-only-when-we-want-it-to-a8d92306f559
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 (
<MyContext.Provider value={contextValue}>
<SomeLargeComponentTree />
</MyContext.Provider>
)
}
function ChildComponent() {
const {a} = useContext(MyContext);
return <div>{a}</div>
}
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 <MyContext.Provider value={{a: 1, b: 2}}>, 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 => 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;
}
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.

All consumers of a Provider will re-render when value prop changes?

I'm trying to understand React Context API better. In the react docs I stumbled on this paragraph:
All consumers that are descendants of a Provider will re-render
whenever the Provider’s value prop changes. The propagation from
Provider to its descendant consumers (including .contextType and
useContext) is not subject to the shouldComponentUpdate method, so the
consumer is updated even when an ancestor component skips an update.
I'm having a hard time understanding this, since the definition of a consumer is a little unclear to me. If I try to create an example (codingsandbox) the result is always the same: If the value changes, all children (not consumers) of the Provider rerender.
I'd love to understand what is meant by the paragraph above. Is there a case where the context value updates without triggering a new render of its children (preferably in a function component)?
The problem, according to your sandbox, is how state is being managed in the main app component.
//your useState
const [store, setStore] = React.useState([]);
//the button that is setting the state
<button onClick={() => setStore([])}>Reset store value</button>
This causes the state in the component to change, which rerenders the App component itself, which causes a rerender of all it's children. If you wish to update the context state only, you should avoid storing the value in state and rather directly mount the function within the Context Object for logic to change the property within the value object

Can someone explain why I shouldn't update a value in React `context` directly?

I'm supplying context via a provider to a child component via consumer. Inside of that consumer component, if the user takes an action, I'd like to update a particular value in context, so that all other consumer components have access to the new context value.
I understand that from the React documentation, the right way to do this is to also pass a setter function in context, which will update the value in state on the component that is responsible for creating the context provider itself.
Reference: https://reactjs.org/docs/context.html#updating-context-from-a-nested-component
What I'm wondering is why can't I just do something as simple as this:
this.context.X = Y;
It seems to work, and I don't see any noticeable side effects, but I'm thinking there must be a reason why the React docs don't show this approach.

Reducer Props getting changed in the component

In my React app, I have a redux reducer which sends a List as props to my component.
I copy the prop to local state, and show as drop down. User changes the Dropdown so my local state changes.
On click of cancel , I am calling redux Toastr which triggers a method to reset my state with the original props.list. But for some reason the props.list also changed similar to my state change. With my knowledge i thought props passed to the componeent will not be changed until again i call action creator.
Anyone faced similar issue? or i am doing something wrong
Sorry for not posting the code, which I will prepare a demo if needed. Thanks!
What you are doing is an anti-pattern as you are breaking react's rule of single source of truth. You can't hold state internally in a component that is tied up with your redux state and expect it work smoothly. A similar issue of breaking the single source of truth arise when using solely React without Redux and when you try to pass props as state. In this case there is a new lifecycle hook static getDerivedStateFromProps but even this hook is advised to be used sparsely you can read about it here. So if your intent is to reset state back to it's original value, you can either:
Use static getDerivedStateFromProps (which is reserved mostly for UI)
Use a key prop which will reset you back to your initial state
Use a memoization helper such as memoize-one

query internal state of component using useState with enzyme

I am using enzyme 3.9.0.
I have just refactored a class component to a component that uses the useState hook.
I was previously testing the component's internal state by extracting it like this:
const shortcutActions = wrapper.state().shortcuts;
expect(shortcutActions).toHaveLength(1);
Is it possible to query the internal state when useState is used?
Hooks are brand new and Enzyme is being updated to support them.
You can follow the general progress here: Issue #2011.
Support for useState in particular is being tracked here: Issue #2008.
So the current level of support for useState is:
mount supports get and rendered element update with set
shallow supports get, but does not yet support rendered element update with set
As for querying the internal state directly, I'm not sure that is necessary.
From this section:
Hooks don’t work inside classes. But you can use them instead of writing classes.
...so hooks only work in what React prefers to now call "function components".
Any state defined using useState will exist only inside the function body and will not be accessible to anything outside the function.
The only way to modify that state is by interacting with the rendered output that the function returns, so testing a function component should just involve interacting with the rendered component and verifying that it re-renders as expected.

Resources