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.
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.
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.
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.