shouldComponentUpdate equivalent for functional component, to ignore state changes - javascript

My code has a component that takes both props and has its own internal state.
The component should rerender ONLY when its props change. State changes should NOT trigger a rerender.
This behaviour can be implemented with a class based component and a custom shouldComponentUpdate function.
However, this would be the first class based component in the codebase. Everything is done with functional components and hooks.
Therefore I would like to know whether it is possible to code the desired functionality with functional components.
After a few answers that didn't approach the real problem, I think I have to reformulate my question. Here is a minimal example with two components:
Inner takes a prop and has state. This is the component in question. It must not rerender after state changes. Prop changes should trigger a rerender.
Outer is a wrapper around inner. It has no meaning in the scope of this question and is only there to give props to Inner and to simulate prop changes.
To demonstrate the desired functionality I have implemented Inner with a class based component. A live version of this code can be found on codesandbox. How can I migrate it to a functional component:
Inner.tsx:
import React, { Component } from 'react'
interface InnerProps{outerNum:number}
interface InnerState{innerNum:number}
export default class Inner extends Component<InnerProps, InnerState> {
state = {innerNum:0};
shouldComponentUpdate(nextProps:InnerProps, nextState:InnerState){
return this.props != nextProps;
}
render() {
return (
<button onClick={()=>{
this.setState({innerNum: Math.floor(Math.random()*10)})
}}>
{`${this.props.outerNum}, ${this.state.innerNum}`}
</button>
)
}
}
Outer.tsx:
import React, { useState } from "react";
import Inner from "./Inner";
export default function Outer() {
const [outerState, setOuterState] = useState(1);
return (
<>
<button
onClick={() => {
setOuterState(Math.floor(Math.random() * 10));
}}
>
change outer state
</button>
<Inner outerNum={outerState}></Inner>
</>
);
}
The official docs say to wrap the component in React.memo. But this doesn't seem to work for preventing rerenders on state change. It only applies to prop changes.
I have tried to make React.memo work. You can see a version of the code with both Outer and Inner being functional components here.
Related questions:
How to use shouldComponentUpdate with React Hooks? : This question only deals with prop changes. The accepted answer advises to use React.memo
shouldComponentUpdate in function components : This question predates stateful functional components. The accepted answer explains how functional components don't need shouldComponentUpdate since they are stateless.

React memo do not stop state changes
React.memo only checks for prop changes. If your function component
wrapped in React.memo has a useState or useContext Hook in its
implementation, it will still rerender when state or context change.
Ref:- https://reactjs.org/docs/react-api.html#reactmemo

Your Inner component depends on the property num of the Outer component, you can't prevent it from rendering on property change as React.memo makes properties comparison:
// The default behaviour is shallow comparison between previous and current render properties.
const areEqual = (a, b) => a.num === b.num;
export default React.memo(Inner, areEqual);
By memoizing the Inner component and removing the num dependency, it won't render on Outer rendering, see sandbox attached.
export default function Outer() {
const [outerState, setOuterState] = useState(1);
return (
<>
...
// v Inner is memoized and won't render on `outerState` change.
<Inner />
</>
);
}
If you want to implement shouldComponentUpdate with hooks you can try:
const [currState] = useState();
// shouldUpdateState your's custom function to compare and decide if update state needed
setState(prevState => {
if(shouldUpdateState(prevState,currState)) {
return currState;
}
return prevState;
});

