Last active
August 14, 2020 04:34
-
-
Save UberMouse/49ff91e95390265d63d3ac1bc778bf72 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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