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