SHARE
    TWEET
    ferrybig

    React useFetch with cache

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

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