React is by design driven by setState -> re-render loop. Props change is in fact a setState somewhere in parent components. If you don't want the setState to trigger a re-render, then why in the first place use it?
You can pull in a const state = useRef({}).current to store your internal state instead.
function InnerFunc(props) {
const state = useRef({ innerNum: 0 }).current;
return (
<button
onClick={() => {
state.innerNum = Math.floor(Math.random() * 10);
}}
>
{`${props.outerNum}, ${state.innerNum}`}
</button>
);
}
That said, it's still a valid question to ask: "how to implement shouldComponentUpdate in a react hook fashion?" Here's the solution:
function shouldComponentUpdate(elements, predicate, deps) {
const store = useRef({ deps: [], elements }).current
const shouldUpdate = predicate(store.deps)
if (shouldUpdate) {
store.elements = elements
}
store.deps = deps
return store.elements
}
// Usage:
function InnerFunc(props) {
const [state, setState] = useState({ innerNum: 0 })
const elements = (
<button
onClick={() => {
setState({ innerNum: Math.floor(Math.random() * 10) });
}}
>
{`${props.outerNum}, ${state.innerNum}`}
</button>
);
return shouldComponentUpdate(elements, (prevDeps) => {
return prevDeps[0] !== props
}, [props, state])
}
Noted that it's impossible to prevent a re-render cycle when setState is called, the above hook merely makes sure the re-rendered result stays the same as prev rendered result.

you should use the event that provide the browser and capture in the function before setState, like this
function setState = (e) =>{ //the e is the event that give you the browser
//changing the state
e.preventDefault();
}

Related

How to make React.memo rerender logic deterministic in React functional components

I have a functional React component that uses useState and useContext, and it derives its display properties from both context and props.
I am using useMemo with its second argument, the areEqual function to prevent the component from re-rendering when new props come in. There are dozens of values for the prop, but I only want to re-render the component when the prop changes to a specific value.
Here is a simplified example:
const MyComponent = (props) => {
const {contextThing} = useContext();
const {stateThing} = useState();
const {propsThing} = props;
// Since only foo changes what is rendered, I want to prevent rerendering unless the
// prop changes to 'foo'
const derivedThing =
(propsThing === 'foo' && contextThing === 'bar') ?
'foobar' :
null;
return (
<>
<div>{contextThing}</div>
<div>{stateThing}</div>
<div>{propsThing}</div>
<div>{derivedThing}</div>
<>
}
function preventRerender(prevProps, nextProps) {
if(prevProps.propsThing !== 'foo' }} nextProps.propsThing !== 'foo') {
return true;
}
return false;
}
export default memo(MyComponent, preventRerender);
This successfully prevents the component from re-rendering, because one level up the tree, the MyComponent is wrapped with MyComponentContainer, which uses a custom hook
to drill the props into MyComponent, like so:
const MyComponentContainer = () {
// Note: `useMyCustomHook` uses `useContext`, `useReducer` and `useState`
const { propsThing } = useMyCustomHook();
return (
<>
<Header />
<MyComponent propsThing={propsThing} />
<Footer />
<>
);
}
export default MyComponentContainer;
The Problem
In reference to the areEqual function:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
https://reactjs.org/docs/react-api.html#reactmemo
React docs seem to say this is a bad practice. I'm essentially treating memo/areEqual like a shouldComponentUpdate in a class component.
I don't have control over when propsThing changes. It is driven by both user actions and by pub/sub to a 3rd party API.
Is this an anti-pattern that I need to replace with a different pattern?
Or is React just saying "don't rely on this for features, because we may change how it works?".
Is there are 3rd party solution to better handle this use case?

useState and changes in the props

