Search code examples
reactjsreduxxlsxredux-saga

Import XLSX file in React app with redux-sagas


I am new to redux-sagas and struggling to make the code below work.

This is the saga i am working with, it is supposed to read an xlsx file and dispatch an action when done. The action should be dispatched AFTER the file has been completely read, but it is not and i have no idea what i am doing wrong.

import { put } from 'redux-saga/effects';
import * as XLSX from 'xlsx';

import * as actionTypes from '../actions/actionTypes';

export function* importFileSaga(action) {
  console.log('[importFileSaga]', action.fileToUpload);
  const response = yield importFile(action.fileToUpload);
  console.log('[importFileSaga]', response);
  yield put ({type: actionTypes.SET_DATA, payload: response});
}

const importFile = file2upload => {
  console.log('[importFile]', file2upload);
  const reader = new FileReader();
  reader.onload = (evt) => {
    /* Parse data */
    const bstr = evt.target.result;
    const wb = XLSX.read(bstr, { type: 'binary' });
    /* Get first worksheet */
    const wsname = wb.SheetNames[0];
    const ws = wb.Sheets[wsname];
    const data = XLSX.utils.sheet_to_csv(ws, { header: 1 });

    const processedData = processData(data);
    console.log('[importFile]',processedData);

    return processedData;
  };
  reader.readAsBinaryString(file2upload);

}

// process CSV data
const processData = dataString => {
    const dataStringLines = dataString.split(/\r\n|\n/);
    const headers = dataStringLines[0].split(/,(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/);

    const list = [];
    for (let i = 1; i < dataStringLines.length; i++) {
      const row = dataStringLines[i].split(/,(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/);
      if (headers && row.length === headers.length) {
        const obj = {};
        for (let j = 0; j < headers.length; j++) {
          let d = row[j];
          if (d.length > 0) {
            if (d[0] === '"')
              d = d.substring(1, d.length - 1);
            if (d[d.length - 1] === '"')
              d = d.substring(d.length - 2, 1);
          }
          if (headers[j]) {
            obj[headers[j]] = d;
          }
        }

        // remove the blank rows
        if (Object.values(obj).filter(x => x).length > 0) {
          list.push(obj);
        }
      }
    }

    // prepare columns list from headers
    const columns = headers.map(c => ({
      name: c,
      selector: c,
    }));

    const processedData = {header: columns, data: list};
    console.log('[processData]', processedData);
    return processedData;
}

When a new file is uploaded, this is the sequence of output i receive in the console

[importFileSaga] 
File { name: "test.csv", lastModified: 1603705847000, webkitRelativePath: "", size: 1580, type: "text/csv" }
importFile.js:7
[importFile] 
File { name: "test.csv", lastModified: 1603705847000, webkitRelativePath: "", size: 1580, type: "text/csv" }
importFile.js:14
[importFileSaga] undefined importFile.js:9
SET_DATA reducers.js:31
undefined
TypeError: action.payload is undefined
The above error occurred in task importFileSaga
    created by takeEvery(UPLOAD_FILE_START, importFileSaga)
    created by watchImport
Tasks cancelled due to error:
takeEvery(UPLOAD_FILE_START, importFileSaga)
takeEvery(TEST_SAGA_INIT, importDeviceSaga) index.js:1
[processData] 
Object { header: (50) […], data: (2) […] }
importFile.js:71
[importFile] 
Object { header: (50) […], data: (2) […] }

This is unexpected...but i assume i might still be a bit confused. I'd expect simething like this

[importFileSaga] 
File { name: "test.csv", lastModified: 1603705847000, webkitRelativePath: "", size: 1580, type: "text/csv" }
importFile.js:7
[importFile] 
File { name: "test.csv", lastModified: 1603705847000, webkitRelativePath: "", size: 1580, type: "text/csv" }
importFile.js:14
[processData] 
Object { header: (50) […], data: (2) […] }
importFile.js:71
[importFile] 
Object { header: (50) […], data: (2) […] }
[importFileSaga] some data here importFile.js:9
SET_DATA reducers.js:31

I understand that this line of code

const response = yield importFile(action.fileToUpload);

means that the line below that is not executed until importFile is finished. Is that correct? How can i fix it?

-- EDIT --

Thanks to markerikson i modified the importFile function

const importFile = file2upload => {
  return new Promise((resolve, reject) => {
    console.log('[importFile]', file2upload);
    const reader = new FileReader();
    reader.onload = (evt) => {
      /* Parse data */
      const bstr = evt.target.result;
      const wb = XLSX.read(bstr, { type: 'binary' });
      /* Get first worksheet */
      const wsname = wb.SheetNames[0];
      const ws = wb.Sheets[wsname];
      const data = XLSX.utils.sheet_to_csv(ws, { header: 1 });

      const processedData = processData(data);
      console.log('[importFile]',processedData);

      //return processedData;
      resolve(processedData);
    };
    reader.readAsBinaryString(file2upload);
  });

}

and now it works like a charm!!


Solution

  • There's two problems here conceptually:

    • yield importFile() is going to pass undefined to the saga middleware, which will presumably keep right on going
    • The file reader will not execute the callback right away, and your code really needs to wait until it's done.

    You'll need to restructure the code so that it waits until the reader logic is done.

    One option might to be to manually create a Promise inside of importFile and return it, and resolve the promise at the end of the .onload callback.