Как организовать сериализацию в Redux и избежать ошибки: non-serializable value

от автора

Введение

Мы пишем онлайн-компилятор для отладки ИИ-сервисов. И статья — это скорее приглашение к обсуждению. Хочу поделиться практическим подходом, который показался простым и удобным в этом проекте. Ваши комментарии и критика будут очень полезны.

Компилятор выглядит достаточно привычно: в левой части экрана находится редактор с вкладками для файлов, а справа — поля для ввода и вывода данных от ИИ-сервисов. Пользователи могут создавать, загружать, скачивать, переименовывать и удалять файлы. Файлы также должны кэшироваться в браузере.

Структура файла в редакторе (псевдокод)
export default class UserFile {     private id: string;     private name: string;     private fullName: string;     private extension: TUserFileExtension;     private downloadedState: TUserFileIsDownloaded;     private content: TUserFileContent;     private cachedState: TuserFileIsCached;    // Методы сериализации   public static fromSerializable(data: IUserFileSerializable): UserFile {}   public toSerializable(): IUserFileSerializable {}    // Методы для изменения и получения значений атрибутов файла.   public setId(id: string): void {}   public setName(name: string): void {}   public setExtension(extension: TUserFileExtension): void {}   public setDownloadedState(state: TUserFileIsDownloaded): void {}   public setFullName(fullName: string): void {}   public setContent(content: TUserFileContent): void {}   public setCachedState(state: TUserFileIsCached): void {}    // Методы для получения значений атрибутов файла.   public getId(): string {}   public getName(): string {}   public getExtension() {}   public getDownloadedState() {}   public getFullName() {}   public getContent() {}   public getCachedState() {}    // Вспомогательные методы для обработки имени файла и его расширения.   private createFullName(name: string, extension: TUserFileExtension): string {}    // Извлекает имя файла из полного имени.   private getNameFromFullName(fullName: string): string {}      // Извлекает расширение файла из полного имени.   private getExtensionFromFullName(fullName: string): TUserFileExtension {} }

Проблема

Изначально я отправлял в Redux экземляры класса UserFile и ни о чем не парился. Все работало замечательно. И все продолжало работать без сериализации. Мазолило глаз только куча ошибок A non-serializable value в консоли браузера.

При попытке сохранть в Redux экземпляр класса UserFile

При попытке сохранть в Redux экземпляр класса UserFile

В доке Redux описано, что мы можем и не сериализовывать данные, если нас не смущает регидратация и time-travel debugging. Почему же тогда при нарушении вылетает не Warning, а целая ошибка?

Тут стоит капнуть в философию Redux. И понять, что всё же нас должна смущать невозможность регидратации при отправке файлов на endpoint.

Основная цель Redux

Сделать состояние приложения предсказуемым и легко управляемым. В этом контексте сериализация состояния — это не просто техническое требование, а ключевая часть философии Redux. И вот почему Redux так строго относится к сериализуемости данных:

  1. Time-travel debugging: несериализуемые объекты (например, экземпляры классов) могут не воспроизводить состояние корректно при «перемотке», так как могут не сохранять методы и прототипы, нарушая предсказуемость.

