Make app data loading safe under StrictMode
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s

This commit is contained in:
Shinuwa 2026-09-11 21:09:29 +02:00
parent b470a34b2a
commit 99d916f6db
6 changed files with 207 additions and 32 deletions

View file

@ -21,12 +21,12 @@ export const INITIAL_DIABLO4_STATE = {
filterOptionKeys: { affixes: "" }
};
export async function loadMhwildsData() {
export async function loadMhwildsData(signal) {
const [monstersResponse, endemicResponse, listIndexResponse, translationsResponse] = await Promise.all([
fetch("/data/mhwilds/monsters.json"),
fetch("/data/mhwilds/endemic_life.json"),
fetch("/data/mhwilds/lists/index.json"),
fetch("/data/mhwilds/i18n/fr.json")
fetch("/data/mhwilds/monsters.json", { signal }),
fetch("/data/mhwilds/endemic_life.json", { signal }),
fetch("/data/mhwilds/lists/index.json", { signal }),
fetch("/data/mhwilds/i18n/fr.json", { signal })
]);
if (!monstersResponse.ok || !endemicResponse.ok || !listIndexResponse.ok || !translationsResponse.ok) {
@ -39,7 +39,7 @@ export async function loadMhwildsData() {
listIndexResponse.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 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 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}.`);
const payload = await response.json();
return normalizeGameListFile(entry, payload);
@ -107,8 +107,8 @@ function formatListTitle(value) {
.replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
}
export async function loadDiablo4Data() {
const response = await fetch("/data/diablo4/affixes_types.json");
export async function loadDiablo4Data(signal) {
const response = await fetch("/data/diablo4/affixes_types.json", { signal });
if (!response.ok) throw new Error("Impossible de charger les données Diablo IV.");
const payload = await response.json();

View file

@ -2,52 +2,62 @@
import { useEffect, useState } from "react";
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) {
const [siteContent, setSiteContent] = useState(null);
const [siteContentError, setSiteContentError] = useState("");
const [games, setGames] = useState([]);
const [gamesError, setGamesError] = useState("");
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
const [diablo4, setDiablo4] = useState(INITIAL_DIABLO4_STATE);
const mhwilds = useGameData(route.startsWith("/games/mhwilds"), INITIAL_MHWILDS_STATE, loadMhwildsData);
const diablo4 = useGameData(route.startsWith("/games/diablo4"), INITIAL_DIABLO4_STATE, loadDiablo4Data);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
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.");
return response.json();
}).catch((error) => {
setSiteContentError(error.message);
if (!signal.aborted) setSiteContentError(error.message);
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.");
return response.json();
}).catch((error) => {
setGamesError(error.message);
if (!signal.aborted) setGamesError(error.message);
return { games: [] };
})
]).then(([content, gamesPayload]) => {
if (signal.aborted) return;
if (content) setSiteContent(content);
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 {
siteContent,
siteContentError,