'React: Issue with fetching and updating the state in useCallback

I am currently working on a component that makes an API call, retrieves the data, and then displays the data in the Fluent UI Datalist.

The issue is as follows: The component loads for the first time, then it re-renders after the API call, and the component shows the correct entries within the table with the state.items being set to correct value. However, when I click on column to run the onColumnClick the items inside the function are empty, which result in an error. The columns are fine, but the state.items is just an empty collection.

How can this possibly be fixed to so that I see the items within the onColumnClick?

Here is a piece of code:

export const ListComponent = (props: ListComponentProps) => {

    const fetchPeople = async () => {
        const entry: ITableEntry[] = [];

        //items ... sdk call

        for await (const item of items) {
            entry.push({
                key: item.id,
                name: item.name,
                lastName: item.lastname
            });
        }
    }

    useEffect(() => {
        fetchPeople();
        .then(elementList => {
            setState(
                state => ({ ...state, items: elementList }),
            );
        });
    }, [])

    const onColumnClick = React.useCallback((ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
        
        const columns = state.columns;
        const items = state.items;
        // PLACE WHERE THE ERROR HAPPENS
        console.log(items);
    }, []);


    const columns: IColumn[] = [
        {
            key: 'column1',
            name: 'First Name',
            fieldName: 'name',
            minWidth: 210,
            maxWidth: 350,
            isRowHeader: true,
            isResizable: true,
            isSorted: true,
            isSortedDescending: false,
            sortAscendingAriaLabel: 'Sorted A to Z',
            sortDescendingAriaLabel: 'Sorted Z to A',
            onColumnClick: onColumnClick,
            data: 'string',
            isPadded: true,
        },
        {
            key: 'column2',
            name: 'Last Name',
            fieldName: 'lastname',
            minWidth: 210,
            maxWidth: 350,
            isRowHeader: true,
            isResizable: true,
            isSorted: true,
            isSortedDescending: false,
            sortAscendingAriaLabel: 'Sorted A to Z',
            sortDescendingAriaLabel: 'Sorted Z to A',
            onColumnClick: onColumnClick,
            data: 'string',
            isPadded: true,
        },
    ];

    const [state, setState] = React.useState({
        items: [] as ITableEntry[],
        columns: columns,
    });

    return (
        <>
            <DetailsList
                items={state.items}
                columns={state.columns}
            />
        </>
    );
});


Solution 1:[1]

const onColumnClick = React.useCallback((ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {   
     const columns = state.columns;
     const items = state.items;
     // PLACE WHERE THE ERROR HAPPENS
     console.log(items);
}, [state]);

add dependency to the use callback to be recalculated when state changes

Solution 2:[2]

This is a total rewrite with some notes

        import React, {useCallback, useEffect, useState} from "react";

/** Option One if the function does not requires variables from the component 
 * itself you can put it outside like in "api" folder */
const fetchPeople = async () => {
    //items ... sdk call
    
    // if items are already calculated and they are not async
    return items.map((item)=>({
        key: item.id,
        name: item.name,
        lastName: item.lastname
    }))

    // else 
    // return (await Promise.all(items)).map((item)=>({
    //     key: item.id,
    //     name: item.name,
    //     lastName: item.lastname
    // }))
}

export const ListComponent = (props: ListComponentProps) => {

    const [items, setItems] = useState<ITableEntry[]>([])

    // Option Two: use callback this function is "saved" inside a variable with a memoization based on the 
    // elements inside the array at the end
    // const fetchPeople = useCallback(async () => {
    //     ...
    // }, [])


    useEffect(() => {

        // option three you can also leave it there so it can be used in other part of the application 
        // const fetchPeople = async () => {
        //     ...
        // }

        // if you like async await toy can run this
        (async () => {
            setItems(await fetchPeople())
        })()

        /** if this is not modifiable you don't need to put it there 
         * and this function will run after the component is "mount" 
         * in my case fetch people will not change and that is why you should use useCallback
        */
    }, [fetchPeople]);

    const onColumnClick = useCallback((ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
        console.log(items);
    }, [items]);

    const columns = [
        {
            key: 'column1',
            name: 'First Name',
            fieldName: 'name',
            minWidth: 210,
            maxWidth: 350,
            isRowHeader: true,
            isResizable: true,
            isSorted: true,
            isSortedDescending: false,
            sortAscendingAriaLabel: 'Sorted A to Z',
            sortDescendingAriaLabel: 'Sorted Z to A',
            onColumnClick: onColumnClick,
            data: 'string',
            isPadded: true,
        },
        {
            key: 'column2',
            name: 'Last Name',
            fieldName: 'lastname',
            minWidth: 210,
            maxWidth: 350,
            isRowHeader: true,
            isResizable: true,
            isSorted: true,
            isSortedDescending: false,
            sortAscendingAriaLabel: 'Sorted A to Z',
            sortDescendingAriaLabel: 'Sorted Z to A',
            onColumnClick: onColumnClick,
            data: 'string',
            isPadded: true,
        },
    ]

    return (
        <>
            <DetailsList
                items={items}
                columns={columns}
            />
        </>
    );
});

keep variables as simple as possible and unless something strange is required just save "datas" in State

Solution 3:[3]

Here is a fix that actually makes this work!

So I actually found a similar post to my issue (although I have searched for it for ages before):

React - function does not print current states

However, the solution had to be modified to this to reflect the changes in the columns. The solution always also refreshes columns upon changes to items (see useEffects, where I set the columns), so the columns are being updated.

export const ListComponent = (props: ListComponentProps) => {

    
    const [state, setState] = React.useState({
        items: [] as IDocument[],
        columns: [] as IColumn[],
      });

    const fetchPeople = React.useCallback(async () => {
        const entry: ITableEntry[] = [];

        //items ... sdk call

        for await (const item of items) {
            entry.push({
                key: item.id,
                name: item.name,
                lastName: item.lastname
            });
        }
  
        setState((state) => ({ ...state, items: elementsList }));
      }, []);

    useEffect(() => {
        setState((state) => ({ ...state, columns: columns }));
      }, [state.items]);

      
    useEffect(() => {
        fetchPeople();
    }, []);

    const _onColumnClick = React.useCallback((ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
        
        const columns = state.columns;
        const items = state.items;
        console.log(items);
    }, [state.items, state.columns]);


    const columns: IColumn[] = [
        {
            key: 'column1',
            name: 'First Name',
            fieldName: 'name',
            minWidth: 210,
            maxWidth: 350,
            isRowHeader: true,
            isResizable: true,
            isSorted: true,
            isSortedDescending: false,
            sortAscendingAriaLabel: 'Sorted A to Z',
            sortDescendingAriaLabel: 'Sorted Z to A',
            onColumnClick: _onColumnClick,
            data: 'string',
            isPadded: true,
        },
        {
            key: 'column2',
            name: 'Last Name',
            fieldName: 'lastname',
            minWidth: 210,
            maxWidth: 350,
            isRowHeader: true,
            isResizable: true,
            isSorted: true,
            isSortedDescending: false,
            sortAscendingAriaLabel: 'Sorted A to Z',
            sortDescendingAriaLabel: 'Sorted Z to A',
            onColumnClick: _onColumnClick,
            data: 'string',
            isPadded: true,
        },
    ];

    return (
        <>
            <DetailsList
                items={state.items}
                columns={state.columns}
            />
        </>
    );
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Matteo Bombelli
Solution 2 Matteo Bombelli
Solution 3 Johhny Bravo