The post has been translated automatically. Original language: Russian
We are writing an online compiler for debugging AI services. And the article is more of an invitation to a discussion. I want to share a practical approach that seemed simple and convenient in this project. Your comments and criticism will be very helpful.
The compiler looks quite familiar: on the left side of the screen there is an editor with tabs for files, and on the right there are fields for entering and outputting data from AI services. Users can create, upload, download, rename, and delete files. The files should also be cached in the browser.
File structure in the editor (pseudocode)
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;
// Serialization methods
public static fromSerializable(data: IUserFileSerializable): UserFile {}
public toSerializable(): IUserFileSerializable {}
// Methods for changing and retrieving file attribute values.
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 {}
// Methods for getting file attribute values.
public getId(): string {}
public getName(): string {}
public getExtension() {}
public getDownloadedState() {}
public getFullName() {}
public getContent() {}
public getCachedState() {}
// Auxiliary methods for processing the file name and its extension.
private createFullName(name: string, extension: TUserFileExtension): string {}
// Extracts the file name from the full name.
private getNameFromFullName(fullName: string): string {}
// Extracts the file extension from the full name.
private getExtensionFromFullName(fullName: string): TUserFileExtension {}
}
Initially, I sent UserFile class instances to Redux and didn't worry about anything. Everything worked great. And everything continued to work without serialization. It was annoying to see only a bunch of mistakes A non-serializable value in the browser console.

The Redux doc describes that we don't have to serialize data if we're not bothered by rehydration and time-travel debugging. Then why is it that when there is a violation, not a Warning crashes, but a whole error?
It's worth delving into the philosophy of Redux. And to understand that we should still be confused by the impossibility of rehydration when sending files to the endpoint.
Make the application state predictable and easily manageable. In this context, state serialization is not just a technical requirement, but a key part of the Redux philosophy. And that's why Redux is so strict about data serializability.:
- Time-travel debugging: unserializable objects (for example, instances of classes) may not reproduce the state correctly during "rewinding", as they may not save methods and prototypes, violating predictability.
- Rehydration and Server Rendering (SSR): The state of Redux is often saved for later restoration, for example, between sessions or during server rendering.
That is, in our case, files sent to the endpoint may lead to an error.
If the state contains non-serializable (object with primitive types) objects, it is difficult or impossible to restore it correctly.
In other words, the risk of losing the functions of the UserFile class is quite high.
Of course, you can still ignore the requirements and disable the verification by adding middleware.:
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // disabling the serializability check
}),
});
In our case, I serialized the data anyway. And it looks like this:
We have a UserFile class responsible for the file and its content (described at the beginning of the article). And there is a FilesService class that is responsible for working with a variety of files, as well as archiving and unzipping files. The following are class methods and their use in components and Redux.
Interface of the serialized file
export interface IUserFileSerializable {
id: string;
name: string;
fullName: string;
extension: TUserFileExtension; // string
downloadedState: TUserFileIsDownloaded; // boolean
content: TUserFileContent; // string
cachedState: TUserFileIsCached; // boolean
}
The UserFile class
// A method for creating a UserFile object from serialized data
// used for storage in Redux.
public static fromSerializable(data: IUserFileSerializable): UserFile {
return new UserFile(
data.id,
data.name,
data.extension,
data.downloadedState,
data.content,
data.cachedState
);
}
// A method for converting a UserFile to a format that can
// be saved in Redux. Returns an object of the IUserFileSerializable interface.
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 class
// Serialization
of public static toSerializableFiles(files: TUserFiles): IUserFileSerializable[] {
return files.map(file => file.toSerializable());
}
// Deserialization
of public static fromSerializableFiles(serializedFiles: IUserFileSerializable[]): TUserFiles {
return serializedFiles.map(fileData => UserFile.fromSerializable(fileData));
}
Slicing files in Redux
const initialState: IUserFilesSlice = {
files: files,
currentFileId: initialCurrentFileKey,
};
export const filesSlice: Slice<IUserFilesSlice> = createSlice({
name: "projectFiles",
initialState,
reducers: {
setCurrentFileId: (state, action: PayloadAction<string>) => {
// Sets the current file ID.
state.currentFileId = action.payload;
},
updateFile: (state, action: PayloadAction<IUserFileSerializable>) => {
if (!state.files) {
return;
}
// Updates the file data in the file array by its ID.
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>) => {
// Adds a new file to the file array.
state.files.push(action.payload);
},
removeFile: (state, action: PayloadAction<string>) => {
if (!state.files) {
return;
}
// Deletes a file from the array by its ID.
state.files = state.files.filter((file: IUserFileSerializable) => file.id !== action.payload);
},
replaceFiles: (state, action: PayloadAction<IUserFileSerializable[]>) => {
// Replaces the entire file array with new data.
state.files = action.payload;
},
deleteAllFiles: (state) => {
// Deletes all files from the state.
state.files = [];
},
},
});
Using files in components
const serializedFiles = useSelector(
(state: TRootState) => state.projectFiles.files
);
// Convert the serialized files back to UserFile objects.
const [files, setFiles] = useState<TUserFiles>(
FilesService.fromSerializableFiles(serializedFiles)
);
Mutating files from components
const addFilesToWorkspace = (files: TUserFiles): void => {
const serializableFiles: IUserFileSerializable[] =
FilesService.toSerializableFiles(files);
// Sending the serialized files to Redux
dispatch(replaceFiles(serializableFiles));
};
In this approach, serialization and deserialization made it possible to save data in Redux in a suitable format without sacrificing flexibility and the ability to work with full-fledged class objects. If you are familiar with such situations, share your experience in the comments — I will be glad to discuss alternative solutions!
Мы пишем онлайн-компилятор для отладки ИИ-сервисов. И статья — это скорее приглашение к обсуждению. Хочу поделиться практическим подходом, который показался простым и удобным в этом проекте. Ваши комментарии и критика будут очень полезны.
Компилятор выглядит достаточно привычно: в левой части экрана находится редактор с вкладками для файлов, а справа — поля для ввода и вывода данных от ИИ-сервисов. Пользователи могут создавать, загружать, скачивать, переименовывать и удалять файлы. Файлы также должны кэшироваться в браузере.
Структура файла в редакторе (псевдокод)
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 описано, что мы можем и не сериализовывать данные, если нас не смущает регидратация и time-travel debugging. Почему же тогда при нарушении вылетает не Warning, а целая ошибка?
Тут стоит копнуть в философию Redux. И понять, что всё же нас должна смущать невозможность регидратации при отправке файлов на endpoint.
Сделать состояние приложения предсказуемым и легко управляемым. В этом контексте сериализация состояния — это не просто техническое требование, а ключевая часть философии Redux. И вот почему Redux так строго относится к сериализуемости данных:
- Time-travel debugging: несериализуемые объекты (например, экземпляры классов) могут не воспроизводить состояние корректно при "перемотке", так как могут не сохранять методы и прототипы, нарушая предсказуемость.
- Регидратация и серверный рендеринг (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 в подходящем формате, не жертвуя гибкостью и возможностью работы с полноценными объектами класса. Если вам знакомы подобные ситуации, делитесь опытом в комментариях — буду рад обсудить альтернативные решения!