Download a file programmatically while providing feedback to the user using Redux

Viewed 396

I have a backend which returns a file to be downloaded.

Currently, when the user does something an action is dispatched which runs the reducer export and it executes the usual trick to download a file. I'm using redux-toolkit.

const mySlice = createSlice({
    name: "my",
    initialState,
    reducers: {
        export: (state) => {
            const link = document.createElement("a")
            link.href = '...'

            document.body.appendChild(link)
            link.click()
            link.parentNode?.removeChild(link)
        },
    }
})

And this works. However, I would like to provide a feedback to the user while the download is happening, i.e. a modal dialog. So I have defined a isExporting property in the state, and there is a component which shows the dialog depending on this property.

However, the reducer written as this does not work:

    export: (state) => {
        state.isExporting = true

        const link = document.createElement("a")
        link.href = '...'

        document.body.appendChild(link)
        link.click()
        link.parentNode?.removeChild(link)

        state.isExporting = false
    },

because I believe I'm introducing side effects, and moreover the state will be set after the reducer has been executed and not in between.

What is the correct way to do this?

1 Answers

Create a async action createAsyncThunk and define a extraReducer with cases:

For this example i create async function download

const fakeDownload = async () =>
  new Promise((resolve) =>
    setTimeout(() => {
      const link = document.createElement("a");
      link.href = "...";
      document.body.appendChild(link);
      link.click();
      link.parentNode?.removeChild(link);

      resolve();
    }, 1000) // I set a timeout to emulate a request.
  );

Create async action:

export const downloadFile = createAsyncThunk(
  "download/file",
  async (_, thunkApi) => {
    try {
      return fakeDownload();
    } catch (e) {
      return thunkApi.rejectWithValue("Impossible to download");
    }
  }
);

The reducer:

const initialState = {
  isExporting: false
};

export const appSlice = createSlice({
  name: "app",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(downloadFile.pending, (state) => {
      state.isExporting = true;
    });
    builder.addCase(downloadFile.fulfilled, (state) => {
      state.isExporting = false;
    });
    builder.addCase(downloadFile.rejected, (state) => {
      // Do something with error
      state.isExporting = false;
    });
  }
});

export const isExportingSelector = (state) => state.app.isExporting;

export default appSlice.reducer;

Now you can use selector and dispatch action.

You can find a live example here:

Edit goofy-thompson-8bbfc

Related