Introduction

I'd like to share my approach at CSV data in a React app. Since this data is structured and tabular I decided to give it a thought on how to experience it just as regular tabular data can be rendered and displayed via a virtual DOM. I was asking myself if it's possible to write tabular data React components that will render specifically for CSV representation download. I also didn't want to include yet another bloated library just for the sake of handling CSV download. Here's my findings.

Approaching a generic downloader component

This component is a generic, re-usable component that is passed children which is a compatible CSV data renderer. Downloader is able to render the child off-screen, grab its textContent by opting in for a DOM API, performing some agreed replacement logic and triggering browser to download the result:

type Props = {
  children: JSX.Element;
  isDisabled?: boolean;
  isLoading?: boolean;
};

export const separators = {
  row: "===========",
  col: "-----------",
  value: "~~~~~~~~~~~~"
};

export function CsvTableDownloader({
    children,
    isDisabled,
    isLoading
}: Props) {
  const tableRef = React.useRef(null);
  const [downloadStarted, setDownloadStarted] = React.useState(false);

  React.useEffect(() => {
      if (!tableRef.current) {
          return;
      }

      downloadCsv(
          tableRef.current.textContent
            .trim()
            .replace(new RegExp(separators.col, "g"), ","),
            .replace(new RegExp(separators.row, "g"), "\n"),
            .replace(new RegExp(separators.value, "g"), '"')
      );

      setDownloadStarted(false);
  }, [downloadStarted]);

  return (
      <>
        <button
            isDisabled={isDisabled}
                isLoading={downloadStarted || isLoading}
                onClick={() => setDownloadStarted(true)}
        >
            Download as CSV
        </button>
          {downloadStated && (
            <div
                ref={tableRef}
                  style={{ position: "absolute", top: "-9999px", left: "-9999px" }}
            >
                {children}
            </div>
           )}
      </>
  );
}

The component renders a button to trigger browser to download a CSV file via a downloadCsv function. Additionally, it renders off-screen a compatible CSV component. Also, I needed to make sure that CSV separators are sufficiently unique and there would be no issues with a browser or React messing up with them. So a set of replacement separators that are used in a compatible CSV renderer.

A compatible CSV renderer

I structured my code in a way that a TableHtml component is accompanied by a TableCsv one wherever it's needed and one resembles another, the difference lies in rendering details:

type Props = {
    data: Array<TEntry>
};

const columnHeadings = [
  "Author",
  "Required",
  "Time",
  "Description"
];

export function TableHtml({
    data
}: Props) {
    return (
    <>
        <CsvTableDownloader>
            <TableCsv data={data} />
        </CsvTableDownloader>
            // below is typical React table rendering
      </>
  );
}

export function TableCsv({
    data
}: Props) {
    return (
    <>
        {columnHeadings.join(separators.col)}
            {separators.row}
            {data?.map(entry => (
            <>
                {separators.value}
                {entry.author}
                {separators.value}
                {separators.col}
                {separators.value}
                {<Description entry={entry} asText />}
                {separators.value}
                  {separators.row}
              </>
          ))}
      </>
  );
}

Please note that TableHtml contains a reference to CsvTableDownloader that is passed TableCsv as a child. Both TableHtml and TableCsv are passed the same props. And TableCsv lays down data using separators defined in CsvTableDownloader. As an added bonus I can isolate more complex pieces of rendering like description here as separate components.

The end result looks simple and is easy to read and maintain.

Conclusion

Rendering non-html with React can be a bit trickier but it's doable and end result can be actually pleasant to work with on the long run.

I hope you liked this article and if so, please share it.