1

The source code is uploaded in codesandbox.

It is a DetailsList, when clicking column header, the list disappears. The reason is that columns is [] in onColumnClick function (there is a console.log line).

Why setColumns updated state successfully but cannot be retrieved in onColumnClick function

UPDATE: I think I found the reason, it is about dependency supplied to useCallback and useEffects. Still dont find a way to remove cyclic dependency

import React, { useState, useEffect, useCallback } from "react";
import {
    DetailsListLayoutMode,
    SelectionMode,
    IColumn
} from "@fluentui/react/lib/DetailsList";
import { ShimmeredDetailsList } from "@fluentui/react/lib/ShimmeredDetailsList";

export interface IPod {
    name: string;
    status: string;
    age: number;
}

export interface IPodListState {
    pods: IPod[];
}

const PodList: React.FunctionComponent = () => {
    const [pods, setPods] = useState<IPod[] | undefined>(undefined);
    const [columns, setColumns] = useState<IColumn[]>([]);

    // sort function
    type FunctionType<T> = (
        items: T[],
        columnKey: string,
        isSortedDescending?: boolean
    ) => T[];

    const onColumnClick = useCallback(
        (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
            const copyAndSort: FunctionType<IPod> = (
                items,
                columnKey,
                isSortedDescending
            ) => {
                const key = columnKey as keyof IPod;
                return items
                    .slice(0)
                    .sort((a: IPod, b: IPod) =>
                        (isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1
                    );
            };
            console.log(columns); // <-- empty
            const newColumns: IColumn[] = columns.slice();
            const currColumn: IColumn = newColumns.filter(
                (currCol) => column.key === currCol.key
            )[0];
            newColumns.forEach((newCol: IColumn) => {
                if (newCol === currColumn) {
                    currColumn.isSortedDescending = !currColumn.isSortedDescending;
                    currColumn.isSorted = true;
                } else {
                    newCol.isSorted = false;
                    newCol.isSortedDescending = true;
                }
            });

            setColumns(newColumns);
            if (pods != null)
                setPods(
                    copyAndSort(
                        pods,
                        currColumn.fieldName!,
                        currColumn.isSortedDescending
                    )
                );
        },
        []
    );

    useEffect(() => {
        setColumns([
            {
                key: "column1",
                name: "File Type",
                //className: classNames.fileIconCell,
                //iconClassName: classNames.fileIconHeaderIcon,
                ariaLabel:
                    "Column operations for File type, Press to sort on File type",
                iconName: "Page",
                isIconOnly: true,
                fieldName: "na2me",
                minWidth: 16,
                maxWidth: 16,
                onColumnClick: onColumnClick
                //onRender: (item: IDocument) => (
                //  <TooltipHost content={`${item.fileType} file`}>
                //    <img src={item.iconName} className={classNames.fileIconImg} alt={`${item.fileType} file icon`} />
                //   </TooltipHost>
                //),
            },
            {
                key: "pod-name",
                name: "Pod 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: "pod-status",
                name: "Status",
                fieldName: "status",
                minWidth: 70,
                maxWidth: 90,
                isResizable: true,
                onColumnClick: onColumnClick,
                data: "string",
                //onRender: (item: IDocument) => {
                //  return <span>{item.dateModified}</span>;
                //},
                isPadded: true
            },
            {
                key: "pod-age",
                name: "Age",
                fieldName: "timestamp",
                minWidth: 70,
                maxWidth: 90,
                isResizable: true,
                isCollapsible: true,
                data: "number",
                onColumnClick: onColumnClick,
                onRender: (pod: IPod) => {
                    var age = "";
                    if (pod.age > 86400)
                        age = Math.floor(pod.age / 86400).toString() + " day";
                    else if (pod.age > 3600)
                        age = Math.floor(pod.age / 3600).toString() + " hour";
                    else if (pod.age > 60)
                        age = Math.floor(pod.age / 3600).toString() + "min";
                    else age = pod.age.toString() + " sec";
                    return (
                        <span style={{ display: "block", textAlign: "right" }}>{age}</span>
                    );
                },
                isPadded: true
            },
            {
                key: "column5",
                name: "File Size",
                fieldName: "fileSizeRaw",
                minWidth: 70,
                maxWidth: 90,
                isResizable: true,
                isCollapsible: true,
                data: "number",
                onColumnClick: onColumnClick
                //onRender: (item: IDocument) => {
                //  return <span>{item.fileSize}</span>;
                //},
            }
        ]);

        const url = "data.json";

        const fetchData = async () => {
            try {
                const response = await fetch(url, { mode: "cors" });
                const json = await response.json();
                if (Array.isArray(json)) {
                    setPods(json);
                } else {
                    console.log(json);
                }
            } catch (error) {
                console.log("error", error);
            }
        };
        fetchData();
    }, []);

    /* eslint-disable no-unused-vars */
    const onItemInvoked = useCallback((pod?: IPod): void => {}, []);

    return (
        <>
            <ShimmeredDetailsList
                items={pods || []}
                compact={false}
                columns={columns}
                enableShimmer={!pods}
                selectionMode={SelectionMode.none}
                setKey="pods"
                layoutMode={DetailsListLayoutMode.justified}
                onItemInvoked={onItemInvoked}
            />
        </>
    );
};

export default PodList;
Mr.Wang from Next Door
  • 13,670
  • 12
  • 64
  • 97

1 Answers1

1

I solved it with useRef

import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
  DetailsListLayoutMode,
  SelectionMode, 
  IColumn,
} from '@fluentui/react/lib/DetailsList';
import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList';
import './PodList.css'

