状态机和 UI:渲染基于 'node-level' 状态而不是 'leaf' 状态

State machines and UI: Rendering based on 'node-level' states instead of 'leaf' states

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

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

我利用 Redux 将状态树存储在名为 store.machine 的子状态中。剩下的 Redux 子状态然后负责存储应用程序 'data.' 这样我就把两个关注点分开了,这样它们就不会越界了。

由此扩展,我还分离了(React)方面的关注点——使用 'state components' 和 'UI components.' 状态组件几乎完全处理状态流,而 UI组件是在屏幕上呈现的那些组件。

我有三种状态组件:

就我的情况而言,我们只关心 NodeLeaf 组件。我遇到的问题是,虽然 UI 组件是基于 'leaf states,' 渲染的,但在某些情况下,'higher-level' 状态可能会影响 UI 的渲染方式.

采用这个简化的状态结构:

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

同样,在用户键入其凭据并完成验证后,将调度 successfail 操作。同样,这会被同一个减速器拾取,然后继续切换到适当的状态:User_PortalLogin_Failed.

React 组件结构如下所示:

我们的 top-level 节点 接收 AppState 作为道具,检查当前状态和 renders/delegates 到 child 叶子个组件。

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

这就是事情变得棘手的地方。 想象一下,为了论证我们有一个 top-level 状态来描述应用程序所使用的语言 – 让我们说 EnglishFrench。我们更新的组件结构可能如下所示:

现在我们的 UI 组件必须以正确的语言呈现,即使描述它的状态不是 Leaf。处理 UI 渲染的 Leaf 组件没有 parent 状态的概念,因此没有应用程序所使用语言的概念。因此,在不破坏模型的情况下,语言状态无法安全地传递给UI。要么 state/UI 边界线必须被移除,要么 parent 状态需要被传递给 children,这两种都是糟糕的解决方案。

一个解决方案是 'copy' 每种语言的 AppState 树结构,本质上是为每种语言创建一个全新的树结构……就像这样:

这几乎和我上面描述的两个解决方案一样糟糕,并且需要越来越多的组件来管理事情。

更合适的解决方案(至少在处理语言之类的东西时)是避免将其用作 'state' 而是保留一些 'data' 。然后每个组件都可以查看此数据(currentLanguage 值或该语言的消息列表 pre-translated)以便正确呈现内容。

这个 'languages' 问题不是一个很好的例子,因为它可以很容易地构造为 'data' 而不是 'state'。但它是证明我的难题的一种方式。也许更好的例子是可以暂停的考试。让我们来看看:

假设考试有两道题。当处于 'paused' 状态时,当前问题将被禁用(即无法进行用户交互)。正如你在上面看到的,我们需要 'duplicate' 为 PlayingPaused 下的每个问题留下,以便可以传递正确的状态——由于我提到的原因,这是不受欢迎的之前。

同样,我们可以在某处存储一个布尔值来描述考试的状态——UI 组件(Q1 和 Q2)可以轮询的东西。但与 'languages' 示例不同的是,这个布尔值在很大程度上是一种“状态”,而不是某种“数据”。因此与语言不同,这种情况要求将这种状态保存在状态树中。

就是这样的场景让我难住了。我有哪些解决方案或选项可以让我在利用 未包含在 Leaf?

中的有关我们应用程序状态的信息的同时提出我的问题

编辑: 上面的例子都使用了FSM。在我的应用程序中,我创建了一些更先进的状态机:

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

非常感谢任何帮助!


@JonasW。这是使用 MSM 的结构:

这样的结构仍然不允许我将 'pausable' 状态信息传递给问题。

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

让我们从您开始遇到实际问题(考试组件树)开始解决您的问题。

正如您所说,问题是您需要在每种可能的情况下复制您的叶子 'Node State'。

如果您可以让树中的任何组件都可以访问某些数据会怎样?对于我来说,这听起来像是一个可以使用 React 16+ 提供的 Context API 的问题。

在你的情况下,我将创建一个 Provider 来包装我的整个应用程序/树的分支,我有兴趣与之共享上下文:

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

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

一些人使用这个的例子:

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 是一个 HOC,它将组件链接到状态中的主题。这样就简化了:

ThemeProvider 有主题数据。在它下面可以有路由、开关、连接组件(如果我理解正确的话,与你的节点非常相似)。然后,与 withStyles 一起使用的组件可以访问主题数据,或者可以使用主题数据来计算某些内容,并将其作为道具注入组件中。***

为了完成,我可以在几行中起草一种实现(我没有尝试过,但它只是为了使用上下文解释进行解释):

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 - 它只是一个从状态获取问题并呈现问题或更多问题的容器...

Q1

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

我希望我回答你的问题并且我的话足够清楚。

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

*** 这是对 material ui 主题如何工作的极其模糊和笼统的解释