← Back

A Tale of a Table

a feature request came along straight from the user he wanted an integration between our system and another system that lives in the MainFrame until our system came along he used to export to excel from the MainFrame, then edit it manually so the most acceptable solution for him was to have the excel experience within our system

the 2 most popular packages for that in react are reactgrid and react-spreadsheet I took Idan’s component since the look and feel was a better fit integration was pretty easy and in less than an hour I already had it running (awesome work Idan!)

but then we needed to add 2 more features - filters per column and row selection (which will allow uploading selected rows) it took a couple of hours to implement but I got the filters right and added a first column with checkboxes for row selection the problem was that once I added the selection part I had to make this component controlled (instead of uncontrolled) which made it 10 times slower - and that’s on my demo excel, the user’s excel has about 50 times more data

and that’s where the sleeves rolled up after a talk with Schniz we decided to make it uncontrolled, which means building a clean new component

the form trick

the cleanest way to make it uncontrolled and then upload its data is to make the table a form element but since I only want the selected rows, I put the form element outside the actual table and set the form attribute only on the selected rows’ inputs this way only the checked rows are included in the form submission - which is a pretty genius idea in my opinion

then to upload the data you can just add a button anywhere with type="submit" and the same form name and it’s even easier with Next.js - just make the form action a server action

the form wrapper

the one thing that bugged me is that with server actions you can’t easily get the response back or handle errors cleanly so I wrapped the regular react form with my own:

import { useCallback } from 'react';

export type FormProps = {
  afterSubmit?: (data?: unknown) => void;
  onError?: (error: unknown) => void;
} & React.DetailedHTMLProps<
  React.FormHTMLAttributes<HTMLFormElement>,
  HTMLFormElement
>;

export function Form(props: FormProps) {
  const { afterSubmit, action: previousAction, ...rest } = props;
  const action = useCallback(
    async (formData: FormData) => {
      let data: unknown;
      if (previousAction) {
        try {
          // @ts-expect-error-next-line -- TODO: fix
          data = await previousAction(formData);
        } catch (error) {
          if (props.onError) {
            props.onError(error);
          }
          return;
        }
      }
      if (afterSubmit) {
        afterSubmit(data);
      }
    },
    [previousAction, afterSubmit]
  );

  return <form {...rest} action={action} />;
}

it takes the original server action, wraps it with try/catch, and gives you afterSubmit and onError callbacks so now you can actually show success messages, redirect, or display errors - all the stuff server actions don’t give you out of the box

the end result is a fast uncontrolled spreadsheet that only submits the rows the user selected, with proper error handling and it handles 50x the data without breaking a sweat