Why I don't use the Activity's Data State

I want to post this topic primary as a feedback from the perspective of a user of A12, but as you will see, I am desperatly looking for a better practice to solve my problem and I would also appreciate any help!
In fact, I am thinking, that this problem should have come up in other projects before me, because it is a general issue with the A12 Activities.

The title says that “I don’t use the Activity’s Data State” - well, that’s not entirely true. Some of the Activities in <PROJECT_NAME> use the intended place in the Redux Store for holding its data state: acitivities[id].dataHolders[id].data. But others don’t, and I will try to explain why. Let’s look at an example Activity: The <PROJECT_NAME>'s EditorActivity. It is the main feature of the <PROJECT_NAME> Application, it is the editor itself and by the way, it is the only Activity that can justify that its data is stored in a global state like Redux (I’ll explain that later). With the editor, a user can edit the structure of a form model, with pages, sections, subsections, groups, row, and so on. So, the data state is nested and this nesting causes the problem.

The problem

When I am implementing the EditorActivity or any other feature, I am always trying to follow Redux patterns, as it is set by A12. So, I would like to use those patterns, when a user is editing the form model, but…

I can’t use explicit Actions and Reducers to change the data state of an Activity.

Writing custom Actions and Reducers seems not to be a considered way to work with the Activity’s state. As far as I know, the only way to change the Activity’s data, after it is loaded by a DataLoader, is to dispatch the Acitivity Action setData. Dispatching an Action is a side-effect and should not be part of a Reducer, obviously. Dispatching Actions is only possible inside connected Components, Sagas (and Middlewares).

Let’s look closer, how I would like to handle the data changes. For example, the following Action and Reducer are handling the usecase when a user updates a node inside the form model.

Action:

export interface UpdateNodeActionPayload {
  node: Partial<Node> & { id: string };
}
export namespace EditorActions {
  export const updateNode = actionCreator<UpdateNodeActionPayload>("UPDATE_NODE");
}

Reducer:

function updateNode(state: Readonly<FormModel>, payload: UpdateNodeActionPayload): FormModel {
  const { id, ...changes } = payload.node;
  const affectedNode = FormModelService.findNode((node) => node.id === id);
  const newState = FormModelService.changeNodeInFormModel(state, { ...affectedNode, ...changes });
  return newState;
}

Actually, it is not me who likes to write Reducers and Actions. It’s Redux. When a node is changed by the user, there are three steps to perform:

  1. Get the whole form model state.
  2. Find the affected node.
  3. Copy and Change the node and all direct ancestors.

The state transition has two more characteristics:

  1. Unaffected siblings, children and indirect ancestors should not be copied and/or changed.
  2. There is no side effect.

Isn’t this exacly what a Reducer is designed for?

Sagas instead of Reducers

Yes, technically I could also implement a Saga that gets triggered by a custom Action like the one I showed you and calls ActivityAction.setData in the end. But I decided not to. It just does not feel right to break with the Redux patterns. I will try to explain my further concerns (besides the fact, that it should be a Reducer):

  • First of all, getting the state (step 1) is more complicated, because I have to go through technical layers (Activities, DataHolders) to select the very domain-specific form model data state.
  • When changing the state (step 3), I suddenly have to worry about immutability and how to correctly change the state inside a Saga. Other people in my team (or my future self) might wonder why I am doing this not-so-trivial object transitions, just to trigger another Action.
  • I have no control on how the state gets modified, after triggering ActivityAction.setData. Does it get copied again? Shallow or deep?

All in all it increases complexity, confusion (or the need of explanation) and also breaks with the mental model Redux-Saga is describing in their documentation, that “a saga … is solely responsible for side effects”. Implementing a Saga instead of a reducer can’t be a good practice.

Side note

There is a way to integrate custom Reducers that handle ActivityActions in A12. By passing a custom DataReducer to ActivityReducers.createActivityReducers I am able to listen to these kind of Actions and it even gives me the right DataHolder that contains the data i want to change. But this way I can’t implement my own explicit Actions, because I can only listen to ActivityActions and that’s why I have to use ActivityActions.setData yet again, and implementing everything outside of the Reducer… this leads to nothing, sadly.

The lesser evil

My personal consequence is to seperate the form model state from the A12 parts of the Redux Store. This way, I have full control over the Actions and Reducers used for this custom part of the global state. Nevertheless, I am still using the Activity’s data, but only for the loading state.

This is what the <PROJECT_NAME> Redux Store looks like:

{
  "authentication": {},
  "models": {},
  "activities": {
    "XXXX": {
      "dataHolders": [
        {
          "data": {
            "formModelReference": "id-xxxx"
          },
          "loadingState": "loaded"
        }
      ]
    },
  },
  "locale": {},
  "application": {},
  "editor": {
    "formModel": {
      "id": "id-xxxx",
      "nodes": [...]
    }
  }
}

Now, the corresponding DataLoader has to set the editor state with the Activity’s state.

