@kfedor

Как работать с асинхронными функциями в reducer redux?

Имею следующий код:

import { createSlice } from '@reduxjs/toolkit';

export const canvasSlice = createSlice({
  name: 'canvas',
  initialState: {
    canvas: null,
    undoList: [],
    redoList: [],
  },
  reducers: {
    setCanvas: (state, action) => {
      state.canvas = action.payload;
    },
    pushToUndo: (state, action) => {
      state.undoList.push(action.payload);
    },
    pushToRedo: (state, action) => {
      state.redoList.push(action.payload);

    },
    undo: (state) => {
      const ctx = state.canvas.getContext('2d');
      if (state.undoList.length) {
        const dataURL = state.undoList.pop();
        state.redoList.push(dataURL);
        const img = new Image();
        img.src = dataURL;
        img.onload = () => {
          ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
          ctx.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
        };
      } else {
        ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
        console.log('Нет элементов для отката');
      }
    },
    redo: (state) => {
      const ctx = state.canvas.getContext('2d');
      if (state.redoList.length) {
        const dataURL = state.redoList.pop();
        state.undoList.push(state.canvas.toDataURL());
        const img = new Image();
        img.src = dataURL;
        img.onload = () => {
          ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
          ctx.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
        };
      }
    },
  },
});

export const { setCanvas, pushToUndo, pushToRedo, undo, redo } = canvasSlice.actions;

export default canvasSlice.reducer;

При выполнении кода на строках с ctx.clearRect и ctx.drawImage появляется ошибка:

Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
    at img.onload

Почему это происходит, как можно исправить?
  • Вопрос задан
  • 173 просмотра
Решения вопроса 1
TMProject
@TMProject
Frontend developer React/Redux
Проблема заключается в том, что асинхронная функция img.onload выполняется после того, как функция-редюсер закончила свою работу, и к моменту выполнения img.onload объект state.canvas уже был обновлен и утрачен свою возможность использования. Это приводит к ошибке, которую вы видите.

Для решения этой проблемы можно воспользоваться библиотекой Redux-Thunk, которая позволяет создавать асинхронные функции внутри функций-редюсеров. В вашем случае, вы можете заменить pushToUndo и pushToRedo на асинхронные функции, которые будут использовать dispatch для вызова функций-редюсеров после завершения асинхронной операции.

Пример использования Redux-Thunk для вашего случая:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const loadImage = createAsyncThunk(
  'canvas/loadImage',
  async (dataURL, { getState }) => {
    const state = getState().canvas;
    const img = new Image();
    img.src = dataURL;
    return new Promise((resolve, reject) => {
      img.onload = () => {
        const ctx = state.canvas.getContext('2d');
        ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
        ctx.drawImage(img, 0, 0, state.canvas.width, state.canvas.height);
        resolve();
      };
      img.onerror = reject;
    });
  }
);

export const canvasSlice = createSlice({
  name: 'canvas',
  initialState: {
    canvas: null,
    undoList: [],
    redoList: [],
  },
  reducers: {
    setCanvas: (state, action) => {
      state.canvas = action.payload;
    },
    pushToUndo: (state, action) => {
      state.undoList.push(action.payload);
    },
    pushToRedo: (state, action) => {
      state.redoList.push(action.payload);
    },
    undo: (state) => {
      if (state.undoList.length) {
        const dataURL = state.undoList.pop();
        state.redoList.push(dataURL);
        dispatch(loadImage(dataURL));
      } else {
        console.log('Нет элементов для отката');
      }
    },
    redo: (state) => {
      if (state.redoList.length) {
        const dataURL = state.redoList.pop();
        state.undoList.push(state.canvas.toDataURL());
        dispatch(loadImage(dataURL));
      }
    },
  },
  extraReducers: (builder) => {
    builder.addCase(loadImage.pending, (state) => {
      // обработка начала загрузки изображения
    });
    builder.addCase(loadImage.fulfilled, (state) => {
      // обработка завершения загрузки изображения
    });
    builder.addCase(loadImage.rejected, (state) => {
      // обработка ошибки загрузки изображения
    });
  },
});

export const { setCanvas, pushToUndo, pushToRedo, undo, redo } = canvasSlice.actions;

export default canvasSlice.reducer
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы