DEV Community

just-vicky
just-vicky

Posted on

React-Native CSV Editor

In this we are going to use React-Native Expo app to implement a simple in-app CSV Editor.

Run the following command to create a new React Native project called "CSV-Editor":

npx create-expo-app CSV-Editor
cd CSV-Editor
npx expo start

Or

yarn create expo-app CSV-Editor
cd CSV-Editor
yarn expo start
Enter fullscreen mode Exit fullscreen mode

This will start a development server for you.

Running your React Native application

Install the Expo Go app on your iOS or Android phone and connect to the same wireless network as your computer.
On Android, use the Expo Go app to scan the QR code from your terminal to open your project. On iOS, use the built-in QR code scanner of the default iOS Camera app.

Expo Go

Modifying your app

Now that you have successfully run the app, let's modify it. Open App.js in your text editor of choice and edit some lines. The application should reload automatically once you save your changes.

That's it!
Congratulations! You've successfully run and modified your first React Native app.

Difference between React Native and React Native expo:

React Native React Native Expo
Needs more setup with native development tools. Easier setup, no need for complex tools.
Direct access to all native features. Access to a limited set of features provided by Expo.
More control, but more complex. Easier building and deployment managed by Expo.

What is a state variable?

A state variable is basically a way for a component to keep track of information that can change over time. Consider the state variable to be one of many rooms in your house, each with its own set of lights, fans, and other features that can be turned on and off without affecting the lights and fans in the other rooms.

But how do you change the state of the variable?

That's why React introduced the Hooks method, which essentially hooks to the variable's state and updates it as necessary.
The most widely used hooks are useState() and useEffect():

useState():
  • useState is a Hook that lets you add a state variable to your component.
  • useState takes an initial parameter. It can be a value of any type
  • useState returns an array containing exactly two values.

    • The current state. During the first render, it will match the initialState you provided.
    • The set function allows you to change the state and trigger a re-render. For Example, this is how useState should be initialized:
import { useState } from 'react';

function MyComponent() {
  const [num, setNum] = useState(28);
  const [text, setText] = useState('Taylor');
  const [function, setFunction] = useState(() => createFuntction());
  // ...
Enter fullscreen mode Exit fullscreen mode
useEffect():

The useEffect hook in React allows you to perform side effects in your functional components.
But what are the side effects?
They are basically anything your component does aside from rendering. This could include fetching data, setting timers, or manually changing the DOM.

  • useEffect takes two parameters
  • useEffect(function, dependencies?)

    • Function which should be run when the component is rendered.
    • And a dependency on which the function has to be triggered. This is optional but it is recommended to put an empty array if the function need not be in an eternal loop.

For example, this is how useEffect should be used:

import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // This function will run after the component renders
    fetchData();
  }, []); // Empty dependency array means this effect runs only once, after the initial render

  const fetchData = async () => {
    // Fetch data from an API
    const response = await fetch('https://api.example.com/data');
    const result = await response.json();
    setData(result);
  };
//…

Enter fullscreen mode Exit fullscreen mode

Packages used

The packages used to implement this Csv-Editor are:

  • Expo-document-picker
  • Papaparse
  • Expo-file-system

Expo Document picker

Enables mobile app developers to add features that allow us to select files from the device's storage.In Csv-editor, we need to pick the file in order to display and edit it, Expo Document Picker allows you to easily select files from their phone's storage. As a result, it makes adding file selection capabilities to your app easier and more efficient.

To add the package to your project:
npm i expo-document-picker

To utilize all the functionalities, import the package under whichever name you desire:

import * as DocumentPicker from "expo-document-picker";
Enter fullscreen mode Exit fullscreen mode

Papaparse

Helps us work with CSV (Comma Separated Values) files in the app. It allows us to easily convert the CSV data into a format that the application can use, like arrays or objects, and vice versa. We can read the data from CSV files, manipulate it in the code, and then save it back to a CSV file. As the title describes(React Csv Editor), this package plays a crucial role in implementing this feature.

To add the package to your project:
npm i papaparse

To utilize all the functionalities, import the package under whichever name you desire:

 import Papa from "papaparse";
Enter fullscreen mode Exit fullscreen mode

Expo-file-system

It is a tool for managing files and directories in Expo projects. It supports reading and writing files, creating directories, and checking file properties like size and modification date. It enables the app to interact with the file system of the device on which it is running, allowing features such as local data saving and caching for offline use.

To add the package to your project:
npm i expo-file-system

To utilize all the functionalities, import the package under whichever name you desire:

import * as FileSystem from "expo-file-system";
Enter fullscreen mode Exit fullscreen mode

Enough with the introduction, let’ get into implementing the feature.

CSV-editor

Import Statements:

import React, { useState } from "react";
import { View, Button, FlatList, TextInput, SafeAreaView } from "react-native";
import * as DocumentPicker from "expo-document-picker";
import Papa from "papaparse";
import * as FileSystem from "expo-file-system";

Enter fullscreen mode Exit fullscreen mode

React-native has ready made components such as Button, TextInput, etc. which can be used instead of creating it from scratch.
Now let’s declare the state variables:

const [fileUri, setFileUri] = useState(null);
const [csvData, setCsvData] = useState([]);
const [filePicked, setFilePicked] = useState(false);

Enter fullscreen mode Exit fullscreen mode

Picking a File

const App = () => {
  const [fileUri, setFileUri] = useState(null);
  const [csvData, setCsvData] = useState([]);
  const [filePicked, setFilePicked] = useState(false);

  const pickDocument = async () => {
    console.log("Pick document function called");
    try {
      const result = await DocumentPicker.getDocumentAsync({ type: 'text/csv' });
      if (result.canceled === false) {
        setFileUri(result.assets[0].uri);
        const fileData = await readFile(result.assets[0].uri);
        if (fileData) {
          const parsedData = Papa.parse(fileData);
          if (parsedData.errors.length > 0) {
            console.error("Error parsing CSV:", parsedData.errors);
          } else {
            setCsvData(parsedData.data);
            setFilePicked(true);
          }
        } else {
          console.error("Failed to read file data");
        }
      }
    } catch (error) {
      console.error("Error picking document:", error);
    }
  };

  const readFile = async (uri) => {
    console.log("Reading file");
    try {
      const response = await fetch(uri);
      const fileData = await response.text();
      return fileData;
    } catch (error) {
      return null;
    }
  };



  return (
  <View style={{ flex: 1 }}>
     <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        {!filePicked && <Button title="Pick a file" onPress={pickDocument} />}
     </View>
  </View>

  );
};

export default App;

Enter fullscreen mode Exit fullscreen mode
pickDocument()
  • This function is an async function that is used to pick a CSV file.
  • Async function is a method which allows the other operation to run while the function takes a long time to run.
  • Inside a try block, it awaits the result of calling DocumentPicker.getDocumentAsync({ type: 'text/csv' }). This function then prompts to pick a CSV.
  • It then updates the state variable fileUri.
  • Then, it calls the readFile() function with the URI of the picked file.
  • If the file data is successfully read, it parses the CSV data using Papa.parse, which converts the CSV into something that the app can use.
  • If there are no parsing errors , it sets the CSV data using setCsvData(parsedData.data) and marks that a file has been picked.
  • If there are parsing errors, it logs the errors to the console.
readFile()
  • This function is also an async function and is used to read the content of a file.
  • Then it fetches the file content using fetch(uri) and then reads the response using response.text().
  • It returns the file data if successful, otherwise, it returns null.

Pick file

