Skip to content

Commit e9f6a30

Browse files
authored
fix(website): fix reactivity of pokedex examples (#17)
* fix(website): fix reactivity of pokedex examples * fix(website): pr fixes
1 parent 47d9f6b commit e9f6a30

4 files changed

Lines changed: 119 additions & 72 deletions

File tree

apps/website/src/pages/tutorial/pokedex-context.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,25 @@ const code = {
5656
const pokemon = context("pokemon", "charizard");
5757
5858
const pokemonPicker = ilha
59-
.state("pokemonList", [])
59+
.state('pokemonList', [])
6060
.onMount(({ state }) => {
6161
const fetchList = async () => {
62-
const req = await fetch("https://pokeapi.co/api/v2/pokemon");
62+
const req = await fetch('https://pokeapi.co/api/v2/pokemon');
6363
const list = await req.json();
6464
state.pokemonList(list.results);
6565
};
6666
fetchList();
6767
})
68-
.bind("#pokemon", pokemon)
68+
.bind('#pokemon', pokemon)
6969
.render(({ state }) => {
70-
const options = state
71-
.pokemonList()
72-
.map(({ name }) => html\`
73-
<option value="\${name}">\${name}</option>
74-
\`);
70+
const currentPokemon = pokemon();
71+
const options = state.pokemonList().map(
72+
({ name }) => html\`
73+
<option value="\${name}" \${
74+
name === currentPokemon ? 'selected' : ''
75+
}>\${name}</option>
76+
\`
77+
);
7578
return html\`
7679
<label for="pokemon">Pick a Pokemon</label>
7780
<select id="pokemon">

apps/website/src/pages/tutorial/pokedex-input.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,49 @@ const code = {
5252
5353
const pokemonPicker = ilha
5454
.input(type<{ defaultPokemon: string }>())
55-
.state("pokemon", ({ defaultPokemon }) => defaultPokemon)
56-
.state("pokemonList", [])
57-
.state("pokemonData", null)
55+
.state('pokemon', ({ defaultPokemon }) => defaultPokemon)
56+
.state('pokemonList', [])
57+
.state('pokemonData', null)
5858
.onMount(({ state }) => {
5959
const fetchList = async () => {
60-
const req = await fetch("https://pokeapi.co/api/v2/pokemon");
60+
const req = await fetch('https://pokeapi.co/api/v2/pokemon');
6161
const list = await req.json();
6262
state.pokemonList(list.results);
6363
};
64+
fetchList();
65+
})
66+
.effect(({ state }) => {
67+
const controller = new AbortController();
6468
const fetchPokemon = async () => {
65-
const req = await fetch(\`https://pokeapi.co/api/v2/pokemon/\${state.pokemon()}\`);
66-
const data = await req.json();
67-
state.pokemonData(data);
69+
const pokemon = state.pokemon();
70+
try {
71+
const req = await fetch(
72+
\`https://pokeapi.co/api/v2/pokemon/\${pokemon}\`,
73+
{ signal: controller.signal }
74+
);
75+
const data = await req.json();
76+
// Only update if this request wasn't aborted (still the latest)
77+
if (!controller.signal.aborted) {
78+
state.pokemonData(data);
79+
}
80+
} catch (err) {
81+
// Ignore abort errors
82+
if (err.name !== 'AbortError') throw err;
83+
}
6884
};
69-
fetchList();
7085
fetchPokemon();
86+
return () => controller.abort();
7187
})
72-
.bind("#pokemon", "pokemon")
88+
.bind('#pokemon', 'pokemon')
7389
.render(({ state }) => {
74-
const options = state
75-
.pokemonList()
76-
.map(({ name }) => html\`
77-
<option value="\${name}">\${name}</option>
78-
\`);
90+
const currentPokemon = state.pokemon();
91+
const options = state.pokemonList().map(
92+
({ name }) => html\`
93+
<option value="\${name}" \${
94+
name === currentPokemon ? 'selected' : ''
95+
}>\${name}</option>
96+
\`
97+
);
7998
8099
const card = state.pokemonData()
81100
? html\`

apps/website/src/pages/tutorial/pokedex-onmount.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ const content = dedent`
1717
fetch a resource, resolve its result, and write it into state — Ilha will
1818
re-render automatically once the data arrives.
1919
20-
The example fetches both the Pokémon list and the selected Pokémon's data on mount.
21-
Because \`.onMount()\` doesn't await async functions directly, each fetch is wrapped
22-
in an inner async function and called immediately — a common pattern for async work
23-
inside synchronous callbacks.
20+
The example uses \`.onMount()\` to fetch the Pokémon list once. For the selected
21+
Pokémon's data, it uses \`.effect()\` which re-runs whenever \`state.pokemon()\` changes.
22+
The \`fetchPokemon\` function inside the effect reads the current selection and
23+
fetches the corresponding data, writing the result to \`state.pokemonData()\`.
24+
Because \`.onMount()\` and \`.effect()\` don't await async functions directly, each
25+
fetch is wrapped in an inner async function and called immediately — a common
26+
pattern for async work inside synchronous callbacks.
2427
2528
### Similar concepts
2629
@@ -38,33 +41,52 @@ const code = {
3841
<div data-ilha="pokedex"></div>
3942
`,
4043
script: dedent`
41-
import ilha, { html, mount } from "ilha";
44+
import ilha, { html, mount } from 'ilha';
4245
4346
const pokedex = ilha
44-
.state("pokemon", "charizard")
45-
.state("pokemonList", [])
46-
.state("pokemonData", null)
47+
.state('pokemon', 'charizard')
48+
.state('pokemonList', [])
49+
.state('pokemonData', null)
4750
.onMount(({ state }) => {
4851
const fetchList = async () => {
49-
const req = await fetch("https://pokeapi.co/api/v2/pokemon");
52+
const req = await fetch('https://pokeapi.co/api/v2/pokemon');
5053
const list = await req.json();
5154
state.pokemonList(list.results);
5255
};
56+
fetchList();
57+
})
58+
.effect(({ state }) => {
59+
const controller = new AbortController();
5360
const fetchPokemon = async () => {
54-
const req = await fetch(\`https://pokeapi.co/api/v2/pokemon/\${state.pokemon()}\`);
55-
const data = await req.json();
56-
state.pokemonData(data);
61+
const pokemon = state.pokemon();
62+
try {
63+
const req = await fetch(
64+
\`https://pokeapi.co/api/v2/pokemon/\${pokemon}\`,
65+
{ signal: controller.signal }
66+
);
67+
const data = await req.json();
68+
// Only update if this request wasn't aborted (still the latest)
69+
if (!controller.signal.aborted) {
70+
state.pokemonData(data);
71+
}
72+
} catch (err) {
73+
// Ignore abort errors
74+
if (err.name !== 'AbortError') throw err;
75+
}
5776
};
58-
fetchList();
5977
fetchPokemon();
78+
return () => controller.abort();
6079
})
61-
.bind("#pokemon", "pokemon")
80+
.bind('#pokemon', 'pokemon')
6281
.render(({ state }) => {
63-
const options = state
64-
.pokemonList()
65-
.map(({ name }) => html\`
66-
<option value="\${name}">\${name}</option>
67-
\`);
82+
const currentPokemon = state.pokemon();
83+
const options = state.pokemonList().map(
84+
({ name }) => html\`
85+
<option value="\${name}" \${
86+
name === currentPokemon ? 'selected' : ''
87+
}>\${name}</option>
88+
\`
89+
);
6890
6991
const card = state.pokemonData()
7092
? html\`

apps/website/src/pages/tutorial/pokedex-slot.ts

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,23 @@ const code = {
3737
<div data-ilha="pokedex"></div>
3838
`,
3939
script: dedent`
40-
import ilha, { html, mount, type } from "ilha";
40+
import ilha, { html, mount, context } from "ilha";
41+
42+
const selectedPokemon = context("selectedPokemon", "charizard");
4143
4244
const pokemonPicker = ilha
43-
.input(type<{ defaultPokemon: string }>())
44-
.state("pokemon", ({ defaultPokemon }) => defaultPokemon)
4545
.state("pokemonList", [])
4646
.onMount(({ state }) => {
47-
const fetchList = async () => {
48-
const req = await fetch("https://pokeapi.co/api/v2/pokemon");
49-
const list = await req.json();
50-
state.pokemonList(list.results);
51-
};
52-
fetchList();
47+
fetch("https://pokeapi.co/api/v2/pokemon")
48+
.then((r) => r.json())
49+
.then((list) => state.pokemonList(list.results));
5350
})
54-
.bind("#pokemon", "pokemon")
51+
.bind("#pokemon", selectedPokemon)
5552
.render(({ state }) => {
56-
const options = state
57-
.pokemonList()
58-
.map(({ name }) => html\`
59-
<option value="\${name}">\${name}</option>
60-
\`);
53+
const currentPokemon = selectedPokemon();
54+
const options = state.pokemonList().map(({ name }) =>
55+
html\`<option value="\${name}" \${name === currentPokemon ? "selected" : ""}>\${name}</option>\`
56+
);
6157
return html\`
6258
<label for="pokemon">Pick a Pokemon</label>
6359
<select id="pokemon">
@@ -67,24 +63,32 @@ const code = {
6763
});
6864
6965
const pokemonCard = ilha
70-
.input(type<{ pokemon: string }>())
7166
.state("pokemonData", null)
72-
.onMount(({ state, input }) => {
73-
const fetchPokemon = async () => {
74-
const req = await fetch(\`https://pokeapi.co/api/v2/pokemon/\${input.pokemon}\`);
75-
const data = await req.json();
76-
state.pokemonData(data);
77-
};
78-
fetchPokemon();
67+
.effect(({ state }) => {
68+
const controller = new AbortController();
69+
const pokemon = selectedPokemon();
70+
fetch(\`https://pokeapi.co/api/v2/pokemon/\${pokemon}\`, {
71+
signal: controller.signal,
72+
})
73+
.then((r) => r.json())
74+
.then((data) => {
75+
if (!controller.signal.aborted) {
76+
state.pokemonData(data);
77+
}
78+
})
79+
.catch((err) => {
80+
if (err.name !== 'AbortError') throw err;
81+
});
82+
return () => controller.abort();
7983
})
8084
.render(({ state }) => {
8185
if (!state.pokemonData()) return html\`<p>Loading...</p>\`;
8286
const { name, sprites, types } = state.pokemonData();
83-
const typeBadges = types.map(({ type }) => html\`
84-
<span class="badge">\${type.name}</span>
85-
\`);
87+
const typeBadges = types.map(({ type }) =>
88+
html\`<span class="badge">\${type.name}</span>\`
89+
);
8690
return html\`
87-
<img src="\${sprites.front_default}" />
91+
<img src="\${sprites.front_default}" alt="\${name}" />
8892
<h2>\${name}</h2>
8993
\${typeBadges}
9094
\`;
@@ -93,10 +97,9 @@ const code = {
9397
const pokedex = ilha
9498
.slot("picker", pokemonPicker)
9599
.slot("card", pokemonCard)
96-
.state("selected", "charizard")
97-
.render(({ slots, state }) => html\`
98-
\${slots.picker({ defaultPokemon: state.selected() })}
99-
\${slots.card({ pokemon: state.selected() })}
100+
.render(({ slots }) => html\`
101+
\${slots.picker()}
102+
\${slots.card()}
100103
\`);
101104
102105
mount({ pokedex });

0 commit comments

Comments
 (0)