State machines and UI: Rendering based on 'nod

2019-04-19 16:49发布

问题:

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 failaction 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.

回答1:

Let's try to propose a solution for your architectural problem. Not sure if it will be satisfactory since I am not fully confident in my understanding of your problem.

Let's take your problem from the point you start having real problems, the Exam component tree.

As you stated, the problem is that you need to replicate your leafs in each possible 'Node State'.

What if you could make some data accesible for any of the components in the tree? For my this sounds like a problem that could use the Context API that React 16+ provides.

In your case I will create a Provider that wraps my whole application / Branch of the tree that I am interested in sharing a context with:

In this way you could access your context from any of the components and it can be modified dynamically and through redux.

Then is just left for your UI Components to keep the logic to deal with the UI State provided or computed with the given context. The rest of the application can keep it structure without complicating the lower levels or duplicating nodes, you just need to add a wrapper (Provider) in order to make the Context available.

Some examples of people using this:

Material UI <- They pass the theme as a context and access it whenever and wherever (The theme can also be change dynamically). Very similar to the locale case that you showed. WithStyles is a HOC that links a component to the theme in the state. So that simplified:

ThemeProvider has theme data. Under it there can be Routes, Switch, Connected components (Very similar to your nodes if I understood right). And then you have components that used with withStyles have access to the theme data or can use the theme data to compute something and it is injected in the component as a prop.***

And just to finish I can draft kind of an implementation in few lines (I didn't try it out but it is just for explanation purposes using the Context explanation):

QuestionStateProvider

export const QuestionState = React.createContext({
  status: PLAYING,
  pause: () => {},
});

AppContainer

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      status : PLAYING,
    };

    this.pause = () => {
      this.setState(state => ({
        status: PAUSE,
      }));
    };
  }

  render() {
    return (
      <Page>
        <QuestionState.Provider value={this.state}>
          <Routes ... />
          <MaybeALeaf />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

Leaf - It is just a container that gets questions from the state and render a question or more...

Q1

function Question(props) {
  return (
    <ThemeContext.Consumer>
      {status => (
        <button
          {...props}
          disable={status === PAUSED}
        />
      )}
    </ThemeContext.Consumer>
  );
}

I hope I got your question right and that my words are clear enough.

Correct me if I understood you wrongly or if you want to discuss further.

*** This is a extremely vague and general explanation of how material ui theming works