I'm trying to understand what happens when you have both props and useState in one component.
I wrote little example of it which has one parent component that prints its numbers with another child component -
const MyNumbers = (props) => {
const [numbers, setNumbers] = useState([...props.arr]);
function changeNumbers() {
setNumbers((nums) => [...nums.map(() => Math.floor(Math.random() * 10))]);
}
return (
<div className="MyNumbers">
<div>
<button onClick={changeNumbers}>Chane numbers</button>
</div>
<div>
{numbers.map((num, idx) => (
<SingleNumber key={idx} num={num}></SingleNumber>
))}
</div>
</div>
);
};
const SingleNumber = (props) => {
const [num] = useState(props.num);
useEffect(() => {
console.log("useEffect called");
});
return <h3>The number is {num}</h3>;
};
Here is the above demo
The SingleNumber component uses useState and as you can see clicking on the "Change numbers" action doesn't change the values in the children component.
But when I wrote almost the same code but now SingleNumber doesn't use useState then clicking on the "Change numbers" changes all the values in the children component (like in this demo).
Is it correct to say that a function component with a useState renders once and then only changed if the state changed but not if the props changed ?
OFC the component "rerenders" when the props change, the useEffect hook in SingleNumber is showing you that the "render phase" is run each time the props change.... effects are run each time the component is rendered.
const SingleNumber = (props) => {
const [num] = useState(props.num);
useEffect(() => {
console.log("useEffect called"); // <-- logged each time the component renders
});
return <h3>The number is {num}</h3>;
};
If you added a dependency on props.num and updated the local state (don't actually do this, it's an anti-pattern in react!), you'll see the UI again update each time the props update.
To answer your queston:
Is it correct to say that a function component with a useState renders
once and then only changed if the state changed but not if the props
changed?
No, this is not technically correct to say if "render" to you means strictly react rendered the component to compute a diff, react components rerender when state or props update. Yes, if "render" more generally means you visually see the UI update.
When you call useState it returns an array with two values in it:
The current value of that bit of the state
A function to update the state
If there is no current value when it sets the state to the default value and returns that.
(The default value is the argument you pass to useState).
If you change the values of props in your example, then the component rerenders.
useState returns the current value of that bit of the state. The state has a value, so it doesn't do anything with the argument you pass to useState. It doesn't matter that that value has changed.
Since nothing else has changed in the output, the rerendered component doesn't update the DOM.
Is it correct to say that a function component with a useState renders once and then only changed if the state changed but not if the props changed?
No, it does rerender but doesn't commit the changes.
When parent component MyNumbers re-renders by clicking changeNumbers, by default (unless React.memo used) all its children components (like SingleNumber) will be re-render.
Now when SingleNumber rerenders, notice useState docs.
During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).
You initial the state useState(props.num) but it can only be changed by calling the setter function, therefore the state num won't change because you never calling the setter.
But it will rerender on parent render as mentioned above (notice the useEffect logs).
You don't need to use useState in SingleNumber.
because useState called only once when it rendered.
const SingleNumber = (props) => {
// const [num] = useState(props.num);
// useEffect(() => {
// console.log("useEffect called");
// });
return <h3>The number is {props.num}</h3>;
};
if you want to use useState, you can use like this.
const SingleNumber = (props) => {
const [num, setNum] = useState(props.num);
useEffect(() => {
console.log("useEffect called");
setNum(props.num);
}, [props.num]);
return <h3>The number is {num}</h3>;
};

React: prevent function component from rerender when parents components className changes [duplicate]

