lundi 22 mars 2021

Redux/Ngrx state/reducer design for handling state with dynamic fields that may not exist unless initialized

The details below rely on pseudo code, so please don't assume they're syntactically/sematnically correct Redux/JavaScript/NgRx. My question is a design question rather than implementation specific.

{
  reports: {
    companyAlpha: {
      ...
    },
    companyBeta: {
      ...
    },
  },
}

Suppose I have a state like this, reports contains a 'map' of company object. The keys in reports are dynamic and can't be assumed to exist. I'm getting the exigence using some other means, doesn't matter for my question.

My question is what's the right design pattern here for handling this sort of dynamic state?

Because the issue comes down to the reducers mainly. Because we have to initialize the report entry,

case INIT_REPORT:
  return {
    ...state,
    reports: {
      // add a new entry in reports based on new action
    }
  }

So that means when we have to perform some action like updating a field inside a report,

case UPDATE_REPORT_VALUE_X:
  // first have to check if the report eixsts
  const report = state.reports[action.reportId] // here `action.reportId` is like `companyAlpha` etc.

  if (!report) { return state; } // we can't update because it's not initialized!

  return {
    ...state,
    reports: {
      ...state.reports,
      [action.reportId]: {
        ... // update it however
      }
    }
  }

But now we have to do the if check before we perform an update to see if the report is initialized otherwise the action can't proceed.

Is there a better way to do this so you make 'impossible-states-impossible' (here make it impossible to perform a UPDATE_REPORT_VALUE_X before INIT? Given a state machine, all the further states should be inaccessible unless you've reached the INITIALIZED state from the UNINITIALIZED initial state.

For example, a possible state machine,

  (UNINITIALIZED)
        |
        |
    action: INIT
        |
        v
   (INITIALIZED)
        |
        |------ action: UPDATE_REPORT_VALUE_X ---> (STATE_A)
        |------ action: UPDATE_REPORT_VALUE_Y ---> (STATE_B)
        |------ action: UPDATE_REPORT_VALUE_Z ---> (STATE_c)

Is it possible to do something like this? Enforce INIT before UPDATE_REPORT_VALUE_X is a possible action?

Again the big concern is that if we're not initialized, every single reducer will need to do a is-initialized check making each reducer 2x as complex as it needs to be (two branch paths)

An analogous(?) design in functional programming is that of a monadic Result/Maybe type. If a value is Error/None you can still perform actions along the 'good' path without having to do a if check in each stage of the pipeline. This enables the functions at each stage to avoid the 2x complexity they otherwise would suffer from if you didn't make use of a Result/Maybe (monadic) type.

(forgive me if I used 'monadic' incorrectly here)

Aucun commentaire:

Enregistrer un commentaire