javascript - 状态机和 UI : Rendering based on 'node-level' states instead of 'leaf' states

标签 javascript reactjs redux react-redux state-machine

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

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

我利用 Redux 将我的状态树存储在名为 store.machine 的子状态中。 .其余的 Redux 子状态然后负责存储应用程序“数据”。通过这种方式,我将两个关注点分开,这样它们就不会跨越边界。

在此基础上,我还分离了(React)方面的关注点——使用“状态组件”和“UI 组件”。状态组件几乎完全处理状态流,而 UI 组件是那些在屏幕上呈现的组件。

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

  • 节点:这种状态组件处理状态分支。它根据其当前状态(一种委托(delegate)形式)确定应呈现哪个组件。
  • Leaf:这种状态组件存在于状态树的叶子上。它的工作只是渲染一个 UI 组件,传递负责更新状态树的必要"dispatch"回调。
  • Container:这种状态组件封装了一个 Node 和 UI 组件,可以并排渲染。

  • 对于我的情况,我们只关心 Node 和 Leaf 组件。我遇到的问题是,虽然 UI 组件是根据“叶子状态”呈现的,但在某些情况下,“更高级别”的状态可能会影响 UI 的呈现方式。

    采取这个简化的状态结构:
    simplified state structure
    AppStateHome 开始状态。当用户单击登录按钮时,会出现 to_login行动被分派(dispatch)。负责管理的 reducer AppState将收到此操作并将新的当前状态设置为 Login .

    同样,在用户输入他们的凭据并完成验证后,或者 successfail行动将被 dispatch 。同样,这被同一个 reducer 接收,然后它继续切换到适当的状态:User_PortalLogin_Failed .

    React 组件结构看起来像这样:
    React component structure

    我们的顶级节点收到 AppState作为 Prop ,检查当前状态并呈现/委托(delegate)给子 Leaf 组件之一。

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

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

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

    一种解决方案是“复制”AppState每种语言的树结构,本质上为每种语言创建一个全新的树结构……像这样:
    whole new tree structure per language

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

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

    这个“语言”问题不是一个很好的例子,因为它可以很容易地被构造为“数据”而不是“状态”。但它可以作为证明我的难题的一种方式。也许一个更好的例子是可以暂停的考试。让我们来看看:
    exam that can be paused

    假设考试有两个问题。当处于“暂停”状态时,当前问题被禁用(即无法进行用户交互)。正如您在上面看到的,我们需要为 Playing 下的每个问题“复制”叶子。和 Paused以便可以传递正确的状态——由于我之前提到的原因,这是不受欢迎的。

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

    正是这样的场景让我难受。我有哪些解决方案或选项可以让我在利用有关我们的应用程序状态的信息 来呈现我的问题不包含在叶子中 ?

    编辑:上面的例子都使用 FSM。在我的应用程序中,我创建了一些更先进的状态机:
  • MSM(多状态机):同时处于事件状态的多个状态机的容器
  • DSM(动态状态机):在运行时配置的 FSM
  • DMSM(动态多状态机):在运行时配置的 MSM

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

    任何帮助深表感谢!

    @乔纳斯。这是使用 MSM 的结构:
    the structure utilizing an MSM

    这样的结构仍然不允许我将“可暂停”状态信息传递给问题。

    最佳答案

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

    让我们从您开始遇到实际问题的 Angular 来解决您的问题,即考试组件树。
    Exam component tree

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

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

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

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

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

    人们使用它的一些例子:

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

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

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

    QuestionStateProvider

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

    应用容器
    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>
        );
      }
    }
    

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

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

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

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

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

    关于javascript - 状态机和 UI : Rendering based on 'node-level' states instead of 'leaf' states,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51212681/

    相关文章:

    javascript - 为什么 JavaScript 中的不变性如此重要(或需要)?

    javascript - 如何在不渲染组件的情况下传递 Prop ?

    reactjs - Saga 观察者未接收或处理调度的操作

    reactjs - 如何在 reduxjs/toolkit React 和 Typescript 完成另一个 Action 时分派(dispatch)一个 Action ?

    reactjs - 基本的 redux 概念 - 如何启动 reducer

    Javascript 元素事件委托(delegate)

    javascript - 为什么 safari 日期字段显示为无效日期?

    javascript - 将 JS 注入(inject) Literal 并在页面加载后触发

    javascript - 为什么再次悬停时下拉菜单动画会削高?

    javascript - 防止 Firebase 云功能中的竞争条件