When hiddenLogo changes value, the component is re-rendered. I want this component to never re-render, even if its props change. With a class component I could do this by implementing sCU like so:
shouldComponentUpdate() {
return false;
}
But is there a way to do with with React hooks/React memo?
Here's what my component looks like:
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ConnectedSpringLogo from '../../containers/ConnectedSpringLogo';
import { Wrapper, InnerWrapper } from './styles';
import TitleBar from '../../components/TitleBar';
const propTypes = {
showLogo: PropTypes.func.isRequired,
hideLogo: PropTypes.func.isRequired,
hiddenLogo: PropTypes.bool.isRequired
};
const Splash = ({ showLogo, hideLogo, hiddenLogo }) => {
useEffect(() => {
if (hiddenLogo) {
console.log('Logo has been hidden');
}
else {
showLogo();
setTimeout(() => {
hideLogo();
}, 5000);
}
}, [hiddenLogo]);
return (
<Wrapper>
<TitleBar />
<InnerWrapper>
<ConnectedSpringLogo size="100" />
</InnerWrapper>
</Wrapper>
);
};
Splash.propTypes = propTypes;
export default Splash;
As G.aziz said, React.memo functions similarly to pure component. However, you can also adjust its behavior by passing it a function which defines what counts as equal. Basically, this function is shouldComponentUpdate, except you return true if you want it to not render.
const areEqual = (prevProps, nextProps) => true;
const MyComponent = React.memo(props => {
return /*whatever jsx you like */
}, areEqual);
React.memo is same thing as React.PureComponent
You can use it when you don't want to update a component that you think is static so, Same thing as PureCompoment.
For class Components:
class MyComponents extends React.PureCompoment {}
For function Components:
const Mycomponents = React.memo(props => {
return <div> No updates on this component when rendering </div>;
});
So it's just creating a component with React.memo
To verify that your component doesn't render you can just
activate HightlightUpdates in react extension and check your components reaction on
rendering
We can use memo for prevent render in function components for optimization goal only. According React document:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
According to react documentation:- [https://reactjs.org/docs/react-api.html][1]
React. memo is a higher order component. If your component renders the
same result given the same props, you can wrap it in a call to React.
memo for a performance boost in some cases by memoizing the result.
This means that React will skip rendering the component, and reuse the
last rendered result.
For practical understanding I came across these two videos they are very good if you wanna clear concepts also, better to watch so it'll save your time.
Disclaimer:- This is not my YouTube channel.
https://youtu.be/qySZIzZvZOY [ useMemo hook]
https://youtu.be/7TaBhrnPH78 [class based component]

react memo is not getting props

React memo isn't capturing the props neither the prevProps nor the nextProps and the component render well. The react docs say
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost.
my problem is to stop twice rendering using react memo, but memo seems to be not working and the component renders twice with the same props.
The component renders when the Create New Event is clicked on /events
here is the live sandbox.
Child Component located at /components/Event/CreateEvent/CreateEvent.js
the parent component is located at /Pages/Event/Event.js line number 999' from where the child component is being triggered
Here is the Code:
import React from "react";
import AuthContext from "../../context/global-context";
import CreateEvent from "../../components/Event/CreateEvent/CreateEvent";
function Events({ location }) {
// Sate Managing
const [allEvents, setAllEvents] = React.useState([]);
const [creating, setCreating] = React.useState(false);
// Context As State
const { token, email } = React.useContext(AuthContext);
// Creating Event Showing
const modelBoxHandler = () => {
// works on when the ViewEvent is open
if (eventSelected) {
setEventSelected(null);
return;
}
setCreating(!creating);
};
return (
<div className="events">
{/* New Event Creating */}
{creating && (
<CreateEvent onHidder={modelBoxHandler} allEvents={allEvents} />
)}
{console.log("Event Rendered.js =>")}
</div>
);
}
export default React.memo(Events, () => true);
Child Component where the Rect memo doesn't have props:
import React from "react";
import AuthContext from "../../../context/global-context";
function CreateEvent({ onHidder, allEvents }) {
// Context
const { token } = React.useContext(AuthContext);
console.log("CreatedEvent.js REnder");
return (
... Some code here
);
}
export default React.memo(CreateEvent, (prevProps, nextProps) => {
console.log("Hello", prevProps, nextProps);
});
Thanks in advance for your valuable answer and times!
The problem is that on basis of creating variable you are actually remounting and not rendering the CreateEvent component. What it means is that if creating variable changes, the component is unmounted and re-mounted when creating is true, so its not a re-render
Also you must note that modelBoxHandler function reference also changes on each re-render so even if your CreateEvent component is in rendered state and the parent re-rendered due to some reason , the CreateEvent component too will re-render
There are 2 changes that you need to make to make it work better
Define modelBoxHandler with a useCallback hook
perform conditional rendering in createEvent based on creating prop
// Creating Event Showing
const modelBoxHandler = useCallback(() => {
// works on when the ViewEvent is open
if (eventSelected) {
setEventSelected(null);
return;
}
setCreating(prevCreating => !prevCreating);
}, [eventSelected]);
...
return (
<div className="events">
{/* New Event Creating */}
<CreateEvent creating={creating} onHidder={modelBoxHandler} allEvents={allEvents} />
{console.log("Event Rendered.js =>")}
</div>
);
and in createEvent
function CreateEvent({ onHidder, allEvents, creating }) {
// Context
const { token } = React.useContext(AuthContext);
console.log("CreatedEvent.js REnder");
if(!creating) {
return null;
}
return (
... Some code here
);
}
export default React.memo(CreateEvent);
In your example, you don't have an additional render for React.memo to work.
According to your render logic, there aren't any nextProps, you unmount the component with conditional rendering (creating).
// You toggle with `creating` value, there is only single render each time
creating && <CreateEvent onHidder={modelBoxHandler} allEvents={allEvents}/>
// Works, because there will be multiple renders (nextProps)
true && <CreateEvent onHidder={modelBoxHandler} allEvents={allEvents} />
In this case, you might not need React.memo.

React - functions as props causing extra renders

I have some heavy forms that I'm dealing with. Thus, I'm trying to squeeze performance wherever I can find it. Recently I added the Why-did-you-render addon to get more insight on what might be slowing down my pages. I noticed that, for example, when I click on a checkbox component about all of my other components re-render. The justification is always the same. WDYR says
Re-rendered because of props changes: different functions with the
same name {prev onChangeHandler: ƒ} "!==" {next onChangeHandler: ƒ}
As much as possible, I try to respect best the best practices indications that I find. The callback functions that my component passes follow this pattern
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export function TopLevelComponent({props}){
const defaultData = {name: '', useMale: false, useFemale: false}
const [data, setData] = useState(defData);
const { t } = useTranslation();
const updateState = (_attr, _val) => {
const update = {};
update[_attr] = _val;
setData({ ...data, ...update });
}
const updateName = (_v) => updateState('name', _v);//Text input
const updateUseMale = (_v) => updateState('useMale', _v);//checkbox
const updateUseFemale = (_v) => updateState('useFemale', _v);//checkbox
...
return <div>
...
<SomeInputComponent value={data.name} text={t('fullName')} onChangeHandler={updateName} />
<SomeCheckboxComponent value={data.useMale} onChangeHandler={updateUseMale} text={t('useMale')}/>
<SomeCheckboxComponent value={data.useFemale} onChangeHandler={updateUseFemale} text={t('useFemale')}/>
...
</div>
}
In an example like this one, altering any of the inputs (eg: Writing text in the text input or clicking one of the checkboxes) would cause the other 2 components to re-render with the justification presented above.
I guess that I could stop using functional components and utilize the shouldComponentUpdate() function, but functional components do present some advantages that I'd rather keep. How should I write my functions in such a way that interacting with one input does not force an update on another input?
The problem stems from the way you define your change handlers:
const updateName = (_v) => updateState('name', _v)
This line is called on each render and thus, every time your component is rendered, the prop has a new (albeit functionality-wise identical) value. The same holds for every other handler as well.
As an easy solution you can either upgrade your functional component to a fully fledged component and cache the handlers outside of the render function, or you can implement shouldComponentUpdate() in your child components.
You need to use memo for your child components to reduce renders
const SomeInputComponent = props => {
};
export default memo(SomeInputComponent);
// if it still causes rerender witout any prop change then you can use callback to allow or block render
e.f.
function arePropsEqual(prevProps, nextProps) {
return prevProps.name === nextProps.name; // use your logic to determine if props are same or not
}
export default memo(SomeInputComponent, arePropsEqual);
/* One reason for re-render is that `onChange` callback passed to child components is new on each parent render which causes child components to re-render even if you use `momo` because function is updated on each render so in order to fix this, you can use React hook `useCallback` to get the same function reference on each render.
So in you parent component, you need to do something like
*/
import { useCallback } from 'react';
const updateName = useCallback((_v) => updateState('name', _v), [])
You have to memoize parent function before pass to children, using useCallback for functional component or converting to class property if you use class.
export default class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick() {
console.log("click");
}
render() {
return (
<ChildComponent
onClick={ this.onClick }
/>
);
}
}
with useCallback:
Parent = () => {
const onClick = useCallback(
() => console.log('click'),
[]
);
return (
<ChildComponent
onClick={onClick}
/>
);
}

Categories