Using an output object when saving data to a file from the client in React

Using an output object when saving data to a file from the client in React

Intro:

In this blog post, we will explore how to iterate through nested data and save it to a text file using React. Specifically, we will focus on using a custom output object to organize the data and ensure its proper structure. We will be working with TypeScript and JavaScript to implement this functionality professionally.

Prerequisites:

To follow along with this tutorial, you should have a basic understanding of React, TypeScript, and JavaScript. Familiarity with data structures, interfaces, and classes will also be beneficial.

Getting Started:

We are going to assume you have a basic react project using vite, if you are unsure how to set one up use this

https://www.digitalocean.com/community/tutorials/how-to-set-up-a-react-project-with-vite

or you can grab the repo from here
https://github.com/BenGardiner123/space_data

cd into the src, then run "yarn" to install the dependencies and once that magic has happened "yarn run dev"
you should now see this

First, let's define the initial data structure we'll be working with. The code snippet provided includes TypeScript interfaces for the entities involved: Moon, Planet, BlackHole, and Galaxy. The Galaxy class represents a collection of planets and black holes. The Planet interface contains information about each planet, including its name, distance from the sun, and an array of moons. Using the same idea the BlackHole interface stores details about black holes, such as name, mass, distance from Earth, and discovery date.
Take a look in the models folder and you will see these. I have merged them for simplicity here.

export interface Moon {
  name: string;
  distance_from_planet: number;
}

export interface Planet {
  name: string;
  distance_from_sun: number;
  moons: Moon[];
}

export interface BlackHole {
  name: string;
  mass: number;
  distance_from_earth: number;
  discovery_date: string;
}


class Galaxy {
  name: string;
  age: number;
  planets: Planet[];
  black_holes: BlackHole[];

  constructor(
    name: string,
    age: number,
    planets: Planet[],
    black_holes: BlackHole[]
  ) {
    this.name = name;
    this.age = age;
    this.planets = planets;
    this.black_holes = black_holes;
  }
}
const solarSystem = new Galaxy("Milky Way", 13.6, [
  {
    name: "Mercury",
    distance_from_sun: 0.39,
    moons: 0,
  },
  {
    name: "Venus",
    distance_from_sun: 0.72,
    moons: 0,
  },
  {
    name: "Earth",
    distance_from_sun: 1,
    moons: [
      {
        name: "Moon",
        distance_from_earth: 0.00257,
      },
    ],
  },
  {
    name: "Mars",
    distance_from_sun: 1.52,
    moons: [
      {
        name: "Phobos",
        distance_from_mars: 9.378,
      },
      {
        name: "Deimos",
        distance_from_mars: 23.46,
      },
    ],
  },
], [
  {
    name: "Sagittarius A*",
    mass: 4.31e6,
    distance_from_earth: 25000,
    discovery_date: "1974-12-23",
  },
  {
    name: "Cygnus X-1",
    mass: 14.8,
    distance_from_earth: 6050,
    discovery_date: "1964-05-20",
  },
]);

Why would you need to use a custom object for output?

In React applications, customising your data prior to export is a common requirement (usually done server side but sometimes not) and being able to customize the output format according to specific needs is essential. One way to achieve this is by using output objects. This approach allows for fine-grained control over data representation and organization during the export process.

Implementing Custom Output Objects in React:

To illustrate the concept, let's consider a scenario where we have a Galaxy class representing a solar system and its attributes such as name, age, planets, and black holes. We want to export this data but require a specific structure and formatting.

In the provided code snippet, we define a customSpaceDataOutputObject that extracts relevant information from the solarSystem instance and formats it according to our requirements. This object includes properties like the galaxy name, age, an array of planets with their names, distances from the sun, and moon names joined as a string. Additionally, it includes an array of black holes with their respective details.

The Solar System :

const solarSystem = new Galaxy(
  "Milky Way",
  13.6,
  [
    {
      name: "Mercury",
      distance_from_sun: 0.39,
      moons: [],
    },
    {
      name: "Venus",
      distance_from_sun: 0.72,
      moons: [],
    },
    {
      name: "Earth",
      distance_from_sun: 1,
      moons: [
        {
          name: "Moon",
          distance_from_planet: 0.00257,
        },
      ],
    },
    {
      name: "Mars",
      distance_from_sun: 1.52,
      moons: [
        {
          name: "Phobos",
          distance_from_planet: 9.378,
        },
        {
          name: "Deimos",
          distance_from_planet: 23.46,
        },
      ],
    },
  ],
  [
    {
      name: "Sagittarius A*",
      mass: 4.31e6,
      distance_from_earth: 25000,
      discovery_date: "1974-12-23",
    },
    {
      name: "Cygnus X-1",
      mass: 14.8,
      distance_from_earth: 6050,
      discovery_date: "1964-05-20",
    },
  ]
);

