XStateと反応して取得漏斗を構築する


取得ファンネルは、ほとんどの開発チームが何らかの点で行うよう求められるものです.これは、このファンネルは、しばしば会社の製品やサービスに主なエントリポイントとなる小さな仕事です.これは、そのような漏斗は、バグを顧客を失うのを避けるために完全に信頼性があり、マーケティングチームが転換率を最適化するのを完全に柔軟にする必要があることを意味します.
この2つの部分の記事では、強力なプログラミングパターンを使用して簡単に実現する方法を説明します.私たちは実装のために反応とXstateを使用するでしょう、しかし、原則は本当にどんなスタックにでも適用されることができます.
パート1では、どのようにしてfunnelに権限を与えるステートマシンを実装し、実際の手順を構築し、ユーザデータを処理するかを参照します.
あなたはコードを見つけることができますhere . 私はあなたがプロジェクトをクローンし、それを再生することをお勧め!

ユースケース
いつものようにプログラミングになると、何かを学ぶための最良の方法は手を汚すことです.
我々がチームビルディング会社のために働くと言いましょう、そして、マーケティングチームは我々の顧客が活動を選んで、人々の束を登録することができる獲得漏斗を造ることを望みます.
以下のfunnelを実装する必要があります.
  • ステップ1:ピックのリストの活動の種類を選択する
  • ステップ2:人を登録するフォーム.顧客は、このステップを任意の数の時間を通過する必要があります.このステップからはステップ4に直進できます.
  • ステップ3(オプション):追加情報を与えるためのフォーム;
  • ステップ4 :支払い:💰
  • 第1部では、実際には何も提出せずにトンネルの異なるステップを実装しようとします.我々は、我々がそのパート2を取り扱う方法を見ます:D

    有限状態機械の語
    注:私はそれを読むことをお勧めしますが、このセクションは厳密に我々のソリューションを実装することができる必要はありません.だからあなたがアクションにジャンプしたい場合はImplementation
    我々がそれに下る前に、私たちのモデル選択を正当化したいです:有限状態機械.別の記事でもっと詳しく説明しますが、今のところこのパターンを簡単に説明しましょう.

    有限状態機械とは何か?
    ショートカットを取りましょうWikipedia article :

    A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation. It is an abstract machine that can be in exactly one of a finite number of states at any given time.The FSM can change from one state to another in response to some inputs; the change from one state to another is called a transition.


    基本的には、モデリングプロセスの堅牢で、読みやすく、決定的な方法です.たとえば、我々が状態機械を使用している交通信号をcoudモデル化する方法:

    簡単!

    ステートマシンを使う理由
    上記の定義の重要なビットは、マシンが一度に1つの状態にあることが保証されるということです.この保証は私たちのユースケースを解決するための途方もない前進です.
    このモデルを漏斗に適用するのも簡単です.各ステップを単に状態にマッピングすることによって、私たちが望むことを正確に行うのです.
    さらに、我々のfunnelの状態は、ユーザーによって引き起こされるイベントのシーケンスにより決定されて、完全に決定論的であるでしょう.
    これは、正しい状態図を描画し、ステートマシンを実行できるコンポーネントに供給すると、funnelが望むように振る舞います.

    状態機械の異なるタイプ
    有限状態マシンの2種類があります.
  • The Moore machine : マシンの現在の出力が直接状態に一致するところ.
  • The Mealy machine : 出力が現在の状態と最後の入力の両方に依存する場合.
  • ユースケースに応じて使用するタイプを選択します.ムーアマシンがしばしばよりまっすぐに、そして、読みやすい間、Maly機械は異なる出力の同じ数の状態の数を減らすことができます.
    有限状態マシン以外に、他の種類のマシンがあります.
  • チューリングマシン:そのためには別の記事が必要です.)

  • StateChart : これはプログラミング言語機能(この場合JavaScript)を利用します:「通常の」有限状態機械の能力を越えて.パート2ではユーザ入力を処理する必要があります.
  • 今のところ、我々の唯一の懸念は、漏斗の手順のシーケンスは、ムーアマシンが十分です!

    漏斗のモデリング
    私たちの要件に戻って、我々の取得ファンネルのモデリングは完全に簡単です:

    このプレイヤーはxstate vizualizer 本稿で見つかる機械実装から

    漏斗の建設
    我々のモデルの信頼性に自信がある今、それを実行しましょう!

    プロジェクトの設定
  • 糸をインストール
  • アプリケーションを作成しますyarn create react-app --template typescript
  • 依存関係をインストールするyarn add xstate
  • funnelコンポーネントとコンポーネントを格納するディレクトリを作成しますmkdir src/acquisition-funnel

  • 機械を書く
    状態チャートをコードに翻訳し始めましょう.おかしいこと:しかし、上記のダイアグラムはXState vizualizerによって実際にコードから生成されました.自動ドキュメント!ああ!
    最初に作ろうsrc/acquisition-funnel/types.ts イベントタイプはどこでしょう.
    
    # types.ts
    export const SELECT_ACTIVITY = "SELECT_ACTIVITY";
    export const SUBMIT_ATTENDEE = "SUBMIT_ATTENDEE";
    export const ADD_ATTENDEE = "ADD_ATTENDEE";
    export const ADD_INFO = "ADD_INFO";
    export const SUBMIT_ADDITIONNAL_INFORMATION = "SUBMIT_ADDITIONNAL_INFORMATION";
    
    定数を変数に格納するのは常に良い考えです.
    さあ、マシンそのものに行きましょう.ファイルを作成するstate-machine.ts インsrc/acquisition-funnel , そして以下のコードを追加します.
    
    # state-machine.ts
    
    import { Machine } from "xstate";
    import {
      SELECT_ACTIVITY,
      SUBMIT_ATTENDEE,
      ADD_ATTENDEE,
      ADD_INFO,
      SUBMIT_ADDITIONNAL_INFORMATION,
    } from "./types";
    
    export const stateMachine = Machine({
      id: "funnel-state-machine",
      initial: "activity",
      states: {
        activity: {
          on: {
            [SELECT_ACTIVITY]: "register_attendee",
          },
        },
        register_attendee: {
          on: {
            [ADD_ATTENDEE]: "register_attendee",
            [ADD_INFO]: "additional_information",
            [SUBMIT_ATTENDEE]: "payment",
          },
        },
        additional_information: {
          on: {
            [SUBMIT_ADDITIONNAL_INFORMATION]: "payment",
          },
        },
        payment: {
          type: "final",
        },
      },
    });
    
    export default stateMachine;
    
    
    
    これらのダイアグラムの各状態が表示され、各状態の使用可能な遷移がon 属性.

    ビルディングFunnelProvider現在の状態機械が準備されているので、各状態に関連付けられたステップコンポーネントをレンダリングするコンポーネントをデザインする必要があります.それをするために、私たちはCompound component パターン.
    The FunnelProvider コンテキスト内の現在の状態を保持します.State コンポーネント.
    現在の状態が一致した場合、各状態コンポーネントはレンダリングされます.
    まずファイルを追加するFunnelProvider.ts to src/acquisition-funnel , 次のコードを追加します.
    
    import React, { useContext } from "react";
    import { StateMachine, State } from "xstate";
    import { useMachine } from "@xstate/react";
    
    // We use a generic type to be able to handle
    // any shape of context with type checking
    interface FunnelProviderProps<TContext> {
      stateMachine: StateMachine<TContext, any, any>;
      children: React.ReactNode;
    }
    
    interface FunnelContextValue {
      currentState: State<any>;
      send: (state: string) => void;
    }
    
    const FunnelContext = React.createContext({} as FunnelContextValue);
    
    function FunnelProvider<TContext>({
      stateMachine,
      children,
    }: FunnelProviderProps<TContext>): React.ReactElement {
      const [current, send] = useMachine(stateMachine);
      return (
        <FunnelContext.Provider value={{ currentState: current, send }}>
          {children}
        </FunnelContext.Provider>
      );
    }
    
    // This is a common patter to avoid import 
    // the constext in every consumer
    export const useFunnel = () => useContext(FunnelContext);
    
    export default FunnelProvider;
    
    
    その後、StateRenderer.tsx ファイルsrc/acquisition-funnel idに次のコードを追加します.
    import React from "react";
    import { useFunnel } from "./FunnelProvider";
    
    interface StateProps {
      state: string;
      children: (send: any) => React.ReactNode;
    }
    
    const StateRenderer: React.FunctionComponent<StateProps> = ({
      state,
      children,
    }) => {
      const { currentState, send } = useFunnel();
    
      return currentState.matches(state) ? (
        <div>{children(send)}</div>
      ) : (
        <div></div>
      );
    };
    
    export default StateRenderer;
    
    
    ここではRender props SponentプロパティをStateプロパティに渡すことができるパターン.次のステップでなぜ役に立つのか

    すべてをまとめる
    現在、我々は我々の状態機械と我々の複合成分を持っていますFunnelProvider and StateRenderer , すべてを残して何をレンダリングする選択です.
    次のコードを追加しますApp.tsx :
    
    # App.tsx
    
    import React from "react";
    import FunnelProvider from "./acquisition-funnel/FunnelProvider";
    import StateRenderer from "./acquisition-funnel/StateRenderer";
    import RegistrationStep from "./acquisition-funnel/RegistrationStep";
    import { stateMachine } from "./acquisition-funnel/state-machine";
    import {
      SELECT_ACTIVITY,
      SUBMIT_ATTENDEE,
      ADD_ATTENDEE,
      ADD_INFO,
      SUBMIT_ADDITIONNAL_INFORMATION,
    } from "./acquisition-funnel/types";
    import "./App.css";
    
    function App() {
      return (
        <div className="App">
          <FunnelProvider stateMachine={stateMachine}>
            <StateRenderer state="activity">
              {(send) => {
                return (
                  <div>
                    <h2>Activity Step</h2>
                    <button onClick={() => send(SELECT_ACTIVITY)}>next</button>
                  </div>
                );
              }}
            </StateRenderer>
            <StateRenderer state="register_attendee">
              {(send) => (
                <RegistrationStep
                  add_participant={() => send(ADD_ATTENDEE)}
                  additional_information={() => send(ADD_INFO)}
                  proceed_to_payment={() => send(SUBMIT_ATTENDEE)}
                />
              )}
            </StateRenderer>
            <StateRenderer state="additional_information">
              {(send) => {
                return (
                  <div>
                    <h2>Additional information</h2>
                    <button onClick={() => send(SUBMIT_ADDITIONNAL_INFORMATION)}>
                      next
                    </button>
                  </div>
                );
              }}
            </StateRenderer>
            <StateRenderer state="payment">
              {() => {
                return <h2>payment</h2>;
              }}
            </StateRenderer>
          </FunnelProvider>
        </div>
      );
    }
    
    export default App;
    
    
    
    我々がこのステップをしたすべての仕事のおかげで、本当に簡単です:我々は、単に活発なステップを提出するために我々のプロバイダーの中でstaterendererを使います.Stateerdererは、必要なコールバックを渡し、我々のRenderプロップを使用してfunnelに移動します.
    登録手順でループを見ることができたので、登録ステップはより複雑です.そのため、独自のコンポーネントで定義されています.
    
    import React, { useState } from "react";
    
    interface RegistrationProps {
      add_participant: () => void;
      additional_information: () => void;
      proceed_to_payment: () => void;
    }
    const RegistrationStep: React.FunctionComponent<RegistrationProps> = ({
      add_participant,
      additional_information,
      proceed_to_payment,
    }) => {
      const [counter, setCounter] = useState(1);
    
      return (
        <div>
          <h2>Register participant number {counter}</h2>
          <button
            onClick={() => {
              setCounter((counter) => counter + 1);
              add_participant();
            }}
          >
            Continue registering
          </button>
          <button onClick={additional_information}>
            Add additional information
          </button>
          <button onClick={proceed_to_payment}>Proceed to Payment</button>
        </div>
      );
    };
    
    export default RegistrationStep;
    
    
    
    我々は、単に私たちが呼び出すたびにカウンタをインクリメントadd_participantそして、我々はしました!我々が構築した漏斗は完全に決定論的であり、要件を満たしている.我々が流れを変える必要があるならば、我々がしなければならないすべては更新ですstate-machine.tx . それじゃない?

    Try it out !

    結論
    今日はこれだ!あなたはパート1を楽しんだ希望は、我々はすでに多くを達成した!
    もちろん、まだ何も提出できません.