Once the file has been picked. Let's use the parsed CSV and populate it inside the input fields.
Using the below code:

 return (
    <View style={{ flex: 1 }}>
      <View style={{ padding: 20, backgroundColor: 'lightgray' }}>
        {/* Content for the container at the top */}
      </View>
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        {!filePicked && <Button title="Pick a file" onPress={pickDocument} />}
        {filePicked && (
          <FlatList
            data={csvData}
            keyExtractor={(item, index) => index.toString()}
            renderItem={({ item, index }) => (
              <View style={{ flexDirection: 'row', alignItems: 'center' }}>
                {item.map((cell, cellIndex) => (
                  <TextInput
                    key={cellIndex}
                    style={{ margin: 5, borderWidth: 1, padding: 10, fontSize: 16 }}
                    value={cell}
                    onChangeText={(text) => handleCellChange(text, index, cellIndex)}
                  />
                ))}
              </View>
            )}
          />
        )}
        {filePicked && <Button title="Export file" onPress={exportCsv}  style={{ padding: 20, }}/>}
      </View>
         </View>
  );

Enter fullscreen mode Exit fullscreen mode

We use FlatList to use the CSV data to render the data separately inside TextInput for each data inside the CSV file.
We use a function called handleCellChange() to store the changes made in the TextInput.
The Export File button is also added to export the file once edited.

HandleCellChange:

 const handleCellChange = (text, rowIndex, cellIndex) => {
    setCsvData((prevData) => {
      const newData = [...prevData];
      if (!newData[rowIndex]) {
        newData[rowIndex] = [ ];
      }
      newData[rowIndex][cellIndex] = text;
      return newData;
    });
  };

Enter fullscreen mode Exit fullscreen mode
  • This function accepts three parameters.
    • Text - The new value entered into the cell.
    • rowIndex - The index id of the row containing the cell.
    • cellIndex - The cell's index ID.
  • The setCsvData() function uses the data from previous entries in the imported file.
  • If any data in the row is empty, it appears as an empty input field.
  • Then, if a new input is entered, it is updated within the specified rowIndex and cellIndex. It then provides us with updated data.

Populating the data

Export CSV:

 const exportCsv = async () => {
    if (fileUri) {
      try {
        const modifiedCsvContent = Papa.unparse(csvData);
        await exportCsvFile(modifiedCsvContent);
      } catch (error) {
        console.error("Error exporting modified CSV:", error);
      }
    } else {
      console.error("No file selected");
    }
  }; 

Enter fullscreen mode Exit fullscreen mode
  • This function checks the file Uri and unparses the csvData using Papaparse and stores it as modifiedCsvFile.
  • It is then used inside the exportCsvFile function to export it. Export File
exportCsvFile:
const exportCsvFile = async (modifiedCsvContent, fileName = "File.csv") => {
    try {
      const permissions =
        await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
      if (!permissions.granted) {
        alert("Permissions denied");
        return;
      }

      try {
        await FileSystem.StorageAccessFramework.createFileAsync(
          permissions.directoryUri,
          fileName,
          "text/csv"
        )
          .then(async (uri) => {
            await FileSystem.writeAsStringAsync(uri, modifiedCsvContent, {
              encoding: FileSystem.EncodingType.UTF8,
            });
            alert("CSV file exported successfully");
            setFilePicked(false);
          })
          .catch((e) => {
            console.error("Error saving CSV file:", e);
          });
      } catch (e) {
        console.error("Error creating CSV file:", e);
      }
    } catch (err) {
      console.error("Error reading file:", err);
    }
  };

Enter fullscreen mode Exit fullscreen mode
  • This is an async function which takes two parameters, a CSV data and file name.
  • In the first try block, it request the device storage to access and store the files using FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
  • If the permission is not granted it will return to the app screen
  • If the permissions are granted, then it creates a new file with the file name and type of the file using FileSystem.StorageAccessFramework.createFileAsync(permissions.directoryUri,fileName,"text/csv" )
  • Then using the directory Uri, we write the modified csv content to the file created using await FileSystem.writeAsStringAsync(uri, modifiedCsvContent, { encoding: FileSystem.EncodingType.UTF8,});
  • If the writing of file content is successful, then it will display a popup displaying ”CSV file exported successfully”.
  • And we set the setPickedFile to false so that we can pick a new file once again. Permissions Success Alert

