import { createContext, useSyncExternalStore, useContext, useReducer, Suspense } from 'react'; function assertNever(state) { throw new Error(`Unexpected state: ${state}`); } function createFetchContext() { return { cache: {}, }; } const FetchContext = createContext(createFetchContext()); function createSlice(url) { var debugUrl = new URL(url).pathname console.log(debugUrl + ': Created slice') let state = { state: 'idle', } let listeners = []; const entry = { getState: () => state, dispatch: (action) => { console.log(debugUrl + ': dispatch: ' + action.type) switch (action.type) { case 'reset': state = { state: 'idle', } break; case 'success': state = { state: 'success', data: action.payload, } break; case 'error': state = { state: 'error', data: action.payload, } break; default: return assertNever(action); } listeners.forEach(listener => listener()); }, subscribe: (listener) => { listeners = listeners.toSpliced(-1, 0, listener); console.log(debugUrl + ': subscribe: ' + listeners.length + " listeners") return () => { const index = listeners.indexOf(listener); if (index>= 0) { // We make a copy of the listeners to deal with the case of the listeners changing as we loop over them listeners = listeners.toSpliced(index, 1); } console.log(debugUrl + ': unsubscribe: ' + listeners.length + " listeners") }; }, getStateAndDispatch: () => { if (state.state === 'idle') { // The idle state is a phantom state that is never returned to the user // It is used to indicate that the fetch is in progress console.log(debugUrl + ': getStateAndDispatch: Started fetching') const promise = fetch(url).then(response => response.json()); state = { state: 'loading', data: promise, } promise.then( data => entry.dispatch({ type: 'success', payload: data }), error => entry.dispatch({ type: 'error', payload: error }), ); } return state; }, }; return entry; } function useFetch(url) { var context = useContext(FetchContext); var entry = context.cache[url] ??= createSlice(url); var state = useSyncExternalStore(entry.subscribe, entry.getStateAndDispatch); switch (state.state) { case 'loading': throw state.data; case 'success': return state.data; case 'error': throw state.data; default: return assertNever(state); } } function Expander({ children, summary }) { const [opened, onClick] = useReducer((state) => !state, false); return

{opened && Loading...

}> {children}
}
} function FetchSingle ({ url, Component }) { const json = useFetch(url); return
{url}
; } function FetchList ({ url, Component }) { const json = useFetch(url); return
{url} {json.map(item =>
)}
; } function User ({ data }) { return

User: {data.name}

Email: {data.email}

Phone: {data.phone}

; } function Post ({ data }) { return

Post: {data.title}

Body: {data.body}

; } function Comment ({ data }) { return

Comment: {data.name}

Email: {data.email}

Body: {data.body}

; } function Todo ({ data }) { return

Todo: {data.title}

Completed: {data.completed ? 'Yes' : 'No'}

; } function Album ({ data }) { return

Album: {data.title}

; } function Photo ({ data }) { return

Photo: {data.title}

URL: {data.url}

; } export function App () { return ( Loading...

}>
); }

AltStyle によって変換されたページ (->オリジナル) /