Generalized State Architecture

I’m working on a ProseMirror application that’s starting to get reasonably complex outside of ProseMirror.

I realized that I really liked the architecture of ProseMirror (the plugin system is awesome) and I wanted to see if I could generalize an entire React frontend state management with a similar structure and actually route all transactions through a global state system.

I ended up building 3 different prototypes (live demo here, just a text editor with multiple tabs):

The reason I’m posting here is because I’m trying to understand:

why is EditorState a class as opposed to a plain JSON object?

This decision seems to run pretty deep, all the way down to the Node and Selection classes. It seems like the only reason to use a class is to add helper methods to the classes for syntactic convenience. Is that correct?

To give you some context: in the first example, I model every state object as a class like:

class SomeState {
  apply(action): SomeState
  toJSON(): any
  static fromJSON(json): SomeState
}

In the second example, there are no classes and everything is pure-functional.

type StateMachine<S, A> = {
	init: () => S
	update: (state: S, action: A) => S
}

The first one needed 458 lines of code whereas the second needed 364 lines of code: 20% less. This can be accounted for by the fact that the pure functional example doesn’t need any serialization/deserialization.

In practice, I can imagine that using classes is really convenient when the state model is really complicated. Methods like node.childAfter() would need to be functions like childAfter(node) meaning you have to import a bunch of methods all of the time. Also state.tr would need to be new Transaction(state)

In any case, I was hoping maybe you (@marijn) could share some of your thoughts on these trade-offs. Did you consider a pure-functional approach and if so, what led you to your decision to use classes?

Thanks,

Chet

2 Likes

No, it is also so that we can control the internal data structures used in order to optimize functional updates and possibly other things. See for example the use of rope-sequence in the undo history, and at one point I was planning to use a representation other than a flat array for very long fragments (though that turned out to not be enough of a win to justify the complexity).

2 Likes

I see. So you things like serializing to a JSON array but but using a LinkedList internally… That makes sense!