is there any benefit of using useCallback without React.memo? - javascript

From what i understood from React documentation and other material across web, useCallback is used to avoid re-rendering of child component by ensuring that memoized version of callback is passed to it, thus referentially props remain same for child component. But all this is valid only if I am using React.memo on child component. Without React.memo, child component would re-render anyways. My question is what use is useCallback in this case i.e. without React.memo applied to child component. What are the other benefits of useCallback?

React.memo ensures that a shallow comparison is performed when props enter a component and skips rendering of the component when they are equal.
Given a child component Cell:
When applied to a function component that is created during the render of another, parent component, a new Cell component will be created on each render. This new Cell component will always make shallow comparisons on its props but it will be re-rendered on every render of its parent.
useCallback will however memoize this function callback Cell if it's dependency array does not change during a parent re-render.
Alone a function component Cell wrapped in a useCallback will always re-render when it receives props, which will happen on every render of its parent.
The difference however is that it's entire subtree is re-rendered if the component itself is recreated, as is the case when using React.memo by itself.
Paired together you can however get around re-rendering components defined inside of a parent.
As you will notice, when attempting to drag the Memoized cell, the one not wrapped in useCallback, no dragging happens. That is because the original element you attempt to drag is recreated as a new instance when it's parent re-renders.
This concept is explained in more detail here
Example
const { useCallback, useState, useRef } = React;
function App() {
const [state, setState] = useState(false);
const [title, setTitle] = useState("");
const Cell = useCallback(({ title }) => {
console.log(`${title} rendering`);
function onDragStart() {
setState(true);
}
function onDragEnd() {
setState(false);
}
return (
<div draggable onDragStart={onDragStart} onDragEnd={onDragEnd}>
Drag {title}
</div>
);
}, []);
const MemoizedCell = React.memo(({ title }) => {
console.log(`${title} rendering`);
function onDragStart() {
setState(true);
}
function onDragEnd() {
setState(false);
}
return (
<div draggable onDragStart={onDragStart} onDragEnd={onDragEnd}>
Drag {title}
</div>
);
});
const MemoizedCallbackCell = useCallback(
React.memo(({ title }) => {
console.log(`${title} rendering`);
function onDragStart() {
setState(true);
}
function onDragEnd() {
setState(false);
}
return (
<div draggable onDragStart={onDragStart} onDragEnd={onDragEnd}>
Drag {title}
<button onClick={() => setTitle(" Updated")}>Change Title</button>
</div>
);
}),
[]
);
return (
<div className="App">
<Cell title="Cell" />
<MemoizedCell title="Memoized Cell" />
<MemoizedCallbackCell title={"Memoized Callback Cell" + title} />
Is dragging {`${state}`}
<br />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));

You might also need to pass a function to useEffect without changing useEffect on every render
For example:
import { useCallback, useEffect } from "react";
function useExample() {
function fn() {
console.log("a function");
}
const callback = useCallback(fn, []);
useEffect(() => {
callback();
}, [callback]);
}
Real-world example with useDebounce:

Related

Unable to style component using forwardRef

I am fairly new to react, so still getting my head around the component's lifecycle.
But the problem has left me scratching my head.
For instance, I do not understand why adding "setState(10);" causes style of the "Test" component to revert to it's default value yet the <div ref={ref2}>Hi</div> maintains it's style. (see imagebelow)
I am aware that "setState(10);" will cause a re-render but why is the style of the "Test" component being reverted?
Also, please ignore the "practical use" of calling setState(10) - I am aware it is pointless as it is never used, and I am aware that using "state" as a UseEffect dependency can solve this issue. But the main issue I have is understanding why the component's style reverts to it's default value.
import React, { useEffect, useState, useRef } from "react";
export default function App() {
const [state, setState] = useState();
let ref1 = useRef();
let ref2 = useRef();
useEffect(() => {
console.log("useEffect called ", ref1.current);
ref1.current.style.backgroundColor = "red";
ref2.current.style.backgroundColor = "green";
setState(10);
// }, [state]);
}, []);
const Test = React.forwardRef((props, ref1) => {
console.log("test called - rendering webpage", ref1.current);
return (
<div ref={ref1} {...props}>
HI from Test{" "}
</div>
);
});
return (
<div className="App">
<Test ref={ref1} />
<div ref={ref2}>Hi</div>
</div>
);
}
Console output
test called - rendering webpage undefined
useEffect called <div style="background-color: red;">HI </div>
test called - rendering webpage <div style="background-color: red;">HI </div>
The reason the style is disappearing is that you've defined your Test component inside your App component. That means that every time App renders, you'll define a new component type named Test. The text of that component is identical to the previous one, but it's a new type as far as react can tell, so react is forced to unmount the old one, and mount the new one. This wipes out any changes you made to the old one.
So at the very least, you need to move Test outside of App. That way, the component is just defined once, and will not remount on every render
export default App() {
// ...
}
const Test = React.forwardRef((props, ref1) => {
// ...
})
The above should fix the reset and let you experiment with refs, but i strongly recommend that you do not use refs to style your elements. Refs are an escape hatch that's sometimes needed, but the standard way to style a component is through the style prop. If you need to change the style, then you can have a state variable and let that control the style prop.
If you manually use javascript to set ref1.current.style.backgroundColor, react has no way to know that you did this, and so can't take those changes into account. In some circumstances, react may end up overwriting your changes, or may skip making changes that it doesn't realize it needs to do.
export default function App () {
const [colored, setColored] = useState(false);
useEffect(() => {
setColored(true);
}, [])
return (
<div className="App">
<Test style={colored ? { backgroundColor: "green" } : undefined} />
<div style={colored ? { backgroundColor: "red" } : undefined}>Hi</div>
</div>
);
}
// Don't really need forwardRef anymore, but i left it in
const Test = React.forwardRef((props, ref) => {
return (
<div ref={ref} {...props}>
HI from Test
</div>
);
});
import React, { useEffect, useState, useRef } from "react";
export default function App() {
const [state, setState] = useState();
let ref1 = useRef();
let ref2 = useRef();
useEffect(() => {
console.log("useEffect called ", ref1.current);
ref1.current.style.backgroundColor = "red";
ref2.current.style.backgroundColor = "green";
setState(10);
// }, [ref.current]);
}, [state]);
const Test = React.forwardRef((props, ref1) => {
console.log("test called - rendering webpage", ref1.current);
return (
<div ref={ref1} {...props}>
HI from Test{" "}
</div>
);
});
return (
<div className="App">
<Test ref={ref1} />
<div ref={ref2}>Hi</div>
</div>
);
}
The reason this is happening is once you update the state entire component gets rerendered. Your useEffect will run only once on componentDidMount hence the new ref that you get is not updated. To get rid of this you should use state as a dependency of the useEffect.

Logic of rerendering in react function components

I have a simple App component
export default function App() {
const [count, changeCount] = useState(0)
const onIncreaseClick = useCallback(() => {
changeCount(count + 1)
}, [count])
const onPress = useCallback(() => {
alert('pressed')
}, [])
return (<>
<button onClick={onIncreaseClick}>Increase</button>
<ButtonPressMe onClick={onPress} />
</>);
}
I expect that onPress variable contains always the same link since parameters never change
And i expect that my ButtonPressMe component will be rendered just once - with the first App component rendering... because it has just one prop and value of this prop never change... therefore no need to rerender component. Correct?
Inside my ButtonPressMe component i check it with console.log
const ButtonPressMe = ({ onClick }) => {
console.log('Button press Me render')
return <button onClick={onClick}>Press me</button>
}
And against my expectations it rerenders each time when parent component rerenders after Increase button is pressed.
Did i misunderstood something?
sandbox to check
And against my expectations it rerenders each time when parent component rerenders after Increase button is pressed.
Did i misunderstood something?
That's the default behavior in react: when a component renders, all of its children render too. If you want the component to compare its old and new props and skip rendering if they didn't change, you need to add React.memo to the child:
const ButtonPressMe = React.memo(({ onClick }) => {
return <button onClick={onClick}>Press me</button>
})
By default, when a parent component re-renders, all of its child components re-render too.
useCallback hook will preserve the identity of the onPress function but that won't prevent a re-render of the ButtonPressMe component. To prevent a re-render, React.memo() is used. useCallback hook is used to avoid passing a new reference to a function, as a prop to a child component, each time a parent component re-renders.
In your case, combination of React.memo and useCallback hook will prevent a re-render of ButtonPressMe component.
function App() {
const [count, changeCount] = React.useState(0);
const onIncreaseClick = React.useCallback(() => {
changeCount(count + 1);
}, [count]);
const onPress = React.useCallback(() => {
alert("pressed");
}, []);
return (
<div>
<button onClick={onIncreaseClick}>Increase</button>
<ButtonPressMe onClick={onPress} />
</div>
);
}
const ButtonPressMe = React.memo(({ onClick }) => {
console.log("Button press Me render");
return <button onClick={onClick}>Press me</button>;
});
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The default behavior in React is to change everything in the App when anything changes, in your case you're changing the state of the parent of your custom button, therefore React re-renders everything including your button.
You can find an explanation on how React decides to re-render components here:
https://lucybain.com/blog/2017/react-js-when-to-rerender/#:~:text=A%20re%2Drender%20can%20only,should%20re%2Drender%20the%20component.

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 does React.useCallback trigger rerender, thouh it should not?

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)

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