ferrybig icon

React useFetch with cache

ferrybig | PRO | 03/13/25 12:26:25 PM UTC | 0 ⭐ | 12665 👁️ | Never ⏰ | []
JavaScript |

5.52 KB

|

Source Code

|

0 👍

/

0 👎

 
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>
    );
}
 

Comments