I have two React functional components Parent and Node. Parent renders list of Node components and passes click handler to each Node. When I click on the Node, in the Parent the new state is set and all components rerenders.
const Parent = (props) => {
const [clickedList, setClickedList] = useState([]);
const onNodeClick = (node) => {
let newList = [...clickedList, node];
setClickedList(newList);
}
return (
<div>
{someNodes.map(node => <Node key={node.id} node={node} onNodeClick={props.onNodeClick}/>)}
</div>
)
}
const Node = (props) => {
const handleClick = (node) => {
props.onNodeClick(node);
}
return (
<div onClick={() => handleClick(node)}>
{props.node.name}
</div>
)
}
The list of nodes contains more than 400 items. Rerendering of each component takes time. I tried to use React.memo for Node component, but the feature is that I pass the function to Node, which has different references after each rerendering of the Parent and React.memo component rerenders too. Also I tried to provide a custom comparison function for React.memo where I skip comparsion of passed function. The result is components do not rerender, but component state clickedList does not change in the click handler in the Parent.
I also tried to use useCallback hook on the click handler in the Parent, the result is similar. If I give no dependencies [] then component state clickedList does not change.
const memorizedNodeClick = useCallback(
(node) => {
onNodeClick(node);
}, [],
);
If I give state in dependencies [clickedList] then all more than 400 components rerenders
const memorizedNodeClick = useCallback(
(node) => {
onNodeClick(node);
}, [clickedList],
);
How can I prevent unnecessary component rendering and keep all clicked items in the clickedList state?
Related
how to stop re-rendering of child component if parent update the context or state in react js ?
I am already using React.memo still it is re-rendering.
this is my child component
const Ab = () => {
console.log("---ff-ddd");
const pageContext = useContext(PageContext);
useEffect(() => {
setTimeout(() => {
pageContext.updateGlobalMenu({});
}, 5000);
}, []);
return <div>ddd</div>;
};
export default React.memo(Ab);
I am updating the context. I want it update the context value but not re-render the child component
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = (menuContent) => {
setGlobalMenu(menuContent);
};
return (
<PageContext.Provider
value={{
updateGlobalMenu
}}
>
<Ab />
</PageContext.Provider>
);
}
here is my code
https://codesandbox.io/s/friendly-bartik-3cnqvf?file=/pages/index.js:156-470
if you see it print two times console. it means it is re-rendering two times
If PageContext's value changes, then any component consuming that context (including Ab) will render. React.memo cannot stop this. In your case you're changing the value on every render, so you have room to improve it by memoizing the context value. That way, the value won't change unless it needs to:
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = useCallback((menuContent) => {
setGlobalMenu(menuContent);
}, []);
const value = useMemo(() => ({
updateGlobalMenu
}), [updateGlobalMenu]);
return (
<PageContext.Provider value={value}>
<Ab />
</PageContext.Provider>
);
}
You can also, in addition to memoisation from the previous answer, split your "api" and "data" portion of the state into two different providers.
updateGlobalMenu will be in PageContextAPI provider, globalMenu will be in PageContextData provider. That way when you update the data, only the provider with the data will be re-rendered.
Take a look at this article, I covered this technique in detail here:https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context
Consider example:
const Child = () => {
console.log("I did re-render!");
return null;
};
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
<Child fn={fn} />
</div>
);
};
With every state change in App (click the button), the Child component re-renders, even if the passed prop fn is wrapped with useCallback. However, if I wrap Child with React.memo, it starts to work correctly - it does not re-render when parent re-renders.
My question: What's the point of using useCallbacks without React.memo?? Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Playground: https://codesandbox.io/s/peaceful-khorana-nrojpb?file=/src/App.js
My question: What's the point of using useCallbacks without React.memo??
There is none unless you otherwise tell React to compare based on refernece down the line.
Should I always use React.memo if I dont want the component to always re-render if its parent re-renders?
Yes, or something equivalent.
Should useCallbacks always be used with React.memo? Because it seems like they are senseless and useless without React.memo.
Yes, unless you do something equivalent.
Just to elaborate - other than React.memo you can always wrap a sub-render with useMemo:
const App = () => {
const [_, setValue] = useState();
const fn = useCallback(() => {
// do something
}, []);
const child = useMemo(() => <Child fn={fn} />, [fn]);
return (
<div>
<button onClick={() => setValue(Math.random)}>click</button>
{child}
</div>
);
};
Or "roll your own" with useRef+useEffect or use class components and override shouldComponentUpdate or inherit from React.PureComponent.
I want to avoid re-render of my child component <ChildComponent/> whenever I update my state using a onClick in <ChildComponent/>.
I have my callback function in <ParentComponent/> which updates one of the values for the key-value pair object.
In the parent component
const _keyValueObject = useMemo(() => utilityFunction(array, object), [array, object])
const [keyValueObject, setKeyValueObject] = useState<SomeTransport>(_keyValueObject)
const handleStateChange = useCallback((id: number) => {
setKeyValueObject(keyValueObject => {
const temp = { ... keyValueObject }
keyValueObject[id].isChecked = ! keyValueObject[id].isChecked
return temp
})
}, [])
return(
<Container>
{!! keyValueObject &&
Object.values(keyValueObject).map(value => (
<ValueItem
key={value.id}
category={value}
handleStateChange ={handleStateChange}
/>
))}
</Container>
)
In child component ValueItem
const clickHandler = useCallback(
event => {
event.preventDefault()
event.stopPropagation()
handleStateChange(value.id)
},
[handleStateChange, value.id],
)
return (
<Container>
<CheckBox checked={value.isChecked} onClick={clickHandler}>
{value.isChecked && <Icon as={CheckboxCheckedIcon as AnyStyledComponent} />}
</CheckBox>
<CategoryItem key={value.id}>{value.title}</CategoryItem>
</Container>
)
export default ValueItem
In child component if I use export default memo(ValueItem), then the checkbox does not get updated on the click.
What I need now is to not re-render every child component, but keeping in mind that the checkbox works. Any suggestions?
Spreading (const temp = { ... keyValueObject }) doesn't deep clone the object as you might think. So while keyValueObject will have a new reference, it's object values will not be cloned, so will have the same reference, so memo will think nothing changes when comparing the category prop.
Solution: make sure you create a new value for the keyValueObject's id which you want to update. Example: setKeyValueObject(keyValueObject => ({...keyValueObject, [id]: {...keyValueObject[id], isChecked: !keyValueObject[id].isChecked})). Now keyValueObject[id] is a new object/reference, so memo will see that and render your component. It will not render the other children since their references stay the same.
Working Codesandbox
Explanation
What you need to do is wrap the child with React.memo. This way you ensure that Child is memoized and doesn't re-render unnecessarily. However, that is not enough.
In parent, handleStateChange is getting a new reference on every render, therefore it makes the parent render. If the parent renders, all the children will re-render. Wrapping the handleStateChange with useCallback makes sure react component remembers the reference to the function. And memo remembers the result for Child.
Useful resource
const Parent = () => {
const [thing, setThing] = useState('a string');
// code to update thing
return <Child thing={thing} />
}
const Child = props => {
return <div>I want {props.thing} to be initial value without updating</div>
}
If I want 'thing' to be passed from parent to child but not update when parent changes it, how do I accomplish this?
I've tried useEffect, tried cloning 'thing' to a constant within Child...
I would use useEffect with the empty [] for dependencies, so that it only runs once.
From Reactjs.org:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
const Child = props => {
let thing = props.thing;
let [initialValue, setInitialValue] = useState("");
useEffect(() => {
setInitialValue(thing);
}, []);
return (
<div>
I want <strong>{initialValue}</strong> to be initial value without
updating
</div>
);
};
CodeSandbox
Maybe you can try setting the thing to a variable in the child component when it's null. So when the parent update, you don't have to update the child.
let childThing = null;
const Child = props => {
if(childThing === null)
childThing = props.thing
return <div>I want {childThing} to be initial value without updating</div>
}
I am using Hooks + Render Props in my Preact/React application. Instead of passing simple state to the render(), I am passing a component function that can be used by the user of this component.
Also, my component is Hybrid in a sense that that SortDropdown is not UI-Less like we typically have with Render Prop components. It provides some basic UI in the form of anchor element which triggers the actual drop down and shows the custom content via Render Props render function. This is how I have implemented it:
export function SortDropdown(props: SortDropdownProps) {
const { render, onSelect, value } = props;
const anchor = (ref: Ref<Element>, open: VoidFunction) => (
<SortButton buttonElm={ref} onClick={open} />
);
const surfaceContent = (ref: Ref<any>, select: SortSelectFn) => {
// NESTED INNER COMPONENT - IS IT RIGHT?
function SortMenuItem(props: SortMenuItemProps) {
const { children, field } = props;
// THE HOOK IN QUESTION
const [someState, setSomeState] = useState(false);
const isSelected = value.key === field.key;
const onClick = () => select(field);
return (
<SortMenuButton canTrigger={someState} selected={someState} onClick={onClick}>
{children}
</SortMenuButton>
);
}
return (
<div ref={ref}>{render(SortMenuItem, select)}</div>
);
};
return (
<BaseDropdown anchor={anchor} surface={surfaceContent} onSelect={onSelect as any} />
);
}
Here I have two questions. First, does defining an inner nested component, here - SortMenuItem which is then passed to the render function violates the rules of Hooks? The component may be called n-times or may not be used at all by the calling component.
Second, is it a right practice to define a nested Higher-Order Component, here - SortMenuItem being an abstraction over SortMenuButton?