Why does React.useCallback trigger rerender, thouh it should not? - javascript

I have redux connected Component with onClick action bound to it. Every time I click it rerenders, though I use useCallback hook. Here is my simplified component:
const Map = props => {
const dispatch = useDispatch(); // from react-redux
const coordinates = useSelector(state => state.track.coordinates); // from react-redux
const onClick = useCallback( // from react
data => {
return dispatch({type: 'ADD_COORDINATES', payload: data});
},
[dispatch]
);
return (
<div className="Map">
<GoogleMap
onClick={onClick}>
<Track
coordinates={coordinates}
/>
</GoogleMap>
</div>
);
};

Without giving any additional context, and that the component is really "simplified" (there is nothing else that may cause a render), Map component will re-render only on its parent render:
const Parent = () => {
const coordinates = useSelector(coordinatesSelector);
return <Map />;
};
On dispatching addCoordinates action you may trigger its parent.
You should try and memoize the Map component:
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 in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
const Map = () => {
...
return ....;
};
export default React.memo(Map);
Edit after question update:
Your component re-renders due to useSelector as stated in the docs:
When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
Therefore, you might want to add additional equalityFn:
const coordinates = useSelector(state => state.track.coordinates, areSameCoords)

Related

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 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.

Why useDispatch re-renders parent components?

I'm using useDispatch hook (from Redux) in onSelect callback in the Tree component (from Ant library):
export const MyComponent = () => {
const dispatch = useDispatch();
const onSelect = (selectedNode) => {
const selectedItem = selectedNode[0];
dispatch(fetchSelectedItems(selectedItem));
};
return
<Tree
onSelect={onSelect}
>
<TreeNode .. />
<TreeNode .. />
<TreeNode .. />
</Tree
}
export const fetchSelectedItems = (selected: string) =>
(dispatch) =>
axios({
url: `/api/items?id=${selected}`,
method: 'GET',
}).then(response => {
dispatch(fetchSelectedItemsSuccess(response.data))
}).catch((error: any) => {throw(error)});
Why does useDispatch re-render parent components? Is there any way to prevent from this? I tried useCallback like it's in Redux documentation but this solution is to prevent child components from re-rendering, not parents.
It looks like my assumption in the comment was correct.
So I will show you the workaround.
You can extract the part that uses clickValue in the container to another component, say ClickValue.
Doing so will isolate the update to ClickValue component only.
My fork: https://codesandbox.io/s/soanswer60515755-9cc7u
function ClickValue() {
const clickValue = useSelector(state => state.value);
console.log(clickValue);
return clickValue;
}
export default function Container() {
return (
<div className="Container">
<h3>Container</h3>
<ParentComponent />
<ClickValue />
</div>
);
}
Check out the profile result below.
I would think that on every render you are redeclaring the onSelect function. Functions are reference types. Passing that redeclared function with its new reference on ever render will cause a rerender. Perhaps you should look into using context.
My problem with re-rendering components was caused by useSelector used in parent components where I directly refer to state. Most probably because of new result of this selector..
Solution:
I rewrote this selectors with reselect library to make them memoized (it was suggested in one of comments here but I don't know why its've been removed). I did exactly what is in redux documentation about memoized selectors.

When storing a component in state, it no longer re-renders when its props are changed?

In a particular business case, I have a series of components that are laid out in a linear fashion, however the order of which may change. For example, we have a parent component:
export const Component = () => {
const [data, setData] = useState(null);
const [nextComponent, setNextComponent] = useState(null);
return (
//... component code here
{nextComponent}
);
}
Inside the Component code would be buttons that can change which component is to come next:
onClick={() => setNextComponent(<SomeComponent data={data} setData={setData} />)}
If data changes by calling setData within SomeComponent, the component is not re-rendering with the new data prop value.
I understand this is something to do with the SomeComponent being stored in state, but can not understand why data does not update in SomeComponent - surely data is just a pointer to a memory location, and should therefore have the same value across the parent and child?
This is because the data prop is only updated for SomeComponent when the function setNextComponent is called as part of your onClick callback.
The "right way" to do this is to hold the data that matters to SomeComponent in its parent's state, then pass it down as part of a conditional render of that child.
const ChildComponent = ({ data, setData }) => (
<React.Fragment>
<span>Current Data: {JSON.stringify(data)}</span>
<input onChange={({target}) => setData(target.value)} placeholder="Enter New Data" />
</React.Fragment>
);
const ParentComponent = () => {
const CHILD_TYPES = { signIn: SomeComponent, signOut: OtherComponent };
// keep track which component you want to be showing based on parent state
const [activeComponentType, setActiveComponentType] = useState('signIn');
// keep track of the data required for the child component(s)
const [data, setData] = useState(null);
// find and use the correct class for the current child type
// (you could do this without an extra variable by using `React.createClass`)
const ActiveComponentClass = CHILD_TYPES[activeComponentType];
return (<ActiveComponentClass data={data} setData={setData} />);
}
Also, you should consider not storing components in state. It's bad practice and usually won't work the way you expect.

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