React sibling component updates state of parent causing re-render - reactjs

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) },[])

Related

Need to calculate on the parent the result of a hook call on each subcomponent

I would love getting some help on this one, I think I am getting there, but I am not sure about it and need some guidance.
I have a parent component, which renders multiple subcomponents, and on each of those subcomponents, I get a result from a hook that do a lot of calculations and other multiple hook calls.
This hook only accepts and a single entity, not arrays, and I cannot afford to modify everything in order to accept arrays in them.
So let's say my parent component is
const Parent = () => {
const cardsArray = [...]
return (
<Wrapper>
{cardsArray.map(
card => <CardComponent cardId={cardId} />
)}
</Wrapper>
)}
and my subComponent :
const CardComponent = ({cardId}) => {
const result = useCalculation(cardId)
return (
<div>My Calculation Result: {result}</div>
)}
Now my issue is this: I need to sum up all those results and show them in my Parent Component. What would be my best way to achieve this?
I thought about having an update function in my parent and pass it as a prop to my subcomponents, but, I am getting the problem that when the Card Subcomponent gets the result from the hook, calls the function and updates the parent state, although it works, I get an error on the console saying that I am performing a state update while rendering:
Cannot update a component (Parent) while rendering a different component (CardComponent). To locate the bad setState() call inside CardComponent, follow the stack trace as described in https://github.com/facebook/react/issues/18178#issuecomment-595846312
I feel like the answer must not be hard but I am not seeing it
thanks a lot
I made some assumptions about your implementation but i think it will cover your needs.
Your thought about having an updater function on the parent element and pass it to it's children sounds pretty good and that's the basic idea of my proposed solution.
So let's start with the Parent component:
const Parent = () => {
const cardsArray = [
{ cardId: 1 },
{ cardId: 2 },
{ cardId: 3 },
{ cardId: 4 }
];
const [sum, setSum] = useState(0);
const addToSum = useCallback(
(result) => {
setSum((prev) => prev + result);
},
[setSum]
);
return (
<div>
{cardsArray.map(({ cardId }) => (
<CardComponent key={cardId} cardId={cardId} addToSum={addToSum} />
))}
<strong>{sum}</strong>
</div>
);
};
I named your updater function addToSum assuming it aggregates and sums the results of the children elements. This function has 2 key characteristics.
It's memoized with a useCallback hook, otherwise it would end up in an update loop since it would be a new object (function) on every render triggering children to update.
It uses callback syntax for updating, in order to make sure it always uses the latest sum.
Then the code of your child CardComponent (along with a naive implementation of useCalculation) would be:
const useCalculation = (id) => {
return { sum: id ** 10 };
};
const CardComponent = memo(({ cardId, addToSum }) => {
const result = useCalculation(cardId);
useEffect(() => {
addToSum(result.sum);
}, [result, addToSum]);
return <div>My Calculation Result: {JSON.stringify(result)}</div>;
});
The key characteristics here are:
the updater function runs on an effect only when result changes (effect dependency).
the addToSum dependency is there to make sure it will always run the correct updater function
it is a memoized component (using memo), since it has expensive calculations and you only want it to update when it's props change.
I assumed that useCalculation returns an object. If it returned a primitive value then things could be a little simpler but this code should work for every case.
You can find a working example in this codesandbox.
Create a state in the parent (sum in the example), and update it from the children in a useEffect block, which happens after rendering is completed:
const { useEffect, useState } = React
const useCalculation = cardId => cardId * 3
const CardComponent = ({ cardId, update }) => {
const result = useCalculation(cardId)
useEffect(() => {
update(result)
}, [result])
return (
<div>My Calculation Result: {result}</div>
)
}
const Parent = ({ cardsArray }) => {
const [sum, setSum] = useState(0);
const updateSum = n => setSum(s => s + n)
return (
<div>
{cardsArray.map(
cardId => <CardComponent key={cardId} cardId={cardId} update={updateSum} />
)}
sum: {sum}
</div>
)
}
ReactDOM.render(
<Parent cardsArray={[1, 2, 3]} />,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>

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.

React useState hook (and useEffect) not working in callback function

Having tried useState, it's functiional variation and attempt with useEffect (not allowed in a callback function).
I am so stuck.
I have a parent 'Table' component, which renders a TableHead child and a TableBody child.
The TableHead child has a checkbox, which when clicked executes a callback function on the parent.
At this point the boolean selectAll (from a useState setter and value), is supposed to toggle (change value).
But it remains in it's initial state.
the result is that the first time the header checkbox for selectall, does fire and the re-render does show all the rows in the body as checked, but then unchecking the 'selectAll' does fire the callback, but the 'selectAll' remains false and all the rows remain checked.
Parent component Code:
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
}
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
How to do it?
Thanks
If I make a couple of reasonable assumptions (for instance, that you have the closing ) on the useCallback call), your code for toggling selectAll works, though from your use of useCallback I suspect it doesn't quite work the way you want it to. Here's your code with those assumptions:
const {useState, useCallback} = React;
const TableContainer = ({children}) => {
return <div>{children}</div>;
};
const Table = ({children}) => {
return <div>{children}</div>;
};
const OTableHead = ({onSelectAllClick, children}) => {
console.log(`OTableHead is rendering`);
return <div>
<input type="button" onClick={onSelectAllClick} value="Select All" />
<div>{children}</div>
</div>;
};
const OTableBody = ({selectAll}) => {
return <div>selectAll = {String(selectAll)}</div>;
};
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
});
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
);
}
ReactDOM.render(
<OTable />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
But some things stand out:
You're not passing any dependency array to useCallback. That doesn't do anything useful, because useCallback will always return the new function you pass it. I suspect you meant to have an empty dependency array on it so that it always reused the first function (to avoid unnecessary re-rendering of OTableHead).
You're using the callback form of setSelectAll, but you're using hardcoded values (true and false). This code:
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
});
does exactly what this code would do (given that we know that selectAll is a boolean to start with, it would be very subtly different if we didn't know that):
const onAllRowsSelected = useCallback(() => {
setSelectAll(!selectAll);
});
because the if uses the version of selectAll that the function closes over, not the parameter the callback received. (setSelectAll(selectAll => selectAll = false); is functionally identical to setSelectAll(() => false), assigning to the parameter doesn't have any effect.) And in turn, that code is the same as this:
const onAllRowsSelected = () => {
setSelectAll(!selectAll);
};
But I suspect you used the callback version for the same reason you used useCallback.
The code doesn't succeed in avoiding having the re-rendering, as you can see from the console.log I added to OTableHead above.
useCallback is useful for avoiding making child elements re-render if the callback hasn't really changed, by memoizing the callback. Here's how you'd use it correctly in that code
Pass an empty dependencies array to useCallback so it only ever returns the first callback you define.
Use the parameter value that the function version of setSelectAll passes your callback.
Ensure that the component you want to have not re-render if the callback didn't change implements checks on its properties and doesn't re-render when they haven't changed. With a function component like OTableHead you can do that just by passing it through React.memo.
Here's the example above with those changes:
const {useState, useCallback} = React;
const TableContainer = ({children}) => {
return <div>{children}</div>;
};
const Table = ({children}) => {
return <div>{children}</div>;
};
// *** Use `React.memo`:
const OTableHead = React.memo(({onSelectAllClick, children}) => {
console.log(`OTableHead is rendering`);
return <div>
<input type="button" onClick={onSelectAllClick} value="Select All" />
<div>{children}</div>
</div>;
});
const OTableBody = ({selectAll}) => {
return <div>selectAll = {String(selectAll)}</div>;
};
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
// Callback version, using the parameter value
setSelectAll(selectAll => !selectAll);
}, []); // <=== Empty dependency array
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
);
}
ReactDOM.render(
<OTable />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
If you aren't worried about unnecessary re-rendering, then you can get rid of useCallback entirely and just do this:
const onAllRowsSelected = () => {
setSelectAll(!selectAll);
};
You don't need useCalback in this case. Just update setState like this:
const onAllRowsSelected = () => {
setSelectAll((preState) => !preState);
};
Yes, keeps the inital value because useCallback is a memoization and, if you don't add the state dependencies, it keeps the initial value (due to the memoization itself). To solve, just put selectAll as useCallback dependencies:
const onAllRowsSelected = useCallback(() => {
setSelectAll((prev) => !prev)
}, [selectAll])
There is no need to memoize the state updater function since React guarantees it to be a stable reference.
useState
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
You can simplify your callback to just update the state using the functional update.
const onAllRowsSelected = () => setSelectAll(all => !all);
If OTableHead requires that onSelectAllClick prop be a stable reference, then the useCallback can be used with an empty dependency array in order to provide a stable onAllRowsSelected callback reference. Note: this doesn't effect the ability for onAllRowsSelected to correctly toggle from the previous state value, it's only to provide a stable callback reference to children components.
useCallback
useCallback will return a memoized version of the callback that only
changes if one of the dependencies has changed. This is useful when
passing callbacks to optimized child components that rely on reference
equality to prevent unnecessary renders (e.g.
shouldComponentUpdate).
const onAllRowsSelected = useCallback(
() => setSelectAll(all => !all),
[],
);

Prevent context.consumer from re-rendering component

I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.

Pass function via props cause useEffect infinite loop if I do not destructure props

I have a parent component with a state. And I want to pass a handler to set some state from a child component.
This is my parent component.
function ParentComponent() {
const [filters, setFilters] = useState({});
const setFiltersHandler = useCallback(filtersObj => {
setFilters(filtersObj);
}, []);
useEffect(() => {
// Do something and pass this to <Content /> component
}, [filters]);
return (
<div>
<Content filters={filters}>
<SideBarFilters applyFilters={setFiltersHandler} />
</div>
);
}
And this is my child component. This causes infinit loop.
const SideBarFilters = props => {
const [filterForm, setFilterForm] = useState({
specialities: {value: "all"}
});
// Some code with a input select and the handler to set filterForm
useEffect(() => {
let filterObj = {};
for (let key in orderForm) {
filterObj = updateObject(filterObj, {
[key]: orderForm[key]["value"]
});
}
props.applyFilters(filterObj);
}, [props, orderForm]);
return <OtherComponent />;
};
But if I destructure the props, it does not loop. Like this
const SideBarFilters = ({applyFilters}) => {
// same code as before
useEffect(() => {
// same as before
applyFilters(filterObj);
}, [applyFilters, orderForm]);
return <OtherComponent />;
};
My guess is that has something to do with how React compare props.
Maybe I should memo all props. But I think that is not a pattern
props object is referentially different each time parent re-renders(and re-renders SideBarFilters).
You should not fight that. Trying to find workaround you may run into brand new issues with stale date.
Destructure as you do, it's expected and suggested way to deal with dependencies in hooks.

Resources