0

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}
            />
        </>
    );
});
Johhny Bravo
  • 199
  • 3
  • 15
  • any specific reason for using useCallback? I think that callback has a depency on items right, you should add that in the depency list. – hazimdikenli Jan 28 '22 at 13:14
  • I have replied to your comment in @Matteo Bombelli answer as you seem to have this comment in two places – Johhny Bravo Jan 28 '22 at 13:46
  • Okay, I see the reason you are using a callback, otherwise you have to pass in the items, or the state, to the column click handler. You could also do something like clickHandler(column, items), and on column you would then have to call it like onColumnClick: (event) => clickHandler(column, items, event)... etc, but what you have looks fine now:) – hazimdikenli Jan 28 '22 at 14:49
  • So I just tested something out: I created another div with a similar onClick event handler (I removed the column parameter) and printed out the same elements. And they were all correct, so I am not sure whether there isn't some memorization being done inside. – Johhny Bravo Jan 28 '22 at 18:04

3 Answers3

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

Matteo Bombelli
  • 430
  • 5
  • 11
  • I second this. And I think you do not need the useCallback. – hazimdikenli Jan 28 '22 at 13:16
  • This would be fine, and I have tried, but there is an issue with this. When I add the state, I get an error `Block-scoped variable 'state' used before its declaration.` So I tried to move the state to the top, but then this requires columns variable to move to the top, which in turn then yells `Block-scoped variable 'onColumnClick' used before its declaration`. And there is this cycle that I am not quite able to solve – Johhny Bravo Jan 28 '22 at 13:24
  • @hazimdikenli I also tried it without the useCallback, but the error is still the same – Johhny Bravo Jan 28 '22 at 13:27
  • @hazimdikenli with functional components useCallback is quite important: just look at the sintax: normally when a component renders (for whatever reason) all the render function will run and in this case the function will be redefined and since it is not a basic type this will mean that eventual useEffect that will use the function will rerun even if this mostly not what we wanted. using the hooks it is like using methods inside classes instead of defining methods in class components and have "similar" results. – Matteo Bombelli Jan 28 '22 at 18:16
0

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

Matteo Bombelli
  • 430
  • 5
  • 11
  • So I just verified this, and indeed this actually still produces an empty list. I think the issue is actually within the DetailsList. You can usee my comment under OP, where I mention that I have created a div with a similar onClick and the items are produced correctly. It seems to be a behavior of DetailsList – Johhny Bravo Jan 28 '22 at 21:04
0

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}
            />
        </>
    );
});
Johhny Bravo
  • 199
  • 3
  • 15