状态机和UI:基于"节点级"状态而不是"叶子"状态进行渲染

soo*_*kie 12 javascript state-machine reactjs redux react-redux

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

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

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

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

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

  • 节点:这种状态组件处理状态分支.它根据当前状态(一种委托形式)确定应该呈现哪个组件.
  • Leaf:这种状态组件存在于状态树的叶子中.它的工作仅仅是渲染一个UI组件,传递负责更新状态树的必要"调度"回调.
  • 容器:这种状态组件封装了要并行呈现的节点UI组件.

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

采用这种简化的状态结构: 简化的州结构

AppStateHome州开始.当用户单击登录按钮时,将to_login调度操作.负责管理的减速机AppState将接收此动作并将新的当前状态设置为Login.

同样,在用户键入其凭据并完成验证后,将调度a successfailaction.同样,这会被同一个减速器接收,然后转换到适当的状态:User_PortalLogin_Failed.

React组件结构如下所示: 反应组件结构

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

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

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

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

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

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

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

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

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

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

正是这种情况令我难过.我有哪些解决方案或选项可以让我在利用有关我们的应用程序状态的信息时不会出现问题


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

  • MSM(多状态机):用于同时处于活动状态的多个状态机的容器
  • DSM(动态状态机):在运行时配置的FSM
  • DMSM(动态多状态机):在运行时配置的MSM

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

任何帮助深表感谢!


@JonasW.这是利用MSM的结构: 利用MSM的结构

这样的结构仍然不允许我将"可暂停的"状态信息提供给问题.

Sir*_*ple 1

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

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

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

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

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

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

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

人们使用这个的一些例子:

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

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

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

问题状态提供者

export const QuestionState = React.createContext({
  status: PLAYING,
  pause: () => {},
});
Run Code Online (Sandbox Code Playgroud)

应用容器

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>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

Leaf - 它只是一个从状态获取问题并呈现问题或更多问题的容器...

Q1

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

我希望我的问题是正确的并且我的话足够清楚。

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

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