Auto-translation used

Redux‑those serializations by Kalai uyyymdastyryp, "non‑serializable value" by kateligin boldyrmauga bolady?

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.:

  1. 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.
  2. 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!

Comments 1

Login to leave a comment