Back
First published on 11/2/2024.

Downloading CSV data in React

A little housekeeping

This is going to be the first article in a series of two about downloading files from React! Nothing is, perhaps, very special about these techniques and, I figure, it is pretty common practice at this point. That said, I felt that it was important to write up something quick and easy to reference myself later and for those that are looking for more code examples in one place than what I could readily find on the web after a cursory Kagi search (my google alternative).

So this is the first article about downloading CSV (comma-separated values) files, normally to be read in excel (or today probably more likely google sheets because of the google-ification of the web. See my earlier note about using Kagi over google search). The next article will be for PDFs and should be a little more involved than this one.

Luckily for us UI devs, downloading CSV files in React or vanilla js/html is pretty easy and straight-forward. I mean, "comma-separated values" really does say it all doesn't it! However, what if you want to format numbers in your file? How does one include those commas without it being separated into multiple fields? Well, sit tight baby birds. I'll feed you.

Simple example

Below is the most straight-forward way to format and download CSV data.

1
const fileName = 'my-data.csv';
2
const data = [
3
    ['Name', 'Age', 'Height'],
4
    ['Alice', 30, 1.8],
5
    ['Bob', 25, 1.7],
6
    ['Charlie', 35, 1.9],
7
];
8

9
const csvData = data.map((row) => row.join(',')).join('\n');
10
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
11
const url = URL.createObjectURL(blob);
12

13
return (
14
    <button download={fileName} href={csvData}>
15
        Download CSV
16
    </button>
17
);

Let's break this down a little bit! Firstly, the file to be downloaded needs a name. This can be procedurally generated, and probably should be, with a timestamp and category. I left it "my-data.csv" above to keep it simple. But in any case, it should have a ".csv" extension.

As for the data, the proper formatting is to have each row separated by a line-break (\n) and each row by a comma (,). Above the data comes to us (or in this case is defined) as an array of arrays. However, it is more likely to come as a list of objects. If that is the case, an array of the headers will be needed to specify the order, otherwise, because objects aren't ordered, data might be displayed in the wrong column. Additionally, if the object keys aren't just a camelCased version of the column headers, then a mapping object will be needed. This object will map the properly formatted headers to the object keys. Here's how I've done it with data as a list of objects:

1
const data = [
2
    { name: 'Alice', age: 30, height: 1.8 },
3
    { name: 'Bob', age: 25, height: 1.7 },
4
    { name: 'Charlie', age: 35, height: 1.9 },
5
];
6

7
const DOWNLOAD_HEADERS = ['Name', 'Age', 'Height'];
8

9
// this is only necessary if the column headers aren't just camelCased versions of the headers
10
const mapper = {
11
    [DOWNLOAD_HEADERS[0]]: 'name',
12
    [DOWNLOAD_HEADERS[1]]: 'age',
13
    [DOWNLOAD_HEADERS[2]]: 'height',
14
};
15

16
const csvData = data
17
    .map((row) => {
18
        return DOWNLOAD_HEADERS.map((header) => {
19
            const key = mapper[header];
20
            return row[key];
21
        }).join(','); // comma separate the cells within a row
22
    })
23
    .join('\n'); // line-break separate the rows

If the data objects' keys are consistently just a camel-cased version of the column headers, than this mapping is not necessary and can be generated like so:

1
const headerToCamelCase = (header: string) => {
2
    const pascalCasedKey = string.replace(/\s/g, ''); //remove spaces. This assumes column headers are all capitalized
3
    return pascalCasedKey[0].toLowerCase() + pascalCasedKey.slice(1); // lower case the first char
4
};
5
const csvData = data
6
    .map((row) => {
7
        return DOWNLOAD_HEADERS.map((header) => {
8
            return row[headerToCamelCase(header)];
9
        }).join(',');
10
    })
11
    .join('\n');

So what about that blob?

To make sure the data is interpreted correctly by the browser as a CSV file using utf-8 encoding, we use the built-in URL.createObjectURL function to turn a blob into a proper url. Therefore we need to create a blob! Lucky for us it's just as easy as:

1
const blob = new Blob([downloadData], { type: 'text/csv; charset=utf-8;' });

So, to put it back together, we create a blob, and use that with our URL class to create a properly formatted url. Like so:

1
const createCsvBlobUrl = (downloadData: string) => {
2
    const blob = new Blob([downloadData], { type: 'text/csv; charset=utf-8;' });
3

4
    return URL.createObjectURL(blob);
5
};

Handling number formatting

If, for example, there are large numbers in the data that should be formatted to be human readable (with commas), how does one sanitize these commas so as not to accidentally create new cells? Simply wrap exactly the format in quotes. Like so:

1
const csvData = data
2
    .map((row) => {
3
        return DOWNLOAD_HEADERS.map((header) => {
4
            const value = row[headerToCamelCase(header)];
5

6
            return typeof value === 'number'
7
                ? `"${value.toLocaleString()}"` // toLocaleString will add commas (or periods if European)
8
                : value;
9
        }).join(',');
10
    })
11
    .join('\n');

I personally like to make a helper classes for a case such as this because it undoubtably will be needed again.

1
const escapeCommas = (n: number) => `"${n.toLocaleString()}"`;

Wrap up

So that's it! Pretty straight-forward once you get all the key info: separate your rows with line-breaks (\n), separate the cells with commas (,), and wrap any comma-formatted numbers in double quotes ("number"). Make sure to turn your data strings into blobs and then into formatted urls and pass that cutie to a nice button with download and href tags.

Now you can go off and spreadsheet!

Next up is PDFs! These are a little more involved than CSVs as I stated above, as it requires an external library to be used and the styling is quite different compared to regular CSS. Once it is done I'll link it here! You can find the article here!

Until next time!