Search code examples
react-nativeaxiosmobx-state-tree

Cancelling upload request before destroying object makes mobx-state-tree throw Cannot modify [dead] errors


I have a React Native app where I want to upload some files using Axios.

I've made a mobx-state-tree store for file uploads, and each file has its own CancelTokenSource, which is sent to the Axios network call.

When an upload is in progress, I try to cancel the upload, and then destroy the item.

The simplest way is like I show below, by destroying the item in the store, and then have an beforeDestroy() hook that cancels the upload. But that approach makes mobx-state-tree show the error in the screenshot.

I've also tried calling the file.cancelTokenSource.cancel() explicitly before destroying the item. Same error. I suspect that the operation is not fully cancelled when the cancel() returns, but since it's not an async function, I cannot await its completion.

When I just call the cancel() without destroying, it cancels just fine, so I'm pretty sure that it's a timing issue, where the destroy(file) is called too soon, before cancel() has cleaned up after itself.

What to do here?

enter image description here

file-upload-store.ts

import { destroy, flow, Instance, types } from 'mobx-state-tree'

import { FileUpload, IFileUpload } from '../entities/file-upload/file-upload'
import { getApi } from '../../store-environment'

/**
 * Store for handling the FileUpload
 */
export const FileUploadStore = types
  .model('FileUploadStore')
  .props({
    files: types.array(FileUpload),
  })
  .actions((self) => {
    const api = getApi(self)

    const add = (uri: string, name: string, type: string, size: number) => {
      const file = FileUpload.create({
        uri,
        name,
        type,
        size,
      })
      self.files.push(file)
      upload(file)
    }

    const remove = (file: IFileUpload) => {
      destroy(file)
    }

    const cancel = (file: IFileUpload) => {
      // also tried this - with no luck
      // file.cancelTokenSource.cancel()
      destroy(file)
    }

    const upload = flow(function* (file: IFileUpload) {
      file.status = 'pending'
      file.uploadedBytes = 0
      const { uri, name, type } = file

      try {
        const id = yield api.uploadFile(uri, name, type, file.setProgress, file.cancelTokenSource.token)
        file.status = 'completed'
        file.fileUploadId = id
      } catch (error) {
        file.status = 'failed'
        file.error = error.message
      }
    })

    return {
      afterCreate() {
        // Avoid persistance
        self.files.clear()
      },
      remove,
      cancel,
      retry: upload,
      add,
    }
  })

export type IFileUploadStore = Instance<typeof FileUploadStore>

file-upload.ts

import { Instance, SnapshotIn, types } from 'mobx-state-tree'
import { CancelToken } from 'apisauce'

/**
 * FileUpload contains the particular data of a file, and some flags describing its status.
 */
export const FileUpload = types
  .model('FileUpload')
  .props({
    name: types.string,
    type: types.string,
    uri: types.string,
    size: types.number,
    // set if an arror occours
    error: types.maybe(types.string),
    status: types.optional(types.enumeration(['pending', 'completed', 'failed']), 'pending'),
    // updated by progressCallback
    uploadedBytes: types.optional(types.number, 0),
    // assigned when response from backend is received
    fileUploadId: types.maybe(types.string),
  })
  .volatile(() => ({
    cancelTokenSource: CancelToken.source(),
  }))
  .actions((self) => ({
    setProgress(event: ProgressEvent) {
      self.uploadedBytes = event.loaded
    },
    beforeDestroy() {
      self.cancelTokenSource?.cancel()
    },
  }))

export interface IFileUpload extends Instance<typeof FileUpload> {}
// SnapshotIn, used for creating input to store: {Model}.create({})
export interface IFileUploadSnapshotIn extends SnapshotIn<typeof FileUpload> {}


Solution

  • You are destroying the FileUpload node and cancelling the axios request nicely, but cancelling the request will throw an error, so you need to make sure that your FileUpload node is still alive before you try to update it in the catch.

    import { destroy, flow, Instance, types, isAlive } from 'mobx-state-tree'
    
    // ...
    
    const upload = flow(function* (file: IFileUpload) {
      const { uri, name, type } = file
    
      file.status = "pending"
      file.uploadedBytes = 0
    
      try {
        const id = yield api.uploadFile(
          uri,
          name,
          type,
          file.setProgress,
          file.cancelTokenSource.token
        )
    
        file.status = "completed"
        file.fileUploadId = id
      } catch (error) {
        if (isAlive(file)) {
          file.status = "failed"
          file.error = error.message
        }
      }
    })