export interface IPod {
  name: string;
  status: string;
  age: number;
}

export interface IPodListState {
  pods: IPod[];
}


// sort function
type FunctionType<T> = (items: T[], columnKey: string, isSortedDescending?: boolean) => T[];
const copyAndSort : FunctionType<IPod> = (items, columnKey, isSortedDescending ) => {
  const key = columnKey as keyof IPod;
  return items.slice(0).sort((a: IPod, b: IPod) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
};

type ColumnClickHandler = (ev: React.MouseEvent<HTMLElement>, column: IColumn) => void;

const PodList: React.FunctionComponent = () => {


  const [pods, setPods] = useState<IPod[] | undefined>(undefined);
  const columnsHolder = useRef<IColumn[]>([]);
  const columnClickHandler = useRef<ColumnClickHandler>((ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => {});

  useEffect(() => {
    columnClickHandler.current = (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => {

      const currColumn: IColumn = columnsHolder.current.filter(
                (currCol) => column.key === currCol.key
            )[0];
            columnsHolder.current.forEach((col: IColumn) => {
                if (col === currColumn) {
                    col.isSortedDescending = !col.isSortedDescending;
                    col.isSorted = true;
                } else {
                    col.isSorted = false;
                    col.isSortedDescending = true;
                }
            });

            if (pods != null) {
        setPods(
                    copyAndSort(
                        pods,
                        currColumn.fieldName!,
                        currColumn.isSortedDescending
                    )
                );
      }
                
    };
  }, [pods]);


  // initialize columns
  useEffect(() => {
    columnsHolder.current = [
      {
          key: 'column1',
          name: 'File Type',
          //className: classNames.fileIconCell,
          //iconClassName: classNames.fileIconHeaderIcon,
          ariaLabel: 'Column operations for File type, Press to sort on File type',
          iconName: 'Page',
          isIconOnly: true,
          fieldName: 'na2me',
          minWidth: 16,
          maxWidth: 16,
          onColumnClick: (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => { columnClickHandler.current(ev, column); },
          //onRender: (item: IDocument) => (
          //  <TooltipHost content={`${item.fileType} file`}>
          //    <img src={item.iconName} className={classNames.fileIconImg} alt={`${item.fileType} file icon`} />
          //   </TooltipHost>
          //),
        },
        {
          key: 'pod-name',
          name: 'Pod 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: (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => { columnClickHandler.current(ev, column); },
          data: 'string',
          isPadded: true,
        },
        {
          key: 'pod-status',
          name: 'Status',
          fieldName: 'status',
          minWidth: 70,
          maxWidth: 90,
          isResizable: true,
          onColumnClick: (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => { columnClickHandler.current(ev, column); },
          data: 'string',
          //onRender: (item: IDocument) => {
          //  return <span>{item.dateModified}</span>;
          //},
          isPadded: true,
        },
        {
          key: 'pod-age',
          name: 'Age',
          fieldName: 'age',
          minWidth: 70,
          maxWidth: 90,
          isResizable: true,
          isCollapsible: true,
          data: 'number',
          onColumnClick: (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => { columnClickHandler.current(ev, column); },
          headerClassName : 'DetailsListColumnRight',
          onRender: (pod: IPod) => {
            var age = "";
            if( pod.age > 86400 )
              age = Math.floor(pod.age / 86400).toString() + ' day';
            else if( pod.age > 3600 )
              age = Math.floor(pod.age / 3600).toString() + ' hour';
            else if( pod.age > 60 )
              age = Math.floor(pod.age / 3600).toString() + 'min';
            else
              age = pod.age.toString() + ' sec';
            return <span style={{ display: 'block', textAlign: 'right' }}>{age}</span>;
          },
          isPadded: true,
        },
        {
          key: 'pod-ip',
          name: 'Pod IP',
          fieldName: 'podIp',
          minWidth: 70,
          maxWidth: 90,
          isResizable: true,
          isCollapsible: true,
          data: 'string',
          onColumnClick: (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => { columnClickHandler.current(ev, column); },
        },
        {
          key: 'host-ip',
          name: 'Host IP',
          fieldName: 'hostIp',
          minWidth: 70,
          maxWidth: 90,
          isResizable: true,
          isCollapsible: true,
          data: 'string',
          onColumnClick: (ev: React.MouseEvent<HTMLElement>, column: IColumn) : void => { columnClickHandler.current(ev, column); },
        },
      ];
  }, []);


  useEffect(() => {
    const url = "http://127.0.0.1:8080/api/pods";

    const fetchData = async () => {
        try {
            const response = await fetch(url, {mode:'cors'});
            const json = await response.json();
            if(Array.isArray(json)) {
              setPods(
                copyAndSort( json,  "name", false )
              );
            } else {
              console.log(json);
            }
        } catch (error) {
            console.log("error", error);
        }
    };
    fetchData();
  }, []);

  

  /* eslint-disable no-unused-vars */
  const onItemInvoked = useCallback(
    (pod?: IPod): void => {
    },
    [],
  );

  return (
    <>
      <ShimmeredDetailsList
        items={pods || []}
        compact={false}
        columns={columnsHolder.current}
        enableShimmer={pods === undefined}
        selectionMode={SelectionMode.none}
        setKey="pods"
        layoutMode={DetailsListLayoutMode.justified}
        onItemInvoked={onItemInvoked}
      />
    </>
  );
};

export default PodList;
Mr.Wang from Next Door
  • 13,670
  • 12
  • 64
  • 97