Skip to content

Instantly share code, notes, and snippets.

@kirill578
Created October 17, 2023 16:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kirill578/fb4ff09e412519935f47e201e027fc9d to your computer and use it in GitHub Desktop.
Save kirill578/fb4ff09e412519935f47e201e027fc9d to your computer and use it in GitHub Desktop.
xstate warn issue
import { useActor, useMachine } from "@xstate/react";
import React from "react";
import { createMachine, ActorRefFrom, sendParent } from "xstate";
export const id = "survey";
export const eventStoreId = "survey";
export type SurveyActorRef = ActorRefFrom<ReturnType<typeof createSurvey>>;
const staticConfig = {
id: "survey",
predictableActionArguments: true,
initial: "questionOne",
context: { i: 0 },
schema: {
context: {} as { i: number },
events: {} as
| { type: "NEXT" }
| { type: "CLICK_NO" }
| { type: "CLICK_YES" }
| { type: "SUB_FLOW_DONE" },
},
states: {
done: {},
questionOne: {
on: {
CLICK_YES: {
actions: ["sendAnswerYes"],
target: "generalQuestions",
},
CLICK_NO: {
actions: ["sendAnswerNo"],
target: "generalQuestions",
},
},
},
generalQuestions: {
invoke: {
id: "generalQuestions",
src: "generalQuestions",
},
on: {
SUB_FLOW_DONE: {
target: "done",
},
},
},
},
};
export const createSurvey = (args: { proccessData: (data: string) => void }) =>
createMachine(staticConfig, {
actions: {
sendAnswerYes: () => args.proccessData("answerYes"),
sendAnswerNo: () => args.proccessData("answerNo"),
},
services: {
generalQuestions: () => createGeneralQuestionMachine(args),
},
});
export type GeneralQuestionsActorRef = ActorRefFrom<
ReturnType<typeof createGeneralQuestionMachine>
>;
const staticGeneralConfig = {
id: "generalQuestions",
predictableActionArguments: true,
initial: "questionAorB",
schema: {
events: {} as
| { type: "NEXT" }
| { type: "CLICK_A" }
| { type: "CLICK_B" }
| { type: "SUB_FLOW_DONE" },
},
states: {
done: {
entry: ["subflowDone"],
},
questionAorB: {
on: {
CLICK_A: {
actions: ["sendAnswerA"],
target: "done",
},
CLICK_B: {
target: "done",
actions: ["sendAnswerB"],
},
},
},
},
};
export const createGeneralQuestionMachine = (args: {
proccessData: (data: string) => void;
}) =>
createMachine(staticGeneralConfig, {
actions: {
sendAnswerA: () => args.proccessData("answerA"),
sendAnswerB: () => args.proccessData("answerB"),
subflowDone: sendParent("SUB_FLOW_DONE"),
},
});
export function TopLevelGeneralController({
actorRef,
}: {
actorRef: GeneralQuestionsActorRef;
}) {
const [state, send] = useActor(actorRef);
if (state.matches("questionAorB")) {
return (
<div>
<>A or B?</>
<div role="button" tabIndex={0} onClick={() => send("CLICK_A")}>
A
</div>
<div role="button" tabIndex={0} onClick={() => send("CLICK_B")}>
B
</div>
</div>
);
}
if (state.matches("done")) {
return <>done</>;
}
throw new Error("unreachable");
}
export function TopLevelSurveyController({
actorRef,
}: {
actorRef: SurveyActorRef;
}) {
const [state, send] = useActor(actorRef);
if (state.matches("questionOne")) {
return (
<div>
<>yes or now?</>
<div role="button" tabIndex={0} onClick={() => send("CLICK_YES")}>
yes
</div>
<div role="button" tabIndex={0} onClick={() => send("CLICK_NO")}>
no
</div>
</div>
);
}
if (state.matches("generalQuestions")) {
return (
<TopLevelGeneralController
actorRef={state.children.generalQuestions as GeneralQuestionsActorRef}
/>
);
}
if (state.matches("done")) {
return <>done</>;
}
throw new Error("unreachable");
}
const topMachine = createMachine({
predictableActionArguments: true,
invoke: {
id: "inner",
src: "inner",
},
});
const useProccessData = () => {
const [i, setI] = React.useState(0);
React.useEffect(() => {
// simulates a hook that changes its reference all the time (could be a third party librar that we don't have control over)
const tid = setTimeout(() => setI((a) => a + 1), 1000);
return () => clearTimeout(tid);
}, [i]);
return React.useCallback(
(data: string): number => {
console.log("posting data", data);
return i;
},
[i],
);
};
const App = () => {
const proccessData = useProccessData();
const args = React.useMemo(
() => ({
proccessData,
}),
[proccessData],
);
// this is a simplified usecase, in reality it is used to capture events from children that are triggered with sendParent
const [state] = useMachine(
React.useMemo(
() =>
createMachine({
predictableActionArguments: true,
invoke: {
id: "inner",
src: "inner",
},
}),
[],
),
React.useMemo(
() => ({
services: {
inner: createSurvey(args).withContext({
i: proccessData("abc"),
}),
},
}),
[proccessData],
),
);
if (!state.children.inner) {
return <>loading</>;
}
return (
<>
<TopLevelSurveyController
actorRef={state.children.inner as SurveyActorRef}
/>
</>
);
};
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment