Update only necessary states in useContext hook - reactjs

In the following example, a click on the first button re-renders both of the child components (ComponentOne and ComponentTwo), although the state of the second number does not change (because the whole useContext array value changes). I would like to know how we can detect multiple individual values, or any other method to only render states that changed.
const TwoNumbers = createContext();
function ComponentOne(){
const number = useContext(TwoNumbers)[0];
return(
<div>{number}</div>
)
}
function ComponentTwo(){
const number = useContext(TwoNumbers)[1];
return(
<div>{number}</div>
)
}
function App() {
const [numberOne, setNumberOne] = useState(0);
const [numberTwo, setNumberTwo] = useState(0);
return (
<TwoNumbers.Provider value={[numberOne, numberTwo]}>
<div className="App">
<ComponentOne/>
<ComponentTwo/>
<button onClick = {()=>{setNumberOne(numberOne+1)}}>First+1</button>
<button onClick = {()=>{setNumberTwo(numberTwo+1)}}>Second+1</button>
</div>
</TwoNumbers.Provider>
);
}
In real scenario the two numbers will represent the view count and other real-time data I would like to update after the backend updated. I see Stack Overflow is not doing this. Is real-time state matching an expensive move? Does it extensively add more time for page loading?

One solution is to create two Contexts and use two context providers. Then each of the child components (ComponentOne and ComponentTwo) consumes its own respective context. Finally in order to prevent a child component from being updated by its parent (App) due to the changes on the context value of the other sibling component, you can use React.memo.
const ContextNumberOne = createContext();
const ContextNumberTwo = createContext();
React.memo(function ComponentOne(){
const number = useContext(ContextNumberOne);
return(
<div>{number}</div>
)
})
React.memo(function ComponentTwo(){
const number = useContext(ContextNumberTwo);
return(
<div>{number}</div>
)
})
function App() {
const [numberOne, setNumberOne] = useState(0);
const [numberTwo, setNumberTwo] = useState(0);
return (
<ContextNumberOne.Provider value={numberOne}>
<ContextNumberTwo.Provider value={numberTwo}>
<div className="App">
<ComponentOne/>
<ComponentTwo/>
<button onClick = {()=>{setNumberOne(numberOne+1)}}>First+1</button>
<button onClick = {()=>{setNumberTwo(numberTwo+1)}}>Second+1</button>
</div>
</ContextNumberTwo.Provider>
</ContextNumberOne.Provider>
);
}

Look into using Redux instead, it specially solves this problem. You can use useSelector to subscribe to the particular bit of state that you care about, and only rerender when that changes.

Related

How to prevent components from refreshing when state in context is changed (React)

Im shooting my shot at making a tiktok clone as a first project to learn React. I want to have a global isVideoMuted state. When you toggle it, it should mute or unmute all sound of all videos.
Except something is not working properly. I understand that react re-renders everything when you change one thing within the contextprovider parent component from a childcomponent. This resets the tiktok "scrollprogress" to zero, since its a simple vertical slider. Is there anyway I can prevent this from happening?
This is my VideoContextProvider:
const VideoContextProvider = ({ children }: any) => {
const [isVideoMuted, setIsVideoMuted] = useState(true);
const [videos, setVideos] = useState([] as Video[]);
return (
<VideoContext.Provider
value={{
videos,
setVideos,
isVideoMuted,
setIsVideoMuted,
}}
>
{children}
</VideoContext.Provider>
);
};
And this is the VideoCard.tsx (one single video):
const VideoCard: FC<Props> = ({ video }) => {
const router = useRouter();
const [isLiked, setIsLiked] = useState(false);
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext);
return (
.... (all the remaining videocode is here, including the <video>)
<Box p="xs">
{isVideoMuted ? (
<VscMute
fontSize="28px"
color="white"
onClick={() => setIsVideoMuted(false)}
/>
) : (
<VscUnmute
fontSize="28px"
color="white"
onClick={() => setIsVideoMuted(true)}
/>
)}
</Box>
...
);
};
export default VideoCard;
// export const VideoCardMemoized = memo(VideoCard);
)
See this video for an example of the bug: https://streamable.com/8ljkhl
Thanks!
Edit:
What I've tried so far:
Making a memoized version of the VideoCard and using that, refreshing still occurs
Moving const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext); to a seperate component (VideoSidebar). Problem still occurs, since I have to 'subscribe' to the isVideoMuted variable in the video element
It rerenders only components that are subscribed to that context.
const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext);
To prevent rerendering child components of the subscribed components you can use React.memo
export default React.memo(/* your component */)

React sibling component updates state of parent causing re-render

