Skip to content

Instantly share code, notes, and snippets.

@ataylorme
Created February 19, 2025 16:12
Show Gist options
  • Save ataylorme/7f4a962351c9ad9f3526b64b4b7c3407 to your computer and use it in GitHub Desktop.
Save ataylorme/7f4a962351c9ad9f3526b64b4b7c3407 to your computer and use it in GitHub Desktop.
XState Chuck Norris Joke Machine
import { assign, fromPromise, setup } from "xstate";
async function getChuckNorrisJoke(category?: string): Promise<Joke> {
let url = "https://api.chucknorris.io/jokes/random";
if (category !== undefined && category !== "") {
url += `?category=${category}`;
}
const response = await fetch(url);
const data = await response.json();
return {
id: data.id,
value: data.value,
} as Joke;
}
async function getChuckNorrisJokeCategories(): Promise<string[]> {
const url = "https://api.chucknorris.io/jokes/categories";
const response = await fetch(url);
const data = await response.json();
return data;
}
interface Joke {
id: string;
value: string;
}
interface Context {
jokes: Set<Joke>;
errors: string[];
numJokes: number;
category?: string;
categories: Set<string>;
}
const Context: Context = {
jokes: new Set<Joke>(),
errors: [],
numJokes: 5,
categories: new Set<string>(),
};
interface GET_JOKES {
readonly type: "GET_JOKES";
readonly numJokes: Context["numJokes"];
}
interface CLEAR_JOKES {
readonly type: "CLEAR_JOKES";
}
interface CLEAR_ERRORS {
readonly type: "CLEAR_ERRORS";
}
interface GET_CATEGORIES {
readonly type: "GET_CATEGORIES";
}
interface SET_CATEGORY {
readonly type: "SET_CATEGORY";
readonly category: string;
}
interface SET_NUM_JOKES {
readonly type: "SET_NUM_JOKES";
readonly numJokes: number;
}
type Events =
| GET_JOKES
| CLEAR_JOKES
| CLEAR_ERRORS
| GET_CATEGORIES
| SET_CATEGORY
| SET_NUM_JOKES;
const onGetJoke = async (context: Context): Promise<Partial<Context>> => {
const { category, jokes } = context;
const joke = await getChuckNorrisJoke(category);
return {
jokes: jokes.add(joke),
};
};
const onGetCategories = async (): Promise<Partial<Context>> => {
const newCategories = await getChuckNorrisJokeCategories();
return {
categories: new Set(newCategories),
};
};
const chuckNorrisJokeMachine = setup({
types: {
context: {} as Context,
events: {} as Events,
},
actors: {
onGetJoke: fromPromise<
Partial<Context>,
{ params: GET_JOKES; context: Context }
>(({ input: { context } }) => onGetJoke(context)),
onGetCategories: fromPromise<
Partial<Context>,
{ params: GET_CATEGORIES; context: Context }
>(() => onGetCategories()),
},
actions: {},
}).createMachine({
id: "chuckNorrisJokeMachine",
context: Context,
initial: "Idle",
states: {
GettingJokes: {
id: "gettingJokes",
// How to get jokes in parallel
// based on the number of jokes
// in the context
invoke: {
src: "onGetJoke",
input: ({ context, event }) => {
if (event.type === "GET_JOKES") {
return { context, params: event };
}
throw new Error(`event type ${event.type} cannot call onGetJoke`);
},
onDone: {
target: "Idle",
actions: assign(({ event }) => event.output),
},
},
},
GettingCategories: {
invoke: {
src: "onGetCategories",
input: ({ context, event }) => {
if (event.type === "GET_CATEGORIES") {
return { context, params: event };
}
throw new Error(
`event type ${event.type} cannot call onGetCategories`,
);
},
onDone: {
target: "Idle",
actions: assign(({ event }) => event.output),
},
},
},
Idle: {
on: {
GET_JOKES: {
target: "GettingJokes",
},
GET_CATEGORIES: {
target: "GettingCategories",
},
CLEAR_ERRORS: {
actions: assign({ errors: [] }),
},
CLEAR_JOKES: {
actions: assign({ jokes: new Set<Joke>() }),
},
SET_NUM_JOKES: {
actions: assign({
numJokes: ({ event }) => event.numJokes,
}),
}
},
},
},
});
export default chuckNorrisJokeMachine;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment