Advertisement
ferrybig

React useFetch with cache

Mar 13th, 2025
101
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 5.52 KB | Source Code | 0 0
  1.  
  2. import { createContext, useSyncExternalStore, useContext, useReducer, Suspense } from 'react';
  3.  
  4. function assertNever(state) {
  5.     throw new Error(`Unexpected state: ${state}`);
  6. }
  7. function createFetchContext() {
  8.     return {
  9.         cache: {},
  10.     };
  11. }
  12.  
  13. const FetchContext = createContext(createFetchContext());
  14.  
  15. function createSlice(url) {
  16.     var debugUrl = new URL(url).pathname
  17.     console.log(debugUrl + ': Created slice')
  18.     let state = {
  19.         state: 'idle',
  20.     }
  21.     let listeners = [];
  22.     const entry = {
  23.         getState: () => state,
  24.         dispatch: (action) => {
  25.             console.log(debugUrl + ': dispatch: ' + action.type)
  26.             switch (action.type) {
  27.                 case 'reset':
  28.                     state = {
  29.                         state: 'idle',
  30.                     }
  31.                     break;
  32.                 case 'success':
  33.                     state = {
  34.                         state: 'success',
  35.                         data: action.payload,
  36.                     }
  37.                     break;
  38.                 case 'error':
  39.                     state = {
  40.                         state: 'error',
  41.                         data: action.payload,
  42.                     }
  43.                     break;
  44.                 default:
  45.                     return assertNever(action);
  46.             }
  47.             listeners.forEach(listener => listener());
  48.         },
  49.         subscribe: (listener) => {
  50.             listeners = listeners.toSpliced(-1, 0, listener);
  51.             console.log(debugUrl + ': subscribe: ' + listeners.length + " listeners")
  52.  
  53.             return () => {
  54.                 const index = listeners.indexOf(listener);
  55.                 if (index >= 0) {
  56.                     // We make a copy of the listeners to deal with the case of the listeners changing as we loop over them
  57.                     listeners = listeners.toSpliced(index, 1);
  58.                 }
  59.                 console.log(debugUrl + ': unsubscribe: ' + listeners.length + " listeners")
  60.             };
  61.         },
  62.         getStateAndDispatch: () => {
  63.             if (state.state === 'idle') {
  64.                 // The idle state is a phantom state that is never returned to the user
  65.                 // It is used to indicate that the fetch is in progress
  66.                 console.log(debugUrl + ': getStateAndDispatch: Started fetching')
  67.                 const promise = fetch(url).then(response => response.json());
  68.                 state = {
  69.                     state: 'loading',
  70.                     data: promise,
  71.                 }
  72.                 promise.then(
  73.                     data => entry.dispatch({ type: 'success', payload: data }),
  74.                     error => entry.dispatch({ type: 'error', payload: error }),
  75.                 );
  76.             }
  77.             return state;
  78.         },
  79.     };
  80.     return entry;
  81. }
  82.  
  83. function useFetch(url) {
  84.     var context = useContext(FetchContext);
  85.     var entry = context.cache[url] ??= createSlice(url);
  86.     var state = useSyncExternalStore(entry.subscribe, entry.getStateAndDispatch);
  87.     switch (state.state) {
  88.         case 'loading':
  89.             throw state.data;
  90.         case 'success':
  91.             return state.data;
  92.         case 'error':
  93.             throw state.data;
  94.         default:
  95.             return assertNever(state);
  96.     }
  97. }
  98.  
  99. function Expander({ children, summary }) {
  100.     const [opened, onClick] = useReducer((state) => !state, false);
  101.     return <>
  102.         <p>
  103.             <button onClick={onClick}>{opened ? 'Close ' : 'Open '}{summary}</button>
  104.         </p>
  105.         {opened && <Suspense fallback={<p>Loading...</p>}>
  106.             {children}
  107.         </Suspense>}
  108.     </>
  109. }
  110.  
  111. function FetchSingle ({ url, Component }) {
  112.     const json = useFetch(url);
  113.     return <fieldset>
  114.         <legend><code>{url}</code></legend>
  115.         <Component data={json} />
  116.     </fieldset>;
  117. }
  118.  
  119. function FetchList ({ url, Component }) {
  120.     const json = useFetch(url);
  121.     return <fieldset>
  122.         <legend><code>{url}</code></legend>
  123.         {json.map(item => <fieldset key={item.id}><Component data={item} /></fieldset>)}
  124.     </fieldset>;
  125. }
  126.  
  127. function User ({ data }) {
  128.     return <>
  129.         <p>User: {data.name}</p>
  130.         <p>Email: {data.email}</p>
  131.         <p>Phone: {data.phone}</p>
  132.         <Expander summary="Posts">
  133.             <FetchList url={`https://jsonplaceholder.typicode.com/users/${data.id}/posts`} Component={Post} />
  134.         </Expander>
  135.     </>;
  136. }
  137. function Post ({ data }) {
  138.     return <>
  139.         <p>Post: {data.title}</p>
  140.         <p>Body: {data.body}</p>
  141.         <Expander summary={`User ${data.userId}`}>
  142.             <FetchSingle url={`https://jsonplaceholder.typicode.com/users/${data.userId}`} Component={User} />
  143.         </Expander>
  144.         <Expander summary={`Comments`}>
  145.             <FetchList url={`https://jsonplaceholder.typicode.com/posts/${data.id}/comments`} Component={Comment} />
  146.         </Expander>
  147.         <Expander summary={`Todos`}>
  148.             <FetchList url={`https://jsonplaceholder.typicode.com/posts/${data.id}/todos`} Component={Todo} />
  149.         </Expander>
  150.         <Expander summary={`Albums`}>
  151.             <FetchList url={`https://jsonplaceholder.typicode.com/posts/${data.id}/albums`} Component={Album} />
  152.         </Expander>
  153.     </>;
  154. }
  155. function Comment ({ data }) {
  156.     return <>
  157.         <p>Comment: {data.name}</p>
  158.         <p>Email: {data.email}</p>
  159.         <p>Body: {data.body}</p>
  160.         <Expander summary={`Post ${data.postId}`}>
  161.             <FetchSingle url={`https://jsonplaceholder.typicode.com/posts/${data.postId}`} Component={Post} />
  162.         </Expander>
  163.     </>;
  164. }
  165. function Todo ({ data }) {
  166.     return <>
  167.         <p>Todo: {data.title}</p>
  168.         <p>Completed: {data.completed ? 'Yes' : 'No'}</p>
  169.         <Expander summary={`User ${data.userId}`}>
  170.             <FetchSingle url={`https://jsonplaceholder.typicode.com/users/${data.userId}`} Component={User} />
  171.         </Expander>
  172.     </>;
  173. }
  174. function Album ({ data }) {
  175.     return <>
  176.         <p>Album: {data.title}</p>
  177.         <Expander summary={`User ${data.userId}`}>
  178.             <FetchSingle url={`https://jsonplaceholder.typicode.com/users/${data.userId}`} Component={User} />
  179.         </Expander>
  180.         <Expander summary={`Photos`}>
  181.             <FetchList url={`https://jsonplaceholder.typicode.com/albums/${data.id}/photos`} Component={Photo} />
  182.         </Expander>
  183.     </>;
  184. }
  185. function Photo ({ data }) {
  186.     return <>
  187.         <p>Photo: {data.title}</p>
  188.         <p>URL: <a href={data.url}>{data.url}</a></p>
  189.     </>;
  190. }
  191.  
  192. export function App () {
  193.     return (
  194.         <Suspense fallback={<p>Loading...</p>}>
  195.             <FetchSingle url="https://jsonplaceholder.typicode.com/users/1" Component={User} />
  196.         </Suspense>
  197.     );
  198. }
  199.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement