2

I keep getting the statement "do not save non-serializable variables in your state" in almost every google search result - But what happens when I really should?

Progect: I am building an app for deviceS connected via SerialPort (using SerialPort WebAPI). I wish to save the connection instance since I use it throughout all my application and I am honestly tired of passing the instance down and up whenever I need it without react knowing to re-render data and display new data - which is important for me too.

Steps that I have done:

  • It was easy to ignore the non-serializable error using serializableCheck: false:

    export default configureStore({

      reducer: {
          serialport: SerialPortDevicesReducer,
          bluetooth: BluetoothDevicesReducer,
      },
    
      middleware: getDefaultMiddleware => 
          getDefaultMiddleware({
              thunk,
              serializableCheck: false
          }).concat(logger),})
    

But now I am facing the big problem:

  • Whenever I create a connection I get the object that handles that specific SerialPort device object that is connected.

    deviceReducer: { id: 1, instance: SerialPort{[attr and methods here]}, ... }

  • Whenever I use methods like open(), write() or read() it changes the main connection instance object and breaks with that known error:

Error: Invariant failed: A state mutation was detected between dispatches, in the path 'serialport.0.instance.readable'. This may cause incorrect behavior

  • Since It's not serializable I cannot clone it (which I think is the reason?) and then re-assign it + I think cloning a connection instance will cause other device-connection issues.

I ended up writing the connect method case directly in the state with a "promise" new variable to handle the result.

// click in a react component 
const handleConnect = () => {
    try {
        if ( dispatch(connect(device)) ) {
            setActiveStep((prevActiveStep) => prevActiveStep + 1)
            return true
        }
    } 
    catch (e) {
        console.error("Device cannot connect: ", e)
    }
}

// In a file that trigges dispatch() to the reduces
const connect = (deviceId) => async (dispatch, getState) => {
    try {
        dispatch({
            type: "serialport/connect",
            payload: deviceId
        })
    } catch(e) {
        console.log(e)
    }
}

// in reducer
const SerialPortDevicesReducer = (state = initialState, action) => {

    switch (action.type) {

        case 'serialport/connect':
            try {
                return {
                    ...state,
                    [action.payload]: {
                        ...state[action.payload],
                        promise: state[action.payload].instance.open({baudRate: 115200})
                    }
                }
            } catch (e) {
                console.error("Cannot run promise inside reducer: ", e)
            }

This is the only workaround I currently found. And this basically forces me to handle (maybe some complex) things in the reducer instead of just passing data to it. I tried applying the same for the write method:

// click in component
const handleExecute = (command) => {
    try {

        dispatch(writeToSP(device1.device, command))
    } catch (e) {
        console.log(e)
    }
}


// In file which trigges the dispatch()
const writeToSP = (deviceId, command = "Z !\n") => async (dispatch) => {
    let startTime = new Date().getTime()
    let encoder = new TextEncoder()
    try {
        dispatch({
            type: "serialport/write", 
            payload: {
                id: deviceId,
                // cmd: encoder.encode(command), 
                // startTime
            }
        })
    } catch (e) {
        console.error("error writing: ", e)
    }
}


// in reducer 
...

    case 'serialport/write':
        try {
            const writer = state[action.payload.id].instance.writable.getWriter()
        } catch (e) {
            console.error("Cannot run promise inside reducer: ", e)
        }

and again, get the error of "Error: Invariant failed: A state mutation was detected..." which I am guessing a result of it changing other attributes in the SerialPort instance.

Having packages like redux-promise-middleware are awesome, but it seems like an object in my state is the one responsible for its own promise and changes.

How do I handle this specific situation?

Imnotapotato
  • 5,308
  • 13
  • 80
  • 147
  • maybe not the best solution but an easy one: why not save these devices in a globa variable (in an object with a key => device) and tehn just store the key in redux and then use this key respectively a helper method to get the device from the global variable? – OschtärEi Jan 11 '22 at 12:31
  • Sure possible. That's what i have been doing before, but then I won't get any reactiveness. I do want to know what's the status of a device: is it in read mode, write mode, signal data, etc - and update the DOM respectively. Which basically what redux does, just without non-serializable data in its initial setup. The object contains that data. and setting changes will trigger the rendering. I even tried to force the DOM to re-render and my code turned into spaghetti, which was why I chose to use redux other than passing up/down data through components. @OschtärEi – Imnotapotato Jan 11 '22 at 12:36

1 Answers1

3

Simple: don't put it into Redux. Redux is made for data, not for arbirtary external libraries/dependency injection.

If that value will never change after initialization and you do the initialization outside of React, just put it into a global variable that is exported.

If that value will change over time, you should use the Dependency Injection mechanism of React for it: Context. This is really what context is made for - not sharing state values, but global dependencies.

phry
  • 35,762
  • 5
  • 67
  • 81
  • But what if we have to persist the value across app reloads? – Irfan wani Jul 08 '23 at 10:09
  • 1
    @Irfanwani Then you need to separate the non-serializable part from the value. Redux or not, you'll never be able to store something non-serializable and restore it later in the same form without manually splitting out the serializable part and restoring from that later. It's the literal definition of "serializable". – phry Jul 08 '23 at 12:30
  • https://stackoverflow.com/q/76642357/13789135. This is actually my issue. I want to persist the session but not sure how to do so. Can you please have a look. Though I may be wrong here, but I don't find any other way to do this. – Irfan wani Jul 08 '23 at 12:40
  • @Irfanwani sorry I have no idea about the ReactNative session you are talking about there, but I'll repeat: "serializable" means "can be brought into a format that can be saved to and restored from disk". If something is non-serializable, that means it's not possible. Maybe that session library offers a way to do that? – phry Jul 08 '23 at 20:15
  • thanks for your time. I was searching for the same thing, if the library provides any way. Found nothing till now. Will try to find some way. – Irfan wani Jul 09 '23 at 04:19