状态机和UI:基于“节点级”状态而不是“叶子”状态进行渲染 [英] State machines and UI: Rendering based on 'node-level' states instead of 'leaf' states

查看:93
本文介绍了状态机和UI:基于“节点级”状态而不是“叶子”状态进行渲染的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在继续之前,我想指出这个问题的标题很难说。如果应该使用更合适的标题,请告诉我,以便我可以更改它并使这个问题对其他人更有用。



好的,关于问题......我目前正在研究React / Redux项目。我做出的一个设计决定是几乎完全用(分层)状态机来管理应用程序状态和UI,原因有很多(我不会深入研究)。



我利用Redux将我的状态树存储在名为 store.machine 的子状态中。然后Redux子系统的其余部分负责存储应用程序的数据。通过这种方式,我将这两个问题分开,以便它们不会跨越边界。



除此之外,我还将(React)方面的问题分开 - 使用状态组件和UI组件。状态组件几乎完全处理状态流,而UI组件是那些在屏幕上呈现的组件。



我有三种类型的状态组件:




  • 节点:这种状态组件处理状态分支。它根据当前状态(一种委托形式)确定应该呈现哪个组件。

  • Leaf :这种状态组件存在于州树。它的工作仅仅是呈现一个UI组件,传递负责更新状态树的必要调度回调。

  • Container :这种状态组件封装了一个 Node UI 组件,可以并排呈现。



对于我的情况,我们只关心 Node Leaf 组件。我遇到的问题是,当UI组件基于叶子状态呈现时,可能存在更高级别状态可能影响UI应该如何呈现的情况。



采用这种简化的州结构:



AppState Home 状态开始。当用户单击登录按钮时,将调度 to_login 操作。负责管理 AppState 的reducer将收到此操作,并将新的当前状态设置为 Login



同样,在用户输入凭证并完成验证后, success 失败将调度操作。同样,这会被同一个reducer接收,然后他们继续切换到适当的状态: User_Portal Login_Failed



React组件结构如下所示:



我们的顶级 Node 收到 AppState 作为道具,检查当前状态是什么,并呈现/委托给其中一个孩子 Leaf 组件。



Leaf 组件然后呈现传递回调的具体UI组件,以允许它们分派必要的行动(如上所述)来更新状态。虚线表示'state'和'ui'之间的边界,此边界仅在 Leaf 组件处交叉。这使得可以独立地处理 State UI ,因此我想维护它。



这里的事情变得棘手。想象一下,为了争论我们有一个顶级状态来描述应用程序所在的语言 - 假设英语法语。我们更新的组件结构可能如下所示:



现在我们的UI组件必须以正确的语言呈现,即使描述这个的州不是 Leaf 。处理UI呈现的 Leaf 组件没有父状态的概念,因此没有应用程序所在语言的概念。因此,语言状态无法安全地传递给用户界面不破坏模型。要么必须删除状态/ UI边界线,要么需要将父状态传递给子节点,这两者都是可怕的解决方案。



一个解决方案是'复制'每种语言的 AppState 树结构,实质上是为每种语言创建一个全新的树结构......如下所示:



这几乎与我上面描述的两个解决方案一样糟糕,并且需要不断增加的组件来管理事物。



更合适的解决方案(至少在处理语言之类的事情时)是避免将其用作状态,而是保留一些数据。然后,每个组件都可以查看此数据( currentLanguage 值或以该语言预翻译的消息列表),以便正确呈现内容。



这种语言问题不是一个很好的例子,因为它可以很容易地构造为数据而不是状态。但这可以作为展示我的难题的一种方式。也许更好的例子是可以暂停的考试。我们来看看:



让我们假设考试有两个问题。当处于暂停状态时,当前问题被禁用(即,不能进行用户交互)。如您所见,我们需要在播放暂停下为每个问题复制叶子,以便正确状态可以传递 - 由于我之前提到的原因,这是不可取的。



同样,我们可以在某处描述考试状态的布尔值 - 这是UI组件(Q1和Q2)可以轮询。但与语言示例不同,此布尔值非常状态,而不是某些数据。因此,与语言不同,此场景要求将此状态保存在状态树中。



正是这种情况令我难过。我有哪些解决方案或选项可以让我在使用有关我们的应用状态未包含在Leaf 中的信息时提出问题?



< hr>

编辑:以上示例均使用FSM。在我的应用程序中,我创建了一些更高级的状态机:




  • MSM(多状态机):Container对于同时处于活动状态的多个状态机

  • DSM(动态状态机):在运行时配置的FSM

  • DMSM(动态多状态机):在运行时配置的MSM



如果这些类型的状态机可以帮助我解决问题,请随时告诉我。



非常感谢任何帮助!






@JonasW。以下是使用MSM的结构:



这样的结构仍然不允许我得到关于问题的'可暂停'状态信息。

解决方案

让我们尝试为您的架构问题提出解决方案。不确定它是否会令人满意,因为我对我对你的问题的理解并不完全有信心。



让我们从你开始遇到真正问题的角度考虑你的问题,考试组件树。



正如您所述,问题是您需要在每个可能的节点状态中复制您的叶子'。



如果您可以为树中的任何组件提供一些数据,该怎么办?对于我而言,这听起来像是一个可以使用React 16+提供的



通过这种方式,您可以从任何组件访问您的上下文,它可以是动态修改并通过redux。



然后,您的UI组件将保留逻辑,以处理使用给定上下文提供或计算的UI状态。应用程序的其余部分可以保持结构而不会使较低级别或重复节点复杂化,您只需添加一个包装器(Provider)以使Context可用。



使用此功能的人的一些例子:



材料用户界面< - 他们将主题作为上下文传递,并随时随地访问它(主题也可以动态更改)。与您展示的区域设置案例非常相似。 WithStyles 是一个HOC,它将组件链接到状态中的主题。简化:



ThemeProvider有主题数据。在它下面可以有Routes,Switch,Connected组件(如果我理解的话,非常类似于你的节点)。然后你有与withStyles一起使用的组件可以访问主题数据,或者可以使用主题数据来计算某些东西,并将其作为道具注入组件中。***



只是为了完成我可以在几行中草拟一种实现(我没有尝试过,但仅仅是出于解释目的使用Context解释):



QuestionStateProvider

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

AppContainer

  class App扩展React.Component {
构造函数(props){
super(props);
this.state = {
status:PLAYING,
};

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

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

Leaf - 这是只是一个从州获得问题并提出问题或更多问题的容器......



Q1

  function问题(道具){
return(
< ThemeContext.Consumer>
{status =>(
< button
{... props}
disable = {status === PAUSED}
/>
)}
< /ThemeContext.Consumer> ;
);
}

我希望我的问题正确,我的话语很清楚。



如果我错误地理解你或者你想进一步讨论,请纠正我。



*** 这是关于材料ui主题如何工作的非常模糊和一般性的解释


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.

解决方案

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

这篇关于状态机和UI:基于“节点级”状态而不是“叶子”状态进行渲染的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