Skip to content

Instantly share code, notes, and snippets.

@UberMouse
Last active August 14, 2020 04:34
Show Gist options
  • Save UberMouse/49ff91e95390265d63d3ac1bc778bf72 to your computer and use it in GitHub Desktop.
Save UberMouse/49ff91e95390265d63d3ac1bc778bf72 to your computer and use it in GitHub Desktop.
import { useMachine } from "@xstate/react/lib";
import { assign, createMachine } from "xstate";
import { uniqueId } from "lodash";
import React from "react";
import { makeMachineRoutable } from "./makeMachineRoutable";
type PromiseContext<TData, TParameters> = {
parameters?: TParameters;
data?: TData;
message?: string;
actions?: {
[transitionedTo in PromiseState<TData, TParameters>["value"]]?: (
ctx: Extract<PromiseState<TData, TParameters>, { value: transitionedTo }>["context"]
) => void;
};
};
type PromiseState<TData, TParameters> =
| {
value: "loading";
context: PromiseContext<TData, TParameters>;
}
| { value: "loaded"; context: PromiseContext<TData, TParameters> & { data: TData } }
// TODO: Fill in this state in the machine
| { value: "error"; context: PromiseContext<TData, TParameters> & { message: string } };
export function createPromiseMachine<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TPromise extends (...args: any[]) => Promise<TData>,
TData = ReturnType<TPromise> extends Promise<infer TPromiseReturn> ? TPromiseReturn : never,
TParameters = Parameters<TPromise>,
TProviderProps = TParameters extends []
? { parameters?: TParameters; actions?: PromiseContext<TData, TParameters>["actions"] }
: { parameters: TParameters; actions?: PromiseContext<TData, TParameters>["actions"] }
>(promise: TPromise) {
const promiseMachine = createMachine<
PromiseContext<TData, Parameters<TPromise>>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
PromiseState<TData, Parameters<TPromise>>
>(
{
id: uniqueId("promiseMachine"),
initial: "loading",
states: {
loading: {
entry: "loadingEntered",
invoke: {
src: ctx => {
if (ctx.parameters) {
return promise(...ctx.parameters);
}
return promise();
},
onDone: {
target: "loaded",
actions: assign({ data: (_ctx, event) => event.data })
},
onError: "error"
}
},
error: {
entry: "errorEntered",
type: "final"
},
loaded: {
entry: "loadedEntered",
type: "final",
data: ctx => ctx.data
}
}
},
{
// I'm unaware of any way to enforce with the type system these actions will only be called in the correct state
// The types of the action at the callsite are correct however so just @ts-ignore it
actions: {
loadingEntered: ctx => ctx.actions?.loading?.(ctx),
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
loadedEntered: ctx => ctx.actions?.loaded?.(ctx),
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
errorEntered: ctx => ctx.actions?.error?.(ctx)
}
}
);
const { Match, provider: Provider } = makeMachineRoutable(promiseMachine);
const PromiseProvider: React.FC<TProviderProps> = props => {
const [current, send] = useMachine(promiseMachine, {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
context: { parameters: props.parameters, actions: props.actions }
});
return <Provider value={[send, current]}>{props.children}</Provider>;
};
return {
Match,
Provider: PromiseProvider
};
}
import { StateMachine, StateSchema, EventObject, Typestate, Interpreter, State } from "xstate";
import * as _ from "lodash";
import React, { useContext } from "react";
export function makeMachineRoutable<
TContext,
TStateSchema extends StateSchema,
TEvent extends EventObject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TTypestate extends Typestate<TContext> = any
>(_machine: StateMachine<TContext, TStateSchema, TEvent, TTypestate>) {
type Send = Interpreter<TContext, TStateSchema, TEvent, TTypestate>["send"];
type Value = [Send, State<TContext, TEvent, TTypestate>];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const MachineContext = React.createContext<Value>(null as any);
const useMachine = () => useContext(MachineContext);
type Props<T extends TTypestate["value"]> = {
state: T | T[];
children: (args: {
context: Extract<TTypestate, { value: T | T[] }>["context"];
send: Send;
}) => JSX.Element;
};
function Match<T extends TTypestate["value"] = TTypestate["value"]>({
state,
children
}: Props<T>) {
const [send, current] = useMachine();
const matched = _.isArray(state)
? _.some(state, s => current.matches(s))
: current.matches(state);
if (matched) {
// Not sure what's going on here. Doesn't really matter, I don't think this can generate useful type safety
// What matters is that the child functions get passed the correct context based on the matched state
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return children({ context: current.context as any, send });
}
return null;
}
return {
provider: MachineContext.Provider,
useMachine,
Match
};
}
// function getRepositoryHistory(path: string): Promise<QueryResultContainer<Commit[]>>
const { Match, Provider } = createPromiseMachine(getRepositoryHistory);
type Props = {
path: string;
};
export function CommitList({ path }: Props) {
const [send] = useRepositoryMachine();
return (
<Provider
parameters={[path]}
actions={{
loaded: ctx => {
const [commit] = ctx.data.data;
if (commit) {
send({ type: "COMMIT_SELECTED", commit: ctx.data.data[0] });
}
}
}}
>
<Match state="loaded">
{({ context }) => (
<>
{context.data.data.map(commit => (
<Commit
key={commit.sha}
{...commit}
onClick={() => send({ type: "COMMIT_SELECTED", commit })}
/>
))}
</>
)}
</Match>
</Provider>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment