Neal Rogers
Neal Rogers

Reputation: 500

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

Upvotes: 2

Views: 3566

Answers (4)

T.J. Crowder
T.J. Crowder

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:

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

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

  1. Pass an empty dependencies array to useCallback so it only ever returns the first callback you define.

  2. Use the parameter value that the function version of setSelectAll passes your callback.

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

Drew Reese
Drew Reese

Reputation: 202618

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

Upvotes: 0

Giovanni Esposito
Giovanni Esposito

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

Viet
Viet

Reputation: 12777

You don't need useCalback in this case. Just update setState like this:

const onAllRowsSelected = () => {
  setSelectAll((preState) => !preState);
};

Upvotes: 0

Related Questions