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 <>
<p>
<button onClick={onClick}>{opened ? 'Close ' : 'Open '}{summary}</button>
</p>
{opened && <Suspense fallback={<p>Loading...</p>}>
{children}
</Suspense>}
</>
}
function FetchSingle ({ url, Component }) {
const json = useFetch(url);
return <fieldset>
<legend><code>{url}</code></legend>
<Component data={json} />
</fieldset>;
}
function FetchList ({ url, Component }) {
const json = useFetch(url);
return <fieldset>
<legend><code>{url}</code></legend>
{json.map(item => <fieldset key={item.id}><Component data={item} /></fieldset>)}
</fieldset>;
}
function User ({ data }) {
return <>
<p>User: {data.name}</p>
<p>Email: {data.email}</p>
<p>Phone: {data.phone}</p>
<Expander summary="Posts">
<FetchList url={`https://jsonplaceholder.typicode.com/users/${data.id}/posts`} Component={Post} />
</Expander>
</>;
}
function Post ({ data }) {
return <>
<p>Post: {data.title}</p>
<p>Body: {data.body}</p>
<Expander summary={`User ${data.userId}`}>
<FetchSingle url={`https://jsonplaceholder.typicode.com/users/${data.userId}`} Component={User} />
</Expander>
<Expander summary={`Comments`}>
<FetchList url={`https://jsonplaceholder.typicode.com/posts/${data.id}/comments`} Component={Comment} />
</Expander>
<Expander summary={`Todos`}>
<FetchList url={`https://jsonplaceholder.typicode.com/posts/${data.id}/todos`} Component={Todo} />
</Expander>
<Expander summary={`Albums`}>
<FetchList url={`https://jsonplaceholder.typicode.com/posts/${data.id}/albums`} Component={Album} />
</Expander>
</>;
}
function Comment ({ data }) {
return <>
<p>Comment: {data.name}</p>
<p>Email: {data.email}</p>
<p>Body: {data.body}</p>
<Expander summary={`Post ${data.postId}`}>
<FetchSingle url={`https://jsonplaceholder.typicode.com/posts/${data.postId}`} Component={Post} />
</Expander>
</>;
}
function Todo ({ data }) {
return <>
<p>Todo: {data.title}</p>
<p>Completed: {data.completed ? 'Yes' : 'No'}</p>
<Expander summary={`User ${data.userId}`}>
<FetchSingle url={`https://jsonplaceholder.typicode.com/users/${data.userId}`} Component={User} />
</Expander>
</>;
}
function Album ({ data }) {
return <>
<p>Album: {data.title}</p>
<Expander summary={`User ${data.userId}`}>
<FetchSingle url={`https://jsonplaceholder.typicode.com/users/${data.userId}`} Component={User} />
</Expander>
<Expander summary={`Photos`}>
<FetchList url={`https://jsonplaceholder.typicode.com/albums/${data.id}/photos`} Component={Photo} />
</Expander>
</>;
}
function Photo ({ data }) {
return <>
<p>Photo: {data.title}</p>
<p>URL: <a href={data.url}>{data.url}</a></p>
</>;
}
export function App () {
return (
<Suspense fallback={<p>Loading...</p>}>
<FetchSingle url="https://jsonplaceholder.typicode.com/users/1" Component={User} />
</Suspense>
);
}