I'll preface this question by saying I've spent about 3 weeks with React (previously have worked with Vue) so still pretty green.
I have a component structure like the following:
const initialState = { propertyA: null, propertyB: null };
const WrapperComponent: FC<Props> = (props) => {
const [dynamicObject, setDynamicObject] = React.useState(initialState);
const customCallbackFunction = (newObjectVal) => { setDynamicObject(newObjectVal) };
return (
<div>
<SiblingComponent dynamicObject={dynamicObject} />
<DifferentSiblingComponent onAction={customCallbackFunction} />
</div>
);
}
Problem I'm facing is each call to customCallbackFunction is triggering re-render in both SiblingComponent and DifferentSiblingComponent. The re-render in SiblingComponent is desired, because I want that component to display the dynamic data being emitted by customCallbackFunction. However, I'd like to avoid the re-render of DifferentSiblingComponent.
For more context, customCallbackFunction is being fired on certain hover events on a canvas - so the constant re-rendering is causing an infinite callback loop.
Is there a way to handle this without pulling in something like Redux? Any help/insight is appreciated.
Note: I did read that React.FC is discouraged, that is what the team has used in the past so I was just following those templates
Problem I'm facing is each call to customCallbackFunction is triggering re-render in both SiblingComponent and DifferentSiblingComponent.
Yes, that's normal. Unless you do something to prevent it, all child components of the component whose state was updated are updated.
To prevent that in your situation, you need to do two things:
Memoize DifferentSiblingComponent. Since it's a function component, you'd do that with React.memo.
Memoize customCallbackFunction with useCallback or useMemo or similar so that it's stable across the lifecycle of the parent component, rather than being newly created every time the component renders. That way, DifferentSiblingComponent sees a stable value, and the memoization works.
const customCallbackFunction = useCallback(
(newObjectVal) => { setDynamicObject(newObjectVal) },
[]
);
I go into more detail on this in this answer.
Live Example:
const { useState, useCallback } = React;
const initialState = { propertyA: null, propertyB: null };
const SiblingComponent = React.memo(({ dynamicObject }) => {
console.log(`SiblingComponent rendered`);
return <div>{JSON.stringify(dynamicObject)}</div>;
});
// Just so we can see things change
let counter = 0;
const DifferentSiblingComponent = React.memo(({ onAction }) => {
console.log(`DifferentSiblingComponent rendered`);
return (
<input
type="button"
onClick={() => onAction({ ...initialState, counter: ++counter })}
value="Update"
/>
);
});
const WrapperComponent /*: FC<Props>*/ = (props) => {
const [dynamicObject, setDynamicObject] = useState(initialState);
const customCallbackFunction = useCallback((newObjectVal) => {
setDynamicObject(newObjectVal);
}, []);
return (
<div>
<SiblingComponent dynamicObject={dynamicObject} />
<DifferentSiblingComponent onAction={customCallbackFunction} />
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<WrapperComponent />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
When a parent rerenders all of its children are rerendered also.
To prevent that React.memo might be helpful.
const MyComponent = React.memo(function DifferentSiblingComponent(props) {
/* .... */
});
After you apply memo to one of your components you may need to apply useCallback to the customCallbackFunction so that it is not different during rerenders and doesn't cause memo to rerender the component anyway.
e.g.
let customCallbackFunction = React.useCallback((newObjectVal) => { setDynamicObject(newObjectVal) },[])

Change state of something from another class with a button [duplicate]

I am new to React and have had some hard time to understand the concept of states.
Down below I export a stepper from MUI. I use state
export default function CustomizedSteppers() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
//...
//Stepper stuff...
//...
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</Stack>
);
}
I now want to split this code so that I can setActiveStep from another component.
Meaning, I want to put the button outside of this component and put it in another class, but still allow that button to change the value of activeStep - by accessing the method handleNext on click on a button outside this class. How do I manage to do this?
You need to make a parent component, define activeStep and handleNext there, and pass them to your CustomizedSteppers
Parent.js
import CustomizedSteppers from "./CustomizedSteppers"
export default function Parent() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<>
<CustomizedSteppers activeStep={activeStep} handleNext={handleNext}>
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</>
)
}
CustomizedSteppers.js
export default function CustomizedSteppers({activeStep, handleNext}) {
return (
//...
//Stepper stuff...
//...
</Stack>
);
}
You have a couple options.
The first is to lift your state, like #Peter said. That is, you'll need to move activeStep to the component that houses both this component and the other component you want to have activeStep's value, and then pass that value to both (a practice known as prop drilling). This is probably the simplest way, if this is the only variable you want to do that for.
The second is to use state management tools, like #ahmetkilinc said. The Context API is native to React, so it won't require any extra installations or tools. There are also third-party tools like Redux that try to solve this problem (though I believe Redux is still just using Contexts behind the scenes, anyway). This option is better if this is a need you anticipate having multiple times throughout your application.
You can just pass the state updating function as a prop to your component.
Your Button Component:
function CustomButtonComponent (props) {
return (
<button onClick={() => props.setCount(count => count + 1)}>
Click me
</button>
)
}
export default CustomButtonComponent
And In Your Main Component
<CustomButtonComponent setCount={setCount} />
Also Since state updates are scheduled in React, you shouldnt use
onClick={() => setCount(count + 1)}
Instead use
onClick={() => setCount(prevCount => prevCount + 1)}
Try understanding following code .
const Child = (props)=>{
return (
<Button variant="contained" onClick={props.handleNext}>Hello
World</Button>
)
}
const Parent = ()=>{
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<Child handleNext={(e)=>handleNext(e)} />
)
}
Take this code as a sample example and try implementing it with your code .
What is happening in this Parent Function ?
We are passing handleNext as a prop to to Child component which triggers handleNext function inside Parent component when a button is clicked inside Child component.