Full code:

import React, { useState } from "react";
import { View, Button, FlatList, TextInput, SafeAreaView } from "react-native";
import * as DocumentPicker from "expo-document-picker";
import Papa from "papaparse";
import * as FileSystem from "expo-file-system";

const App = () => {
  const [fileUri, setFileUri] = useState(null);
  const [csvData, setCsvData] = useState([]);
  const [filePicked, setFilePicked] = useState(false);

  const pickDocument = async () => {
    console.log("Pick document function called");
    try {
      const result = await DocumentPicker.getDocumentAsync({});
      if (result.canceled === false) {
        setFileUri(result.assets[0].uri);
        const fileData = await readFile(result.assets[0].uri);
        if (fileData) {
          const parsedData = Papa.parse(fileData);
          if (parsedData.errors.length > 0) {
            console.error("Error parsing CSV:", parsedData.errors);
          } else {
            setCsvData(parsedData.data);
            setFilePicked(true);
          }
        } else {
          console.error("Failed to read file data");
        }
      }
    } catch (error) {
      console.error("Error picking document:", error);
    }
  };

  const readFile = async (uri) => {
    console.log("Reading file");
    try {
      const response = await fetch(uri);
      const fileData = await response.text();
      return fileData;
    } catch (error) {
      return null;
    }
  };

  const handleCellChange = (text, rowIndex, cellIndex) => {
    setCsvData((prevData) => {
      const newData = [...prevData];
      if (!newData[rowIndex]) {
        newData[rowIndex] = [];
      }
      newData[rowIndex][cellIndex] = text;
      return newData;
    });
  };

  const exportCsv = async () => {
    if (fileUri) {
      try {
        const modifiedCsvContent = Papa.unparse(csvData);
        await exportCsvFile(modifiedCsvContent);
      } catch (error) {
        console.error("Error exporting modified CSV:", error);
      }
    } else {
      console.error("No file selected");
    }
  };

  const exportCsvFile = async (modifiedCsvContent, fileName = "File.csv") => {
    try {
      const permissions =
        await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
      if (!permissions.granted) {
        alert("Permissions denied");
        return;
      }

      try {
        await FileSystem.StorageAccessFramework.createFileAsync(
          permissions.directoryUri,
          fileName,
          "text/csv"
        )
          .then(async (uri) => {
            await FileSystem.writeAsStringAsync(uri, modifiedCsvContent, {
              encoding: FileSystem.EncodingType.UTF8,
            });
            alert("CSV file exported successfully");
            setFilePicked(false);
          })
          .catch((e) => {
            console.error("Error saving CSV file:", e);
          });
      } catch (e) {
        console.error("Error creating CSV file:", e);
      }
    } catch (err) {
      console.error("Error reading file:", err);
    }
  };

  return (
    <View style={{ flex: 1 }}>
      <View style={{ padding: 20, backgroundColor: 'lightgray' }}>
        {/* Content for the container at the top */}
      </View>
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        {!filePicked && <Button title="Pick a file" onPress={pickDocument} />}
        {filePicked && (
          <FlatList
            data={csvData}
            keyExtractor={(item, index) => index.toString()}
            renderItem={({ item, index }) => (
              <View style={{ flexDirection: 'row', alignItems: 'center' }}>
                {item.map((cell, cellIndex) => (
                  <TextInput
                    key={cellIndex}
                    style={{ margin: 5, borderWidth: 1, padding: 10, fontSize: 16 }}
                    value={cell}
                    onChangeText={(text) => handleCellChange(text, index, cellIndex)}
                  />
                ))}
              </View>
            )}
          />
        )}
        {filePicked && <Button title="Export file" onPress={exportCsv}  style={{ padding: 20, }}/>}
      </View>
      <View style={{ padding: 20, backgroundColor: 'lightgray' }}>
        {/* Content for the container at the top */}
      </View>
    </View>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)