I don’t claim that this is a good solution for the problem, because now it conflicts with A12 patterns. It just feels better, and it is more self-explanatory.

There is still the component’s state

As the editor is a quite complex feature, there are multiple places where state transition are getting triggered (or the current state is read). This is the reason, why I need the editor’s state to be global. But recently, we developed another feature that is much simpler, though it contains a similar nested state which is dynamically changing in response to user interactions.

We gathered two findings after implementing this simpler feature. First, it is another example for a nested data state, so, maybe, data nesting is not as specific or uncommon as I thought in the first place. And maybe, a framework like A12 should provide solutions or practices for it. (Actually, that’s why I want to start this discussion now.)

Second, it is much easier to implement (and understandable), if we are using the component’s state, when separating the data state from the Acitivty’s state. In this case, we are using the A12 methods to load the data and store the loaded data, but we are using it only as initial data. When the user interacts and data changes, we are just updating the component’s data, which is a copy of the loaded data.

It looks like this (in the container component):

function Container(props: CompProps): JSX.Element {
	const [nodes, setNodes] = React.useState<Node[]>([]);
	React.useEffect(() => {
		setNodes(props.activity.data.initial);
	}, [props.activity.data]);
  // ...do something with the nodes, when user interacts
  // ...return rendered feature, using the nodes
}

Further personal thoughts

Sometimes I wish, that the A12 Framework would be more flexible, like plugins that I can combine and manage myself. It is nice to put up a feature that fits into the mindset of A12, but as projects will never have the same requirements, there will always be features that don’t fit into the scheme. And then, developers are conflicting with A12 patterns like putting everything in the global Redux state by default, ordered by technical layers (not domain-specific). Redux itself is very open-minded and unstrict with the topic what to put in the global state. A12 seems too inflexible and opinionated for a framework. Why don’t let the project teams manage the state by their own? …This is a very personal perspective, so please, don’t judge me.

Hi @alexander-pure-wind,

I think you missed some points in DataReducer.

Let’s use your example. By using activity’s data reducer, your action & reducer code can be converted to this:

export interface UpdateNodeActionPayload {
  // activityId is a must have property to make your explicit action work with DataReducer
  activityId: string;
  node: Partial<Node> & { id: string };
}
export namespace EditorActions {
  export const updateNode = actionCreator<UpdateNodeActionPayload>("UPDATE_NODE");
}

export const updateNodeDataReducer: ActivityReducers.DataReducer = {
  canHandle(dataHolder: Activity.DataHolder, action: AnyAction): boolean {
    // There should be also some condition checks to make sure the data holder here is your editor's data holder
    return EditorActions.updateNode.match(action);
  },
  reduce(dataHolder: Activity.DataHolder<FormModel>, action: Action<UpdateNodeActionPayload>): Activity.DataHolder<FormModel> {
    // The rest of your updateNode logic will be placed here.
    const { id, ...changes } = action.payload.node;
    const affectedNode = FormModelService.findNode(node => node.id === id);
    const newState = FormModelService.changeNodeInFormModel(dataHolder.data, { ...affectedNode, ...changes });
    return { ...dataHolder, data: newState };
  }
};


In appsetup.ts (or wherever you call ApplicationFactories.createApplicationSetup), register your data reducer as follow.

// ./appsetup.ts
---
const appReducer = combineReducers<object>({
  activities: ActivityReducers.createActivityReducers({
    dataReducers: [
      ...bapFormEngineDataReducers,
      updateNodeDataReducer
    ]
  }),
  ...otherGlobalReducers,
});
---

Then, you can take all advantages of using A12 feature without having to synchronize any A12’s activity features to your global editor state.

Sorry, I cannot find any BAP Client’s official documentation regarding the data reducer for Activity but you can see the typing in their code here (@com.mgmtp.a12/bap-client/lib/core/activity/internal/reducers/index.d.ts).

Please correct me if I was wrong or still missing some points in your post.

Best.

Thanks for the hint @tri-sheer-boulder!
I thought, I tried to listen to my own Actions inside a DataReducer …but maybe you are right and I missed something or did it wrong. I will reproduce it once more and report the result.

You’re welcome :relaxed: I use DataReducer in a newly created product and I don’t see any issues with it.

Well, the thing I didn’t know about last time, is the activityId in the action’s payload. When I correctly pass the activityId, I manage to implement my own custom DataReducer.

(I nearly missed it this time again, but I found the if-clause in the bap-client’s reducer code, which is checking for the presence of this property - and then noticed that I also missed @tri-sheer-boulder’s comment in his extended example.)

To find this obstacle might also be the hardest part for everyone else, who is trying to use this A12 feature.

I’m still not sure, whether this is the right solution for us… Having to pass the activityId in every single action that is affecting the FormModel is not self-explanatory, because it is a global state and not only relevant for one particular Activity. That’s also why I would have to select the right activityId before dispatching any of these actions, and selecting it might not be trivial from outside of the Activity’s scope. At the moment, I think it is still easier and less confusing, when I keep a seperate part of the Redux Store in sync.