Before proceeding, I'd like to point out that the title of this question was rather difficult to phrase. If a more suitable title should be used, please let me know so that I may change it and make this question more useful to others.
OK, on to the problem… I am currently working on a React/Redux project. A design decision I made was to manage app state and UI almost entirely with (hierarchical) state machines, for a number of reasons (which I won't delve into).
I have taken advantage of Redux to store my state tree in a substate called store.machine
. The rest of the Redux substates are then responsible for storing app 'data.' In this way I have separated out the two concerns so that they don't cross boundaries.
Extending from this, I have also separated concerns on the (React) side – using 'state components' and 'UI components.' State components deal almost entirely with the flow of state, while UI components are those components that get rendered on the screen.
I have three types of state components:
- Node: This kind of state component deals with state branching. It determines which component should be rendered based on its current state (a form of delegation).
- Leaf: This kind of state component exists at the leaves of the state tree. Its job is merely to render a UI component, passing along the necessary 'dispatch' callbacks responsible for updating the state tree.
- Container: This kind of state component encapsulates a Node and UI component to be rendered side-by-side.
For my circumstances, we're only concerned with Node and Leaf components. The problem I'm having is that while the UI components are being rendered based on 'leaf states,' there may be scenarios where 'higher-level' states could factor into how the UI should be rendered.
Take this simplified state structure:
AppState
starts off in the Home
state. When the user clicks the login button, a to_login
action is dispatched. The reducer whose duty is to manage AppState
will receive this action and set the new current state to Login
.
Likewise, after the user types their credentials and validation is done, either a success
or fail
action would be dispatched. Again, this gets picked up by the same reducer who then proceeds to switch to the appropriate state: User_Portal
or Login_Failed
.
The React component structure would look something like this:
Our top-level Node receives AppState
as a prop, checks what the current state is and renders/delegates to one of the child Leaf components.
The Leaf components then render the concrete UI components passing along callbacks to allow them to dispatch the necessary actions (described above) to update state. The dotted line represents the boundary between 'state' and 'ui,' and this boundary is only crossed at Leaf components. This makes it possible to work on State and UI independently, and is therefore something I would like to maintain.
Here is where things get tricky. Imagine for the sake of argument we have a top-level state to describe the language the app is in – let's say English
and French
. Our updated component structure might look like this:
Now our UI components would have to render in the correct language, even though the state describing this is not a Leaf. The Leaf components that deal with the rendering of UI have no concept of parent states and therefore no concept of the language the app is in. And thus, the language state cannot be safely passed to the UI without breaking the model. Either the state/UI boundary line would have to be removed or parent state would need to be passed down to children, both of which are terrible solutions.
One solution is to 'copy' the AppState
tree structure for each language, essentially creating a whole new tree structure per language… like so:
This is almost as bad a solution as the two I described above, and would need an escalating number of components to manage things.
The more appropriate solution (at least when dealing with something like languages) is to refrain from using it as a 'state' and instead keep some 'data' about it. Each component can then look to this data (either a currentLanguage
value or a list of messages pre-translated in that language) in order to render things correctly.
This 'languages' problem isn't a very good example because it can be structured as 'data' rather than 'state' quite easily. But it served as a way to demonstrate my conundrum. Perhaps a better example is an exam that can be paused. Let's take a look:
Let's imagine the exam has two questions. When in a 'paused' state, the current question gets disabled (i.e. no user interaction can be done). As you can see above, we need to 'duplicate' leaves for each question under Playing
and Paused
so that the correct state can be passed along – something that is undesirable due to the reasons I mentioned before.
Again, we could store a boolean somewhere that describes the exam's state – something that the UI components (Q1 & Q2) can poll. But unlike the 'languages' example, this boolean is very much a “state” and not some piece of “data.” And so unlike languages, this scenario demands that this state be kept in the state tree.
It is this kind of scenario that has me stumped. What solutions or options do I have that could allow me to render my questions while utilizing information about our app's state that is not contained in a Leaf?
Edit: The above examples all use FSMs. In my application I have created some more advances state machines:
- MSM (multi-state machine): Container for multiple state machines that are active simultaneously
- DSM (dynamic state machine): A FSM that gets configured at runtime
- DMSM (dynamic multi-state machine): An MSM that gets configured at runtime
If either of these types of state machines can help to provide a solution to my problem, please feel free to let me know.
Any help is much appreciated!
@JonasW. Here is the structure utilizing an MSM:
Such a structure still wouldn't allow me to get the 'pausable' state information over to the questions.