  2. Регидратация и серверный рендеринг (SSR): состояние Redux часто сохраняется для последующего восстановления — например, между сессиями или при серверном рендеринге.

То есть в нашем случае, отправленные файлы на endpoint, могут привести к ошибке.

Если состояние содержит несериализуемые (объект с примитивными типами) объекты, его сложно или невозможно восстановить корректно.

То есть риск потерять функции класса UserFile достаточно велик.

Отключение проверки сериализации

Вы можете, конечно, все же проигнорировать требования и отключить проверку, добавив middleware:

import { configureStore } from '@reduxjs/toolkit';  const store = configureStore({   reducer: rootReducer,   middleware: (getDefaultMiddleware) =>     getDefaultMiddleware({       serializableCheck: false, // отключаем проверку сериализуемости     }), });

Решение кейса

В нашем кейсе я все-таки сериализовал данные. И выглядит это следующим образом:

У нас есть класс UserFile, отвечающий за файл и его контент (описан в начале статьи). И есть класс FilesService, отвечающий за работу со множеством файлов, а также, архивирование и разархивирование файлов. Ниже приведены методы классов и их использование в компонентах и Redux.

Интерфейс сериализованного файла

export interface IUserFileSerializable {     id: string;     name: string;     fullName: string;     extension: TUserFileExtension; // string     downloadedState: TUserFileIsDownloaded; // boolean     content: TUserFileContent; // string     cachedState: TUserFileIsCached; // boolean }

Класс UserFile

// Метод для создания объекта UserFile из сериализованных данных, // используемых для хранения в Redux. public static fromSerializable(data: IUserFileSerializable): UserFile {     return new UserFile(       data.id,       data.name,       data.extension,       data.downloadedState,       data.content,       data.cachedState     ); }  // Метод для преобразования UserFile в формат, который можно // сохранить в Redux. Возвращает объект интерфейса IUserFileSerializable. public toSerializable(): IUserFileSerializable {     return {       id: this.id,       name: this.name,       fullName: this.fullName,       extension: this.extension,       downloadedState: this.downloadedState,       content: this.content,       cachedState: this.cachedState,     }; }

Класс FilesService

// Сериализация public static toSerializableFiles(files: TUserFiles): IUserFileSerializable[] {     return files.map(file => file.toSerializable()); }  // Десериализация public static fromSerializableFiles(serializedFiles: IUserFileSerializable[]): TUserFiles {     return serializedFiles.map(fileData => UserFile.fromSerializable(fileData)); }

Слайс файлов в Redux

const initialState: IUserFilesSlice = {     files: files,     currentFileId: initialCurrentFileKey, };  export const filesSlice: Slice<IUserFilesSlice> = createSlice({     name: "projectFiles",     initialState,     reducers: {         setCurrentFileId: (state, action: PayloadAction<string>) => {           // Устанавливает текущий идентификатор файла.           state.currentFileId = action.payload;         },                updateFile: (state, action: PayloadAction<IUserFileSerializable>) => {           if (!state.files) {               return;           }           // Обновляет данные файла в массиве файлов по его идентификатору.           const index = state.files.findIndex((file: IUserFileSerializable) => file.id === action.payload.id);           if (index !== -1) {               state.files[index] = action.payload;           }         },                addFile: (state, action: PayloadAction<IUserFileSerializable>) => {           // Добавляет новый файл в массив файлов.           state.files.push(action.payload);         },                removeFile: (state, action: PayloadAction<string>) => {           if (!state.files) {               return;           }           // Удаляет файл из массива по его идентификатору.           state.files = state.files.filter((file: IUserFileSerializable) => file.id !== action.payload);         },                replaceFiles: (state, action: PayloadAction<IUserFileSerializable[]>) => {           // Заменяет весь массив файлов новыми данными.           state.files = action.payload;         },                deleteAllFiles: (state) => {           // Удаляет все файлы из состояния.           state.files = [];         },     }, });

Использование файлов в компонентах

const serializedFiles = useSelector(   (state: TRootState) => state.projectFiles.files );  // Преобразуем сериализованные файлы обратно в объекты UserFile. const [files, setFiles] = useState<TUserFiles>(   FilesService.fromSerializableFiles(serializedFiles) );

Мутация файлов из компонентов

const addFilesToWorkspace = (files: TUserFiles): void => {   const serializableFiles: IUserFileSerializable[] =         FilesService.toSerializableFiles(files);   // Отправляем сериализованные файлы в Redux   dispatch(replaceFiles(serializableFiles)); };

Заключение

В этом подходе сериализация и десериализация позволили сохранить данные в Redux в подходящем формате, не жертвуя гибкостью и возможностью работы с полноценными объектами класса. Если вам знакомы подобные ситуации, делитесь опытом в комментариях — буду рад обсудить альтернативные решения!


ссылка на оригинал статьи https://habr.com/ru/articles/857430/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *