Make app data loading safe under StrictMode
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
This commit is contained in:
parent
b470a34b2a
commit
99d916f6db
6 changed files with 207 additions and 32 deletions
|
|
@ -32,3 +32,4 @@
|
||||||
{"id":"int-f9e84a155b896c7f68c83ee2454f904d","kind":"field_change","created_at":"2026-09-11T11:56:43.680278965Z","actor":"Shinuwa","issue_id":"sokko-g-sc7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reproduced memo draft loss on notification, isolated per-memo drafts using useInlineEdit, preserved multiline/trim/delete behavior. Goal Tree reconciles selection and valid tabs before DOM commit with StrictMode regressions. npm run check:full passed: 100 Node tests and 21 E2E."}}
|
{"id":"int-f9e84a155b896c7f68c83ee2454f904d","kind":"field_change","created_at":"2026-09-11T11:56:43.680278965Z","actor":"Shinuwa","issue_id":"sokko-g-sc7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reproduced memo draft loss on notification, isolated per-memo drafts using useInlineEdit, preserved multiline/trim/delete behavior. Goal Tree reconciles selection and valid tabs before DOM commit with StrictMode regressions. npm run check:full passed: 100 Node tests and 21 E2E."}}
|
||||||
{"id":"int-4b64d68baf132fa99cb3244d8487034c","kind":"field_change","created_at":"2026-09-11T12:05:04.580370366Z","actor":"Shinuwa","issue_id":"sokko-g-kqo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Shared keyed state initializes Notepad/ImageAnnotation temporary drawings and RNG history before DOM commit; ignores setters bound to a different active key. Storage keys and write/error paths preserved. npm run check:full passed: 100 Node tests and 23 E2E including StrictMode key switches and real Notepad canvas/page/reload checks."}}
|
{"id":"int-4b64d68baf132fa99cb3244d8487034c","kind":"field_change","created_at":"2026-09-11T12:05:04.580370366Z","actor":"Shinuwa","issue_id":"sokko-g-kqo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Shared keyed state initializes Notepad/ImageAnnotation temporary drawings and RNG history before DOM commit; ignores setters bound to a different active key. Storage keys and write/error paths preserved. npm run check:full passed: 100 Node tests and 23 E2E including StrictMode key switches and real Notepad canvas/page/reload checks."}}
|
||||||
{"id":"int-80659dd55d71f2a33d1067b008bf2abd","kind":"field_change","created_at":"2026-09-11T17:30:09.794064421Z","actor":"Shinuwa","issue_id":"sokko-g-275","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed RNG draft/edit transitions and ImageViewer pre-commit resets; cover decoding now guards stale completion/errors and releases resources. Four new StrictMode E2E cases pass; npm run check:full passes with 27 E2E. No storage or style changes."}}
|
{"id":"int-80659dd55d71f2a33d1067b008bf2abd","kind":"field_change","created_at":"2026-09-11T17:30:09.794064421Z","actor":"Shinuwa","issue_id":"sokko-g-275","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed RNG draft/edit transitions and ImageViewer pre-commit resets; cover decoding now guards stale completion/errors and releases resources. Four new StrictMode E2E cases pass; npm run check:full passes with 27 E2E. No storage or style changes."}}
|
||||||
|
{"id":"int-6ad8b329d6c9580520514a4234fc5230","kind":"field_change","created_at":"2026-09-11T18:38:38.171744457Z","actor":"Shinuwa","issue_id":"sokko-g-rcs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed lazy game loading without synchronous effect state updates; AbortController reaches editorial, game and nested list requests. Stale outcomes ignored, navigation cache preserved. Three new StrictMode behavior tests; npm run check:full passes with 30 E2E; targeted set-state-in-effect lint passes."}}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Déclarer le jeu dans `website/public/data/site.json`.
|
- Déclarer le jeu dans `website/public/data/site.json`.
|
||||||
- Vérifier la page `/games`.
|
- Vérifier la page `/games`.
|
||||||
- Vérifier la page `/games/<gameId>`.
|
- Vérifier la page `/games/<gameId>`.
|
||||||
|
- Vérifier le chargement à la première visite, la conservation des données pendant la navigation et les erreurs réseau ; sous StrictMode, les requêtes abandonnées ne doivent pas remplacer les données actuelles ni afficher d'erreur d'annulation.
|
||||||
- Vérifier le bouton toolbox et le panneau latéral.
|
- Vérifier le bouton toolbox et le panneau latéral.
|
||||||
- Vérifier le breadcrumb.
|
- Vérifier le breadcrumb.
|
||||||
- Vérifier les textes avec retours à la ligne.
|
- Vérifier les textes avec retours à la ligne.
|
||||||
|
|
|
||||||
119
e2e/app-data.spec.js
Normal file
119
e2e/app-data.spec.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Rôle : couvre chargement différé, cache de navigation, erreurs et annulations des données publiques.
|
||||||
|
/* global window, document */
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/app-data-probe.js", import.meta.url))}`;
|
||||||
|
const siteUrl = "/data/site.json";
|
||||||
|
const gamesUrl = "/data/games.json";
|
||||||
|
const diabloUrl = "/data/diablo4/affixes_types.json";
|
||||||
|
const mhwBase = "/data/mhwilds/";
|
||||||
|
|
||||||
|
async function setup(page, route = "/") {
|
||||||
|
await page.route("**/__app-data-probe", (request) => request.fulfill({
|
||||||
|
contentType: "text/html", body: '<!doctype html><div id="probe"></div>'
|
||||||
|
}));
|
||||||
|
await page.goto("/__app-data-probe");
|
||||||
|
await page.evaluate(async ({ url, route }) => {
|
||||||
|
const { mountAppDataProbe } = await import(url);
|
||||||
|
window.dataProbe = mountAppDataProbe(document.getElementById("probe"), route);
|
||||||
|
}, { url: probeUrl, route });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function state(page) {
|
||||||
|
return JSON.parse(await page.locator("#data-state").textContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function respond(page, url, payload, status = 200, aborted = false) {
|
||||||
|
await page.evaluate(({ url, payload, status, aborted }) => {
|
||||||
|
window.dataProbe.respond(url, payload, status, aborted);
|
||||||
|
}, { url, payload, status, aborted });
|
||||||
|
}
|
||||||
|
|
||||||
|
test("game data loads only on demand and remains cached through navigation", async ({ page }) => {
|
||||||
|
await setup(page);
|
||||||
|
expect((await state(page)).mhwilds.loading).toBe(false);
|
||||||
|
expect(await page.evaluate(() => window.dataProbe.requests().map((request) => request.url)))
|
||||||
|
.toEqual([siteUrl, gamesUrl, siteUrl, gamesUrl]);
|
||||||
|
await respond(page, siteUrl, { title: "Current" });
|
||||||
|
await respond(page, gamesUrl, { games: [{ id: "mhwilds" }] });
|
||||||
|
await expect.poll(async () => (await state(page)).siteContent?.title).toBe("Current");
|
||||||
|
await respond(page, siteUrl, { title: "Obsolete" }, 200, true);
|
||||||
|
await respond(page, gamesUrl, {}, 503, true);
|
||||||
|
|
||||||
|
const snapshots = await page.evaluate(() => {
|
||||||
|
window.dataProbe.navigate("/games/mhwilds");
|
||||||
|
return window.dataProbe.snapshots;
|
||||||
|
});
|
||||||
|
expect(snapshots.length).toBeGreaterThan(0);
|
||||||
|
for (const snapshot of snapshots) {
|
||||||
|
expect(snapshot.mhwilds.loading).toBe(true);
|
||||||
|
expect(snapshot.mhwilds.loaded).toBe(false);
|
||||||
|
expect(snapshot.siteContent.title).toBe("Current");
|
||||||
|
expect(snapshot.gamesError).toBe("");
|
||||||
|
}
|
||||||
|
await respond(page, `${mhwBase}monsters.json`, { monsters: [{ id: "rey-dau" }] });
|
||||||
|
await respond(page, `${mhwBase}endemic_life.json`, {});
|
||||||
|
await respond(page, `${mhwBase}lists/index.json`, { lists: [{ id: "farm", file: "farm.json" }] });
|
||||||
|
await respond(page, `${mhwBase}i18n/fr.json`, { hello: "Bonjour" });
|
||||||
|
await expect.poll(() => page.evaluate(() => window.dataProbe.requests().some((request) => request.url.endsWith("/farm.json")))).toBe(true);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.dataProbe.navigate("/games/diablo4");
|
||||||
|
window.dataProbe.navigate("/");
|
||||||
|
});
|
||||||
|
await respond(page, `${mhwBase}lists/farm.json`, { titre: "Farm", items: [{ nom: "Potion", quantite: 2 }] });
|
||||||
|
await respond(page, diabloUrl, { affixes: [{ id: "damage" }], categories: [{ id: "attack" }] });
|
||||||
|
await expect.poll(async () => (await state(page)).mhwilds.loaded).toBe(true);
|
||||||
|
await expect.poll(async () => (await state(page)).diablo4.loaded).toBe(true);
|
||||||
|
const loaded = await state(page);
|
||||||
|
expect(loaded.mhwilds.lists[0].categories[0].items[0].quantity).toBe(2);
|
||||||
|
expect(loaded.diablo4.categoryMap.attack).toEqual({ id: "attack" });
|
||||||
|
const count = await page.evaluate(() => window.dataProbe.requests().length);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.dataProbe.navigate("/games/mhwilds/monsters");
|
||||||
|
window.dataProbe.navigate("/games/diablo4");
|
||||||
|
});
|
||||||
|
expect(await page.evaluate(() => window.dataProbe.requests().length)).toBe(count);
|
||||||
|
expect((await state(page)).mhwilds.loading).toBe(false);
|
||||||
|
await page.evaluate(() => window.dataProbe.unmount());
|
||||||
|
expect(await page.evaluate(() => window.dataProbe.requests().every((request) => request.aborted))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("StrictMode discards obsolete game failures and preserves current editorial errors", async ({ page }) => {
|
||||||
|
await setup(page, "/games/diablo4");
|
||||||
|
const requests = await page.evaluate(() => window.dataProbe.requests().filter((request) => request.url.includes("diablo4")));
|
||||||
|
expect(requests.map((request) => request.aborted)).toEqual([true, false]);
|
||||||
|
expect((await state(page)).diablo4.loading).toBe(true);
|
||||||
|
await respond(page, diabloUrl, { affixes: [{ id: "current" }] });
|
||||||
|
await expect.poll(async () => (await state(page)).diablo4.loaded).toBe(true);
|
||||||
|
await respond(page, diabloUrl, {}, 503, true);
|
||||||
|
await respond(page, siteUrl, {}, 503);
|
||||||
|
await respond(page, gamesUrl, { games: [] });
|
||||||
|
await expect.poll(async () => (await state(page)).siteContentError).toBe("Impossible de charger le contenu du site.");
|
||||||
|
expect((await state(page)).diablo4.error).toBe("");
|
||||||
|
expect((await state(page)).diablo4.affixes).toEqual([{ id: "current" }]);
|
||||||
|
await page.evaluate(() => window.dataProbe.unmount());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("failed game loading stops and pending list requests are aborted on unmount", async ({ page }) => {
|
||||||
|
await setup(page, "/games/diablo4");
|
||||||
|
await respond(page, diabloUrl, {}, 503);
|
||||||
|
await expect.poll(async () => (await state(page)).diablo4.error).toBe("Impossible de charger les données Diablo IV.");
|
||||||
|
expect((await state(page)).diablo4).toMatchObject({ loaded: true, loading: false });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.dataProbe.navigate("/");
|
||||||
|
window.dataProbe.navigate("/games/diablo4");
|
||||||
|
window.dataProbe.navigate("/games/mhwilds");
|
||||||
|
});
|
||||||
|
expect(await page.evaluate(() => window.dataProbe.requests().filter((request) => request.url.includes("diablo4")).length)).toBe(2);
|
||||||
|
await respond(page, `${mhwBase}monsters.json`, {});
|
||||||
|
await respond(page, `${mhwBase}endemic_life.json`, {});
|
||||||
|
await respond(page, `${mhwBase}lists/index.json`, { lists: [{ file: "pending.json" }] });
|
||||||
|
await respond(page, `${mhwBase}i18n/fr.json`, {});
|
||||||
|
await expect.poll(() => page.evaluate(() => window.dataProbe.requests().some((request) => request.url.endsWith("/pending.json")))).toBe(true);
|
||||||
|
await page.evaluate(() => window.dataProbe.unmount());
|
||||||
|
expect(await page.evaluate(() => window.dataProbe.requests().every((request) => request.aborted))).toBe(true);
|
||||||
|
await respond(page, `${mhwBase}lists/pending.json`, {}, 503, true);
|
||||||
|
await respond(page, siteUrl, {}, 503, true);
|
||||||
|
await respond(page, gamesUrl, {}, 503, true);
|
||||||
|
});
|
||||||
44
e2e/fixtures/app-data-probe.js
Normal file
44
e2e/fixtures/app-data-probe.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Rôle : observe le chargement réel de l'application avec des réponses réseau contrôlées sous StrictMode.
|
||||||
|
import { createElement, StrictMode, useLayoutEffect } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
|
import { useAppData } from "../../website/src/hooks/useAppData.js";
|
||||||
|
|
||||||
|
export function mountAppDataProbe(element, initialRoute) {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const requests = [];
|
||||||
|
const snapshots = [];
|
||||||
|
const root = createRoot(element);
|
||||||
|
globalThis.fetch = (url, { signal } = {}) => new Promise((resolve) => {
|
||||||
|
// Le double ignore volontairement abort pour tester aussi les réponses déjà en cours.
|
||||||
|
requests.push({ url, signal, resolve, settled: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
function Probe({ route }) {
|
||||||
|
const data = useAppData(route);
|
||||||
|
useLayoutEffect(() => { snapshots.push(data); });
|
||||||
|
return createElement("pre", { id: "data-state" }, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(route) {
|
||||||
|
snapshots.length = 0;
|
||||||
|
flushSync(() => root.render(createElement(StrictMode, null, createElement(Probe, { route }))));
|
||||||
|
}
|
||||||
|
navigate(initialRoute);
|
||||||
|
return {
|
||||||
|
snapshots,
|
||||||
|
requests: () => requests.map(({ url, signal, settled }) => ({ url, aborted: signal?.aborted, settled })),
|
||||||
|
respond(url, payload, status = 200, aborted = false) {
|
||||||
|
for (const request of requests) {
|
||||||
|
if (request.url !== url || request.settled || request.signal?.aborted !== aborted) continue;
|
||||||
|
request.settled = true;
|
||||||
|
request.resolve({ ok: status >= 200 && status < 300, json: async () => payload });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigate,
|
||||||
|
unmount() {
|
||||||
|
flushSync(() => root.unmount());
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -21,12 +21,12 @@ export const INITIAL_DIABLO4_STATE = {
|
||||||
filterOptionKeys: { affixes: "" }
|
filterOptionKeys: { affixes: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadMhwildsData() {
|
export async function loadMhwildsData(signal) {
|
||||||
const [monstersResponse, endemicResponse, listIndexResponse, translationsResponse] = await Promise.all([
|
const [monstersResponse, endemicResponse, listIndexResponse, translationsResponse] = await Promise.all([
|
||||||
fetch("/data/mhwilds/monsters.json"),
|
fetch("/data/mhwilds/monsters.json", { signal }),
|
||||||
fetch("/data/mhwilds/endemic_life.json"),
|
fetch("/data/mhwilds/endemic_life.json", { signal }),
|
||||||
fetch("/data/mhwilds/lists/index.json"),
|
fetch("/data/mhwilds/lists/index.json", { signal }),
|
||||||
fetch("/data/mhwilds/i18n/fr.json")
|
fetch("/data/mhwilds/i18n/fr.json", { signal })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!monstersResponse.ok || !endemicResponse.ok || !listIndexResponse.ok || !translationsResponse.ok) {
|
if (!monstersResponse.ok || !endemicResponse.ok || !listIndexResponse.ok || !translationsResponse.ok) {
|
||||||
|
|
@ -39,7 +39,7 @@ export async function loadMhwildsData() {
|
||||||
listIndexResponse.json(),
|
listIndexResponse.json(),
|
||||||
translationsResponse.json()
|
translationsResponse.json()
|
||||||
]);
|
]);
|
||||||
const lists = await loadGameLists("mhwilds", listIndexJson);
|
const lists = await loadGameLists("mhwilds", listIndexJson, signal);
|
||||||
const monsterFilterKey = Object.keys(monstersJson).find((key) => key !== "monsters") || "";
|
const monsterFilterKey = Object.keys(monstersJson).find((key) => key !== "monsters") || "";
|
||||||
const endemicFilterKey = Object.keys(endemicJson).find((key) => key !== "endemicLife" && key !== "aquaticLife") || "";
|
const endemicFilterKey = Object.keys(endemicJson).find((key) => key !== "endemicLife" && key !== "aquaticLife") || "";
|
||||||
|
|
||||||
|
|
@ -62,10 +62,10 @@ export async function loadMhwildsData() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadGameLists(gameId, indexPayload) {
|
async function loadGameLists(gameId, indexPayload, signal) {
|
||||||
const entries = Array.isArray(indexPayload?.lists) ? indexPayload.lists : [];
|
const entries = Array.isArray(indexPayload?.lists) ? indexPayload.lists : [];
|
||||||
const results = await Promise.all(entries.map(async (entry) => {
|
const results = await Promise.all(entries.map(async (entry) => {
|
||||||
const response = await fetch(`/data/${gameId}/lists/${entry.file}`);
|
const response = await fetch(`/data/${gameId}/lists/${entry.file}`, { signal });
|
||||||
if (!response.ok) throw new Error(`Impossible de charger la liste ${entry.file}.`);
|
if (!response.ok) throw new Error(`Impossible de charger la liste ${entry.file}.`);
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
return normalizeGameListFile(entry, payload);
|
return normalizeGameListFile(entry, payload);
|
||||||
|
|
@ -107,8 +107,8 @@ function formatListTitle(value) {
|
||||||
.replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
|
.replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDiablo4Data() {
|
export async function loadDiablo4Data(signal) {
|
||||||
const response = await fetch("/data/diablo4/affixes_types.json");
|
const response = await fetch("/data/diablo4/affixes_types.json", { signal });
|
||||||
if (!response.ok) throw new Error("Impossible de charger les données Diablo IV.");
|
if (!response.ok) throw new Error("Impossible de charger les données Diablo IV.");
|
||||||
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
|
|
|
||||||
|
|
@ -2,52 +2,62 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { INITIAL_DIABLO4_STATE, INITIAL_MHWILDS_STATE, loadDiablo4Data, loadMhwildsData } from "../features/games/loaders.js";
|
import { INITIAL_DIABLO4_STATE, INITIAL_MHWILDS_STATE, loadDiablo4Data, loadMhwildsData } from "../features/games/loaders.js";
|
||||||
|
|
||||||
|
function useGameData(active, initialState, load) {
|
||||||
|
const [requested, setRequested] = useState(active);
|
||||||
|
const [data, setData] = useState(initialState);
|
||||||
|
if (active && !requested) setRequested(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requested) return undefined;
|
||||||
|
const controller = new AbortController();
|
||||||
|
load(controller.signal)
|
||||||
|
.then((result) => {
|
||||||
|
if (!controller.signal.aborted) setData(result);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (!controller.signal.aborted) setData({ ...initialState, loaded: true, error: error.message });
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [requested, initialState, load]);
|
||||||
|
|
||||||
|
// Une visite déclenche un seul chargement, conservé même si la route change.
|
||||||
|
return requested && !data.loaded ? { ...data, loading: true } : data;
|
||||||
|
}
|
||||||
|
|
||||||
export function useAppData(route) {
|
export function useAppData(route) {
|
||||||
const [siteContent, setSiteContent] = useState(null);
|
const [siteContent, setSiteContent] = useState(null);
|
||||||
const [siteContentError, setSiteContentError] = useState("");
|
const [siteContentError, setSiteContentError] = useState("");
|
||||||
const [games, setGames] = useState([]);
|
const [games, setGames] = useState([]);
|
||||||
const [gamesError, setGamesError] = useState("");
|
const [gamesError, setGamesError] = useState("");
|
||||||
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
|
const mhwilds = useGameData(route.startsWith("/games/mhwilds"), INITIAL_MHWILDS_STATE, loadMhwildsData);
|
||||||
const [diablo4, setDiablo4] = useState(INITIAL_DIABLO4_STATE);
|
const diablo4 = useGameData(route.startsWith("/games/diablo4"), INITIAL_DIABLO4_STATE, loadDiablo4Data);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch("/data/site.json").then((response) => {
|
fetch("/data/site.json", { signal }).then((response) => {
|
||||||
if (!response.ok) throw new Error("Impossible de charger le contenu du site.");
|
if (!response.ok) throw new Error("Impossible de charger le contenu du site.");
|
||||||
return response.json();
|
return response.json();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
setSiteContentError(error.message);
|
if (!signal.aborted) setSiteContentError(error.message);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
fetch("/data/games.json").then((response) => {
|
fetch("/data/games.json", { signal }).then((response) => {
|
||||||
if (!response.ok) throw new Error("Impossible de charger la liste des jeux.");
|
if (!response.ok) throw new Error("Impossible de charger la liste des jeux.");
|
||||||
return response.json();
|
return response.json();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
setGamesError(error.message);
|
if (!signal.aborted) setGamesError(error.message);
|
||||||
return { games: [] };
|
return { games: [] };
|
||||||
})
|
})
|
||||||
]).then(([content, gamesPayload]) => {
|
]).then(([content, gamesPayload]) => {
|
||||||
|
if (signal.aborted) return;
|
||||||
if (content) setSiteContent(content);
|
if (content) setSiteContent(content);
|
||||||
setGames(Array.isArray(gamesPayload.games) ? gamesPayload.games : []);
|
setGames(Array.isArray(gamesPayload.games) ? gamesPayload.games : []);
|
||||||
});
|
});
|
||||||
|
return () => controller.abort();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!route.startsWith("/games/mhwilds") || mhwilds.loaded || mhwilds.loading) return;
|
|
||||||
setMhwilds((state) => ({ ...state, loading: true, error: "" }));
|
|
||||||
loadMhwildsData()
|
|
||||||
.then(setMhwilds)
|
|
||||||
.catch((error) => setMhwilds({ ...INITIAL_MHWILDS_STATE, loaded: true, error: error.message }));
|
|
||||||
}, [route, mhwilds.loaded, mhwilds.loading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!route.startsWith("/games/diablo4") || diablo4.loaded || diablo4.loading) return;
|
|
||||||
setDiablo4((state) => ({ ...state, loading: true, error: "" }));
|
|
||||||
loadDiablo4Data()
|
|
||||||
.then(setDiablo4)
|
|
||||||
.catch((error) => setDiablo4({ ...INITIAL_DIABLO4_STATE, loaded: true, error: error.message }));
|
|
||||||
}, [route, diablo4.loaded, diablo4.loading]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
siteContent,
|
siteContent,
|
||||||
siteContentError,
|
siteContentError,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue