Reputation: 500
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
Upvotes: 2
Views: 3566
Reputation: 1074148
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);
};
Upvotes: 2
Reputation: 202618
There is no need to memoize the state updater function since React guarantees it to be a stable reference.
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
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),
[],
);
Upvotes: 0
Reputation: 11156
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])
Upvotes: 0
Reputation: 12777
You don't need useCalback
in this case. Just update setState like this:
const onAllRowsSelected = () => {
setSelectAll((preState) => !preState);
};
Upvotes: 0