Change state of something from another class with a button

I am new to React and have had some hard time to understand the concept of states.
Down below I export a stepper from MUI. I use state
export default function CustomizedSteppers() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
//...
//Stepper stuff...
//...
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</Stack>
);
}
I now want to split this code so that I can setActiveStep from another component.
Meaning, I want to put the button outside of this component and put it in another class, but still allow that button to change the value of activeStep - by accessing the method handleNext on click on a button outside this class. How do I manage to do this?
You need to make a parent component, define activeStep and handleNext there, and pass them to your CustomizedSteppers
Parent.js
import CustomizedSteppers from "./CustomizedSteppers"
export default function Parent() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<>
<CustomizedSteppers activeStep={activeStep} handleNext={handleNext}>
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</>
)
}
CustomizedSteppers.js
export default function CustomizedSteppers({activeStep, handleNext}) {
return (
//...
//Stepper stuff...
//...
</Stack>
);
}
You have a couple options.
The first is to lift your state, like #Peter said. That is, you'll need to move activeStep to the component that houses both this component and the other component you want to have activeStep's value, and then pass that value to both (a practice known as prop drilling). This is probably the simplest way, if this is the only variable you want to do that for.
The second is to use state management tools, like #ahmetkilinc said. The Context API is native to React, so it won't require any extra installations or tools. There are also third-party tools like Redux that try to solve this problem (though I believe Redux is still just using Contexts behind the scenes, anyway). This option is better if this is a need you anticipate having multiple times throughout your application.
You can just pass the state updating function as a prop to your component.
Your Button Component:
function CustomButtonComponent (props) {
return (
<button onClick={() => props.setCount(count => count + 1)}>
Click me
</button>
)
}
export default CustomButtonComponent
And In Your Main Component
<CustomButtonComponent setCount={setCount} />
Also Since state updates are scheduled in React, you shouldnt use
onClick={() => setCount(count + 1)}
Instead use
onClick={() => setCount(prevCount => prevCount + 1)}
Try understanding following code .
const Child = (props)=>{
return (
<Button variant="contained" onClick={props.handleNext}>Hello
World</Button>
)
}
const Parent = ()=>{
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<Child handleNext={(e)=>handleNext(e)} />
)
}
Take this code as a sample example and try implementing it with your code .
What is happening in this Parent Function ?
We are passing handleNext as a prop to to Child component which triggers handleNext function inside Parent component when a button is clicked inside Child component.

Making the state of a component affect the rendering of a sibling when components are rendered iteratively

I have the following code:
export default function Parent() {
const children1 = someArrayWithSeveralElements.map(foo => <SomeView />);
const children2 = someArrayWithSeveralElements.map(foo => <SomeCheckbox />);
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
For a given element foo, there is a SomeView component that is conditionally rendered based on the state of a SomeCheckbox. I'm having trouble figuring out a way to have the state from the checkbox affect the rendering of the sibling view component.
Normally the solution would be to just declare the state hook in the parent component and pass them down to each child, but since the siblings are rendered via foreach loops it's impossible to do so.
My current solution is to also generate the state hooks for each foo in a loop as well, but that feels a bit hacky since it's better to avoid creating hooks inside of loops (it's worth nothing that someArrayWithSeveralElements is not intended to change after mounting).
Is there a more elegant alternative to solve this?
The solution is what you side, you need to create a state in the parent component and pass it to the children. and this will work for single component or bunch of them, the difference is just simple: use array or object as state.
const [checkboxesStatus, setCheckboxesStatus] = useState({// fill initial data});
const children1 = someArrayWithSeveralElements.map(foo =>
<SomeView
visibile={checkBoxesStatus[foo.id]}
/>);
const children2 = someArrayWithSeveralElements.map(foo =>
<SomeCheckbox
checked={checkBoxesStatus[foo.id]}
onChange={// set new value to foo.id key}
/>)
export default function Parent() {
const [states, setStates] = React.useState([]);
const children1 = someArrayWithSeveralElements.map((foo, i) => <SomeView state={states[i]} />);
const children2 = someArrayWithSeveralElements.map((foo, i) => {
const onStateChange = (state) => {
setStates(oldStates => {
const newStates = [...(oldStates || [])]
newStates[i] = state;
return newStates;
})
}
return <SomeCheckbox state={states[i]} onStateChange={onStateChange} />;
});
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
Use states in the parent componet.
Note: the element of states may be undefined.

Resources