milanHrabos
milanHrabos

Reputation: 1965

How to use state management (zustand) with tanstack table v8?

I have this zustand store:

useStore.js:

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

export const useStore = create(
  immer(set => ({
    table: {
      columns: [],
      data: [],
      globalFilter: '',
      pagination: {},
      setColumns: columns => set(s => { s.table.columns = columns }),
      setData: data => set(s => { s.table.data = data }),
      updateCell: (r, c, val) => set(s => { s.table.data[r][c] = val }),
      addRow: () => set(s => {
        const emptyRow = {};
        s.table.columns.forEach(c => emptyRow[c.accessorKey] = '');
        s.table.data.splice(
          (s.table.pagination.pageIndex + 1) * s.table.pagination.pageSize - 1, 0, emptyRow
        )
      }),
      setGlobalFilter: fs => set(s => { s.table.globalFilter = fs }),
      setPagination: p => set(s => { s.table.pagination = p }),
    }
  }))
)

which I am trying to use with the tanstack table:

new.js:

import TABLE from '../../work.json';
import { useEffect, useState } from "react"
import * as XLSX from 'xlsx/xlsx.mjs';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUpDown } from '@fortawesome/free-solid-svg-icons';
import { useStore } from '../../hooks/useStore';

import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable
} from '@tanstack/react-table';

