FluentUI DetailsList onColumnClick with React Hooks gives empty Items
Asked Answered
R

3

9

I am trying to create a DetailsList with sortable columns (similar as the example in the documentation here: https://uifabric.azurewebsites.net/#/controls/web/detailslist), but instead of a Class component I want to use a functional components and hooks.

The problem is that when the onColumnClick function is being executed, the "items" array is still empty.

This is basically the setup of my component:

const MyList= () => {
  const [items, setItems] = useState([]);
  const [columns, setColumns] = useState([]);

  useEffect(() => {
    const newItems = someFetchFunction()
    setItems(newItems);

    const newColumns= [
      {
        key: "column1",
        name: "Name",
        fieldName: "name",
        onColumnClick: handleColumnClick,
      },
      {
        key: "column2",
        name: "Age",
        fieldName: "age",
        onColumnClick: handleColumnClick,
      },
    ];
    setColumns(newColumns);
  }, []);

  const handleColumnClick = (ev, column) => {
    console.log(items); // This returns [] instead of a populated array.
  };

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

The problem is that in the handleColumnClick the items returns an empty array. This is obiously a problem when you want to sort the items by a specific column.

Rhaetia answered 14/6, 2020 at 13:19 Comment(2)
I'm having the same problem, do you have an idea in the meanwhile?Hundredpercenter
I am facing same issue is there any update here?Cece
H
5

I assume the reason why the items array is empty is because the state got "stolen", have a look at this article: https://css-tricks.com/dealing-with-stale-props-and-states-in-reacts-functional-components/

To be honest, I don't understand why this happens, probably the onColumnClick is handled asynchronously?!

However adding a useRef hook solves the problem for me:

const MyList = () => {
  const [columns, setColumns] = useState([]);
  const [items, setItems] = useState([]);
  const refItems = useRef(items);

  const updateItems = (newItems) => {
    refItems.current = newItems;
    setItems(newItems);
  }

  useEffect(() => {
    const newItems = someFetchFunction()
    updateItems(newItems);

    const newColumns = // ...
    setColumns(newColumns);
  }, []);

  const handleColumnClick = (ev, column) => {
    console.log(refItems.current);
  };

  return <DetailsList items={items} columns={columns} />;
};
Hundredpercenter answered 28/10, 2020 at 12:17 Comment(0)
E
1

This is because columns are created only in the useEffect once and your state items are closed inside this onColumnClick method instance. The next render creates a new onColumnClick instance but all columns have the old one with enclosed old variables. The simples solution to that is to add in your code on each render (so directly in the component function) something like this:

columns.forEach(c => c.onColumnClick = _onColumnClick); 

to reattach the current _onColumnClick to all columns.

Excision answered 6/7, 2022 at 13:51 Comment(1)
This gives an error in TypeScript: "Arrow function should not return assignment". This fix is to add braces, like this: columns.forEach(c => { c.onColumnClick = _onColumnClick }); Krumm
H
0

The issue here is related to closures. Your handleColumnClick column is the closure of the MyList component (function), and captures the items variable state at the time of it's creation.

The useEffect has an empty dependencies array, so it runs only once, assigning the handleColumnClick function to the onColumnClick property with an empty item array - it seems it is empty on first run.

The solution in your case could be moving your newColumns variable outside the useEffect hook, so that both handleColumnClick and newItems have the same instance of items on every render, like so:

const [items, setItems] = useState([]);

useEffect(() => {
    const newItems = someFetchFunction()
    setItems(newItems);
}, []);

const handleColumnClick = (ev, column) => {
    console.log(items);
};

const newColumns = [
    {
        key: "column1",
        name: "Name",
        fieldName: "name",
        onColumnClick: handleColumnClick,
    },
    {
        key: "column2",
        name: "Age",
        fieldName: "age",
        onColumnClick: handleColumnClick,
    },
];

return <DetailsList items={items} columns={newColumns} />;

If you don't like the idea of recreating newColumns on every rerender you could also add a second useEffect with an [items] dependency to run only after the items is eventually available in the components state.

Housebreaking answered 6/7, 2022 at 14:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.