Our custom output object:

 const customSpaceDataOutputObject = {
    galaxy: {
      name: solarSystem.name,
      age: solarSystem.age,
    },
    planets: solarSystem.planets.map((planet) => ({
      name: planet.name,
      distance_from_sun: planet.distance_from_sun,
      moons: planet.moons.map((moon) => moon.name).join(", "),
    })),
    black_holes: solarSystem.black_holes.map((blackHole) => ({
      name: blackHole.name,
      mass: blackHole.mass,
      distance_from_earth: blackHole.distance_from_earth,
      discovery_date: blackHole.discovery_date,
    })),
  };

Cool!

What about if we wanted to do some filtering/selection on the data being passed in?

 const customSpaceDataOutputObject = {
    galaxy: {
      name: solarSystem.name,
      age: solarSystem.age,
    },
    // remember .map() returns a new array
    planets: solarSystem.planets.map((planet) => ({
      name: planet.name,
      distance_from_sun: planet.distance_from_sun,
      // only include moons that are further than 10 units away
      moons: planet.moons
        .filter((moon) => moon.distance_from_planet > 10)
        // create a new array of just the names
        .map((moon) => moon.name)
        .join(", "),
    })),
    black_holes: solarSystem.black_holes.map((blackHole) => ({
      name: blackHole.name.toUpperCase(),
      mass: blackHole.mass,
      distance_from_earth: blackHole.distance_from_earth,
      discovery_date: blackHole.discovery_date,
    })),
  };

Here is a quick example of where you might need to do some work on your object before you return it to the consumer.
It might be some text manipulation or maybe you looped over a few hundred records and you want to drop the items that aren't relevant.

Now we want to write that from our front end. If we were working using node we would use the 'fs' package. But React being in the client doesn;t have access to the file system so we use something like this

interface CustomSpaceDataOutputObject {
  galaxy: {
    name: string;
    age: number;
  };
  planets: {
    name: string;
    distance_from_sun: number;
    moons: string;
  }[];
  black_holes: {
    name: string;
    mass: number;
    distance_from_earth: number;
    discovery_date: string;
  }[];
}

export const writeFile = (stringToWrite: CustomSpaceDataOutputObject) => {
  console.log("writeFile called", JSON.stringify(stringToWrite, null, 2));

  const element = document.createElement("a");
  const file = new Blob([JSON.stringify(stringToWrite, null, 2)], {
    type: "text/plain",
  });

  element.href = URL.createObjectURL(file);
  element.download = "output.txt";

  document.body.appendChild(element); // Required for this to work in Firefox
  element.click();

  document.body.removeChild(element); // Clean up the temporary element
};

so we click the button and get this in our .txt file

{
  "galaxy": {
    "name": "Milky Way",
    "age": 13.6
  },
  "planets": [
    {
      "name": "Mercury",
      "distance_from_sun": 0.39,
      "moons": ""
    },
    {
      "name": "Venus",
      "distance_from_sun": 0.72,
      "moons": ""
    },
    {
      "name": "Earth",
      "distance_from_sun": 1,
      "moons": ""
    },
    {
      "name": "Mars",
      "distance_from_sun": 1.52,
      "moons": "Deimos"
    }
  ],
  "black_holes": [
    {
      "name": "SAGITTARIUS A*",
      "mass": 4310000,
      "distance_from_earth": 25000,
      "discovery_date": "1974-12-23"
    },
    {
      "name": "CYGNUS X-1",
      "mass": 14.8,
      "distance_from_earth": 6050,
      "discovery_date": "1964-05-20"
    }
  ]
}

Nice! So that's it - there are plenty of other options too - you could output the data and inject it into pre-rolled scripts, or SQL queries whatever! you are only limited by your imagination.

Anyway thanks for reading - Happy coding and see you soon.

Did you find this article valuable?

Support Ben Gardiner by becoming a sponsor. Any amount is appreciated!