function NikitaDev() {
  const {
    columns,
    data,
    globalFilter,
    pagination,
    setColumns,
    setData,
    addRow,
    setGlobalFilter,
    setPagination
  } = useStore(s => s.table);

  useEffect(() => {
    setColumns(TABLE.headers.map(h => ({
      accessorKey: h,
      header: h,
      cell: EditableCell
    })));
    setData(TABLE.data);
    setPagination({
      pageIndex: 0,
      pageSize: 14
    });
  }, [])

  const table = useReactTable({
    data,
    columns,
    state: {
      columnOrder: ["Stavební_díl-ID", "Allright_Stavebni_dil_ID"],
      globalFilter,
      pagination
    },
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    columnResizeMode: 'onChange',
    onPaginationChange: setPagination,
  })

  return (
    <div className="flex flex-col gap-[1rem] m-[1rem]">
      <div className="flex">
        <input
          value={globalFilter}
          onChange={e => setGlobalFilter(e.target.value)}
          placeholder='Search All columns'
          className='flex-1 focus:outline-none'
        />
        <button
          onClick={() => {
            const workbook = XLSX.utils.book_new();
            const worksheet = XLSX.utils.json_to_sheet(data);
            XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet 1');
            XLSX.writeFile(workbook, 'data.xlsx');
          }}
          className='bg-green-500 rounded text-white px-4 py-2'
        >
          Export to excel
        </button>
      </div>
      <table className="w-full text-left text-sm text-gray-500">
        <thead className="text-xs text-gray-700 uppercase bg-gray-50">
          {table.getHeaderGroups().map(hg => (
            <tr key={hg.id}>
              {hg.headers.map(h => (
                <th key={h.id} className='relative border px-6 py-3'>
                  {h.column.columnDef.header}
                  <button onClick={h.column.getToggleSortingHandler()}>
                    <span className='ml-[0.5rem]'>
                      {{
                        asc: " 🔼",
                        desc: " 🔽",
                      }[h.column.getIsSorted()] ||
                        <FontAwesomeIcon icon={faUpDown} />
                      }
                    </span>
                  </button>
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(r => (
            <tr key={r.id}>
              {r.getVisibleCells().map(c => (
                <td key={c.id} className='border p-[0.25rem]'>
                  {flexRender(c.column.columnDef.cell, c.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <span className='text-sm'>
        Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
      </span>
      <div className="flex gap-[0.5rem]">
        <button
          disabled={!table.getCanPreviousPage()}
          onClick={() => table.previousPage()}
          className='px-[0.5rem] py-[0.25rem] border border-gray-300 rounded'
        >
          &lt;
        </button>
        <button
          disabled={!table.getCanNextPage()}
          onClick={() => table.nextPage()}
          className='px-[0.5rem] py-[0.25rem] border border-gray-300 rounded'
        >
          &gt;
        </button>
        <select
          value={table.getState().pagination.pageSize}
          onChange={e => {
            table.setPageSize(Number(e.target.value))
          }}
          className='px-[0.5rem] py-[0.25rem] border border-gray-300 rounded'
        >
          {[10, 20, 30, 40, 50, 100, 200, 500].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              {pageSize}
            </option>
          ))}
        </select>
        <button
          onClick={() => addRow()} // Call the addRow function when button is clicked
          className='bg-blue-500 rounded text-white px-4 py-2 ml-2'
        >
          Add Row
        </button>
      </div>
    </div>
  )
}

function EditableCell({ getValue, row, column }) {
  const updateCell = useStore(s => s.table.updateCell);

  const initalValue = getValue();
  const [value, setValue] = useState(initalValue);

  useEffect(() => {
    setValue(initalValue);
  }, [initalValue])

  function onChange(value) {
    setValue(value);
    updateCell(row.index, column.id, value);
  }

  return (
    <input
      type='text'
      value={value}
      onChange={e => onChange(e.target.value)}
      className='w-full px-3 py-2'
    />
  );
}

export default NikitaDev;

previously, I was using useState, to store it locally, but need to have access to data and columns in other components as well. problem. But now, it does not render anything. Here is the version without zustand that works:

old.js:

import TABLE from '../../work.json';
import { useEffect, useState } from "react"
import * as XLSX from 'xlsx/xlsx.mjs';

import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable
} from '@tanstack/react-table';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUpDown } from '@fortawesome/free-solid-svg-icons';

function NikitaDev() {
  const [columns, setColumns] = useState(() => {
    return TABLE.headers.map(h => ({
      accessorKey: h,
      header: h,
      cell: EditableCell
    }));
  });
  const [data, setData] = useState(TABLE.data);
  const [globalFilter, setGlobalFilter] = useState('');
  const [pagination, setPagination] = useState({
    pageIndex: 0,
    pageSize: 14
  });

  const table = useReactTable({
    data,
    columns,
    state: {
      columnOrder: ["Stavební_díl-ID", "Allright_Stavebni_dil_ID"],
      globalFilter,
      pagination
    },
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    columnResizeMode: 'onChange',
    onPaginationChange: setPagination,
    meta: {
      updateData: (rowIndex, columnId, value) => setData(
        prev => prev.map((row, index) => index === rowIndex ? {
          ...prev[index],
          [columnId]: value
        } : row)
      ),
      addRow: () => {
        const newData = [...data];
        const emptyRow = {};
        columns.forEach(col => {
          emptyRow[col.accessorKey] = '';
        });
        newData.splice((pagination.pageIndex + 1) * pagination.pageSize - 1, 0, emptyRow); // Insert at the end of current page
        setData(newData);
      }
    }
  })

  return (
    <div className="flex flex-col gap-[1rem] m-[1rem]">
      <div className="flex">
        <input
          value={globalFilter}
          onChange={e => setGlobalFilter(e.target.value)}
          placeholder='Search All columns'
          className='flex-1 focus:outline-none'
        />
        <button
          onClick={() => {
            const workbook = XLSX.utils.book_new();
            const worksheet = XLSX.utils.json_to_sheet(data);
            XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet 1');
            XLSX.writeFile(workbook, 'data.xlsx');
          }}
          className='bg-green-500 rounded text-white px-4 py-2'
        >
          Export to excel
        </button>
      </div>
      <table className="w-full text-left text-sm text-gray-500">
        <thead className="text-xs text-gray-700 uppercase bg-gray-50">
          {table.getHeaderGroups().map(hg => (
            <tr key={hg.id}>
              {hg.headers.map(h => (
                <th key={h.id} className='relative border px-6 py-3'>
                  {h.column.columnDef.header}
                  <button onClick={h.column.getToggleSortingHandler()}>
                    <span className='ml-[0.5rem]'>
                      {{
                        asc: " 🔼",
                        desc: " 🔽",
                      }[h.column.getIsSorted()] ||
                        <FontAwesomeIcon icon={faUpDown} />
                      }
                    </span>
                  </button>
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(r => (
            <tr key={r.id}>
              {r.getVisibleCells().map(c => (
                <td key={c.id} className='border p-[0.25rem]'>
                  {flexRender(c.column.columnDef.cell, c.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <span className='text-sm'>
        Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
      </span>
      <div className="flex gap-[0.5rem]">
        <button
          disabled={!table.getCanPreviousPage()}
          onClick={() => table.previousPage()}
          className='px-[0.5rem] py-[0.25rem] border border-gray-300 rounded'
        >
          &lt;
        </button>
        <button
          disabled={!table.getCanNextPage()}
          onClick={() => table.nextPage()}
          className='px-[0.5rem] py-[0.25rem] border border-gray-300 rounded'
        >
          &gt;
        </button>
        <select
          value={table.getState().pagination.pageSize}
          onChange={e => {
            table.setPageSize(Number(e.target.value))
          }}
          className='px-[0.5rem] py-[0.25rem] border border-gray-300 rounded'
        >
          {[10, 20, 30, 40, 50, 100, 200, 500].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              {pageSize}
            </option>
          ))}
        </select>
        <button
          onClick={() => table.options.meta.addRow()} // Call the addRow function when button is clicked
          className='bg-blue-500 rounded text-white px-4 py-2 ml-2'
        >
          Add Row
        </button>
      </div>
    </div>
  )
}

function EditableCell({ getValue, row, column, table }) {
  const initalValue = getValue();
  const [value, setValue] = useState(initalValue);

  useEffect(() => {
    setValue(initalValue);
  }, [initalValue])

  function onChange(value) {
    setValue(value);
    table.options.meta.updateData(row.index, column.id, value);
  }

  return (
    <input
      type='text'
      value={value}
      onChange={e => onChange(e.target.value)}
      className='w-full px-3 py-2'
    />
  )
}

export default NikitaDev;

Upvotes: 1

Views: 1071

Answers (1)

vmark
vmark

Reputation: 66

Tanstack react-table expects a function for controlling the change handlers which accept both a value and an updater as its parameter (think of React's useState api).

The setters you're providing from your store, only accept regular values and not updater functions too.

For a regular use-case you can do something like this for e.g. the pagination:

 state: {
   pagination,
 },
 onPaginationChange: (updater) => {
   const newValue =
    updater instanceof Function ? updater(pagination) : updater;
  setSelected(newValue);
},

See the relevant documentation here: https://tanstack.com/table/latest/docs/framework/react/guide/table-state#2-updaters-can-either-be-raw-values-or-callback-functions

Upvotes: 2

Related Questions