Skip to content

App loading flow example

Machine

ts
import { machine, st, XMsg, XModel, XCmd, defineFlow } from "tesm"

type InitialContext = {
    loadingStarted: number;
}
type WaitingInitialize = InitialContext & {
    localSave: LocalSave;
}
type LoadingConfigContext = WaitingInitialize & {
    initialize_data: Initialize
}

type LoadedContext = LoadingConfigContext & {
    config: Config;
    loadingFinished: number;
}

const m = machine(
    {
        initial: st<InitialContext>(),
        waiting_initialize: st<WaitingInitialize>(),
        waiting_config: st<LoadingConfigContext>(),
        loaded: st<LoadedContext>(),
    },
    {
        initialized: (data: Initialize) => ({ data }),
        config_loaded: (config: Config, now: number) => ({ config, now }),
        local_storage_loaded: (localSave: LocalSave) => ({ localSave }),
    },
    {
        loadLocalStorage: () => ({}),
        initialize: () => ({}),
        loadConfig: (configUrl: string) => ({ configUrl }),
    }
)

const AppLoadingState = defineFlow(
    m,
    "AppLoadingState",
    () => [m.states.initial({ loadingStarted: Date.now() }), m.cmds.loadLocalStorage()],
    {
        initial: {
            local_storage_loaded: (msg, model) => {
                return [
                    m.states.waiting_initialize({
                        ...model,
                        localSave: msg.localSave
                    }),
                    m.cmds.initialize()
                ];
            }
        },
        waiting_initialize: {
            initialized: (msg, model) => {
                return [
                    m.states.waiting_config({
                        ...model,
                        initialize_data: msg.data
                    }),
                    m.cmds.loadConfig(msg.data.configUrl)
                ];
            }
        },
        waiting_config: {
            config_loaded: (msg, model) => {
                return [
                    m.states.loaded({
                        ...model,
                        config: msg.config,
                        loadingFinished: msg.now
                    }),
                ];
            }
        },
        loaded: {}
    }
)

export namespace AppLoading {
    export type Msgs = ReturnType<typeof AppLoadingState.msgCreator>;
    export type Cmd = XCmd<typeof AppLoadingState>
    export type Model = XModel<typeof AppLoadingState>

    export const machine = AppLoadingState
}



type Initialize = {
    configUrl: string;
    username: string;
    userId: string;
    // ... other properties
}

type LocalSave = {
    // ... properties for local save
}

type Config = {
    // ... properties for config
}

React

context.tsx

tsx
import { createContext, PropsWithChildren, useContext } from "react"
import { AppLoading } from "../state"
import { useTeaSimple } from "tesm/react"

const GlobalStateContext = createContext<
	{ model: AppLoading.Model; msgs: AppLoading.Msgs } | undefined
>(undefined)

export const GlobalStateProvider: React.FC<PropsWithChildren> = (props) => {
	const [state, msgs] = useTeaSimple(AppLoading.machine, {
		initialize: async (cmd, msgs) => {
			await sleep(1000)
			return msgs.initialized({
				configUrl: "https://example.com/config.json",
				userId: "user123",
				username: "user",
			})
		},
		loadConfig: async (cmd, msgs) => {
			await sleep(1000)
			return msgs.config_loaded({}, Date.now())
		},
		loadLocalStorage: async (cmd, msgs) => {
			await sleep(1000)
			return msgs.local_storage_loaded({})
		},
	})
	return (
		<GlobalStateContext.Provider value={{ model: state, msgs }}>
			{props.children}
		</GlobalStateContext.Provider>
	)
}

export function useGlobalState() {
	let ctx = useContext(GlobalStateContext)
	if (!ctx) throw new Error("useGlobalState must be used within a GlobalStateProvider")

	return ctx
}

AppLoader.tsx

tsx
import { SpecificState } from "tesm"
import { AppLoading } from "../state"
import { useGlobalState } from "./context"

type LoadedModel = SpecificState<AppLoading.Model, "loaded">

const App = (props: LoadedModel) => {
	return (
		<div>
			<h1>Welcome {props.initialize_data.username}</h1>
		</div>
	)
}

const LoadingScreen = (props: { stage: AppLoading.Model["state"] }) => {
	return <div>loading stage: {props.stage}</div>
}

export const AppLoader = () => {
	const { model } = useGlobalState()

	switch (model.state) {
		case "initial":
		case "waiting_initialize":
		case "waiting_config":
			return <LoadingScreen stage={model.state} />
		case "loaded":
			return <App {...model} />
	}
}