How to cache react component fetches? - reactjs

Let's say you have a component like this:
function MyComponent({ index }) {
const [data, setData] = useState('');
useEffect(() => {
(async function() {
const result = await fetchData(index);
setData(result);
})();
}, [index]);
return <h1>{data}</h1>;
}
Whenever the index changes, we will re-fetch the data and render it. How can I cache this so that we don't have to re-fetch the same index from before?

You want to memoize your results, like:
const cache = {};
async function memoFetch(index) {
if (cache[index]) {
return cache[index];
}
const data = fetchData(index);
cache[index] = data;
return data;
}
function MyComponent({ index }) {
const [data, setData] = useState('');
useEffect(() => {
(async function() {
const result = await memoFetch(index);
setData(result);
})();
}, [index]);
return <h1>{data}</h1>;
}

create a component cache Context:
const MyComponentCacheContext = React.createContext() ;
2.Create a component Cache Provider and give cache an intial Value of { } (empty Object) :
function MyComponentCacheProvider (props){
//we create this in step 3
const [cache,dispatch]= React.useReducer(MyComponentCacheReducer,{})
const value =[cache,dispatch]
return <MyComponentCacheContext value={value} {...props}/>
}
3.we will make the cache look like this => {{index:data},{index:data},...}so create a component cache Reducer that return the desired shape
:
function MyComponentCacheReducer (state,action) {
switch(action.type){
case 'ADD-TO-CACHE':
return {...state,[action.index]:action.data}
default :{
throw new Error (`unhandled action type ${action.type}`)
}
}
}
4.let's make all the code above within a custome hook so we can make it reusable:
function useMyComponentCache(){
const MyContext = React.useContext(MyComponentCacheContext)
if(!MyContext){
throw new Error (`useMyComponentCache must be within a MyComponentCacheProvider`)
}
return MyContext
}
5.let's customize your Component function so we can use the code above :
function MyComponent({ index }) {
const [data, setData] = useState('');
const [cache ,dispatch]= useMyComponentCache()
useEffect(() => {
if(cache[index]){
return setData(cache[index])
}else{
//here we fetch data --then--> we store it into the cache immedialty
const dataFetcher=fetchData(index).then(
data =>{
dispatch({type:'ADD-TO-CACHE',index,data})
return data
}
)
// updating state
const PromiseHandler =React.useCallback(
promise=>{
promise.then(
data => {
setData(data)
},
)
}
,[setData])
// Execution-Time
PromiseHandler(dataFetcher)
}
}, [cache,dispatch,index,PromiseHandler,setData]); // i assume that fetchData is
//a stable internal module so i didn t put it to the dependecies list
return <h1>{data}</h1>;
}
6.rendering step :put your component within CacheProvider !!!
function App(){
return ( <MyComponentCacheProvider>
<MyComponent index={0} />
</MyComponentCacheProvider>
)
}

Related

How do I initialise state values and methods that uses useSyncExternalStore + Context in React?

Description
I'm creating a state management tool for a small project, using mainly useSyncExternalStore from React, inspired by this video from Jack Herrington https://www.youtube.com/watch?v=ZKlXqrcBx88&ab_channel=JackHerrington.
But, I'm running into a pattern that doesn't look right, which is having to use 2 providers, one to create the state, and the other to initialise it.
The gist of the problem:
I have a property sessionId coming from an HTTP request. Saving it in my store wasn't an issue.
However, once I have a sessionId then all of my POST requests done with notifyBackend should have this sessionId in the request body. And I was able to achieve this requirement using the pattern above, but I don't like it.
Any idea how to make it better ?
Code
CreateStore.jsx (Not important, just providing the code in case)
export default function createStore(initialState) {
function useStoreData(): {
const store = useRef(initialState);
const subscribers = useRef(new Set());
return {
get: useCallback(() => store.current, []),
set: useCallback((value) => {
store.current = { ...store.current, ...value };
subscribers.current.forEach((callback) => callback());
}, []),
subscribe: useCallback((callback) => {
subscribers.current.add(callback);
return () => subscribers.current.delete(callback);
}, []),
};
}
const StoreContext = createContext(null);
function StoreProvider({ children }) {
return (
<StoreContext.Provider value={useStoreData()}>
{children}
</StoreContext.Provider>
);
}
function useStore(selector) {
const store = useContext(StoreContext);
const state = useSyncExternalStore(
store.subscribe,
() => selector(store.get()),
() => selector(initialState),
);
// [value, appendToStore]
return [state, store.set];
}
return {
StoreProvider,
useStore,
};
}
Creating the state
export const { StoreProvider, useStore } = createStore({
sessionId: "INITIAL",
notifyBackend: () => { },
});
index.jsx
<Router>
<StoreProvider>
<InitialisationProvider>
<App />
</InitialisationProvider>
</StoreProvider>
</Router
InitialisationContext.jsx
const InitialisationContext = createContext({});
export const InitializationProvider = ({ children }) {
const [sessionId, appendToStore] = useStore(store => store.session);
const notifyBackend = async({ data }) => {
const _data = {
...data,
sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
appendToStore({ sessionId: result.sessionId });
} else if (result.otherProp) {
appendToStore({ otherProp: result.otherProp });
}
} catch (e) { }
};
useEffect(() => {
appendToStore({ notifyBackend });
}, [sessionId]);
return (
<InitialisationContext.Provider value={{}}>
{children}
</InitialisationContext.Provider>
);
}
I just tried out Zustand, and it's very similar to what I'm trying to achieve.
Feels like I'm trying to reinvent the wheel.
With Zustand:
main-store.js
import create from 'zustand';
export const useMainStore = create((set, get) => ({
sessionId: 'INITIAL',
otherProp: '',
notifyBackend: async ({ data }) => {
const _data = {
...data,
sessionId: get().sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
set({ sessionId: result.sessionId });
} else if (result.otherProp) {
set({ otherProp: result.otherProp });
}
} catch (e) { }
},
}));
SomeComponent.jsx
export const SomeComponent() {
const sessionId = useMainStore(state => state.sessionId);
const notifyBackend = useMainStore(state => state.notifyBackend);
useEffect(() => {
if (sessionId === 'INITIAL') {
notifyBackend();
}
}, [sessionId]);
return <h1>Foo</h1>
};
This answer focuses on OPs approach to createStore(). After reading the question a few more times, I think there are bigger issues. I'll try to get to these and then extend the answer.
Your approach is too complicated.
First, the store is no hook! It lives completely outside of react. useSyncExternalStore and the two methods subscribe and getSnapshot are what integrates the store into react.
And as the store lives outside of react, you don't need a Context at all.
Just do const whatever = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
Here my version of minimal createStore() basically a global/shared useState()
export function createStore(initialValue) {
// subscription
const listeners = new Set();
const subscribe = (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
}
const dispatch = () => {
for (const callback of listeners) callback();
}
// value management
let value = typeof initialValue === "function" ?
initialValue() :
initialValue;
// this is what useStore() will return.
const getSnapshot = () => [value, setState];
// the same logic as in `setState(newValue)` or `setState(prev => newValue)`
const setState = (arg) => {
let prev = value;
value = typeof arg === "function" ? arg(prev) : arg;
if (value !== prev) dispatch(); // only notify listener on actual change.
}
// returning just a custom hook
return () => useSyncExternalStore(subscribe, getSnapshot);
}
And the usage
export const useMyCustomStore = createStore({});
// ...
const [value, setValue] = useMyCustomStore();

React custom hook causes infinite loops

any idea why this custom hook with SWR causes an infinite loop?
export const useOrganization = () => {
const [data, setData] = useState<OrganizationModel | undefined>();
const { organizationId } = useParams();
const { data: dataSWR } = useSWRImmutable<
AxiosResponse<Omit<OrganizationModel, 'id'>>
>(`organizations/${organizationId}`, api);
useEffect(() => {
if (dataSWR?.data && organizationId) {
setData({ id: organizationId, ...dataSWR.data });
console.log({ id: organizationId, ...dataSWR.data });
}
});
return data;
};
I need to fetch data from API and add missing id from URL param. If I use setData(dataSWR.data), everything is fine. The problem occurs when setData({...dataSWR.data}) is used -> loop.
You need to use useEffect based on the scenario. When dataSWR changed the useEffect call again with new data.
You can add the dataSWR as dependencies argument in useEffect hook.
useEffect(() => { do something... }, [dataSWR])
Example:
export const useOrganization = () => {
const [data, setData] = useState<OrganizationModel | undefined>();
const { organizationId } = useParams();
const { data: dataSWR } = useSWRImmutable<AxiosResponse<Omit<OrganizationModel, 'id'>>>(`organizations/${organizationId}`, API);
useEffect(() => {
if (dataSWR?.data && organizationId) {
setData({ id: organizationId, ...dataSWR.data });
console.log({ id: organizationId, ...dataSWR.data });
};
},[dataSWR]);
return data;
};
Usage of hook:
const data = useOrganization()
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}
I found the solution - useMemo hook:
export const useOrganization = () => {
const { organizationId } = useParams();
const { data } = useSWRImmutable<
AxiosResponse<Omit<OrganizationModel, 'id'>>
>(`organizations/${organizationId}`, api);
const result = useMemo(() => {
if (data && organizationId) {
return { id: organizationId, ...data.data };
}
}, [data, organizationId]);
console.log('useOrganization');
return result;
};

how to fetch data from webservice in REACT useEffect function and send it to props object?

I'm trying to fetch data from a webservice to a child component. It looks like Data are fetched ok in console.log() inside useEffect function. I'm not able to work with them in higher function scope. I'd like to access fetched data in the child component (PComponent) through the props object.
Thank you for any advice.
function App(props) {
let data = undefined;
useEffect( () => {
console.log('UseEffect');
const fetchData = async () => {
const result = await axios.get(someUrl)
data = result.data;
console.log('result',data);
return data;
};
data = fetchData();
}, []);
return (
<PComponent settings = {data}/>
);
}
export default App;
Try useState hook to store your data. Any state update will rerender the component and therefore data is passed to the child.
const App = (props) => {
const [data, setData] = React.useState()
const fetchData = async() => {
const result = await axios.get(someUrl)
setData(result);
};
// Fetch data when component mounted
useEffect( () => {
fetchData()
}, []);
return (
<PComponent settings={data}/>
);
}
export default App;
You have to use useState for state which can change in functional components. You can read about that on the React docs.
function App(props) {
const [data, setData] = useState(undefined);
useEffect(async () => {
console.log("useEffect running");
const fetchData = async () => {
const result = await axios.get(someUrl);
console.log(result.data);
return result.data;
};
setData(await fetchData());
}, []);
return <PComponent settings={data} />;
}

React - Error: Rendered more hooks than during the previous render with Custom Hook

I have a component in which I'm calling my custom hook.
The custom hook looks like this:
import { useQuery } from 'react-apollo';
export function useSubscription() {
const { loading, error, data } = useQuery(GET_SUBSCRIPTION_BY_ID)
if (loading) return false
if (error) return null
return data
}
And then the component I'm using it in that causes the error is:
export default function Form(props) {
const router = useRouter();
let theSub = useSubscription();
if (theSub === false) {
return (
<Spinner />
)
} // else I'll have the data after this point so can use it.
useEffect(() => {
if (!isDeleted && Object.keys(router.query).length !== 0 && router.query.constructor === Object) {
setNewForm(false);
const fetchData = async () => {
// Axios to fetch data
};
fetchData();
}
}, [router.query]);
// Form Base States
const [newForm, setNewForm] = useState(true);
const [activeToast, setActiveToast] = useState(false);
// Form Change Methods
const handleUrlChange = useCallback((value) => setUrl(value), []);
const handleSubmit = useCallback(async (_event) => {
// Submit Form Code
}, [url, selectedDuration, included, excluded]);
return (
<Frame>
My FORM
</Frame>
)
}
Any ideas?
You can use useEffect for calling hook at the first time of rendering component.
export default function Form(props) {
const router = useRouter();
const [theSub, setTheSub] = useState(null);
useEffect(() => { setTheSub(useSubscription()); }, []);
if (theSub === false) {
return (
<Spinner />
)
} // else I'll have the data after this point so can use it.
// I have some other states being set and used related to the form e.g:
// const [whole, setWhole] = useState(true);
return (... The form ...)

How to change the parameter of hook in react (fetch hook)

My fetch hook:
import { useEffect, useState } from "react";
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [fetchedData, setFetchedData] = useState(initialData);
useEffect(() => {
let unmounted = false;
const handleFetchResponse = response => {
if (unmounted) return initialData;
setHasError(!response.ok);
setIsLoading(false);
return response.ok && response.json ? response.json() : initialData;
};
const fetchData = () => {
setIsLoading(true);
return fetch(url, { credentials: 'include' })
.then(handleFetchResponse)
.catch(handleFetchResponse);
};
if (initialUrl && !unmounted)
fetchData().then(data => !unmounted && setFetchedData(data));
return () => {
unmounted = true;
};
}, [url]);
return { isLoading, hasError, setUrl, data: fetchedData };
};
I call this hook in a function like so:
//states
const [assignments, setAssignments] = useState([])
const [submissions, setSubmissions] = useState([])
const [bulk_edit, setBulk_edit] = useState(false)
const [ENDPOINT_URL, set_ENDPOINT_URL] = useState('https://jsonplaceholder.typicode.com/comments?postId=1')
let url = 'https://jsonplaceholder.typicode.com/comments?postId=1';
const { data, isLoading, hasError } = useOurApi(ENDPOINT_URL, []);
My question is how can I call this instance of userOurAPI with a different URL. I have tried calling it within a function where I need it but we can't call hooks within functions, so I am not sure how to pass it new url to get new data. I don't want to have many instances of userOurAPI because that is not DRY. Or is this not possible? New to hooks, so go easy on me!
In order to change the URL such that the component updates and fetches new data, you create a set function that changes the URL and you make sure that the useEffect() is run again on the change of URL. Return your setter function for URL so that you can use it outside of the first instance of your hook. In my code, you will see that I return a setUrl, I can use that to update fetch! Silly of me not to notice, but hopefully this will help someone.
You could do it the way you chose to, but there are other ways of working around such a problem.
One other way would be to always re-fetch whenever the URL changes, without an explicit setter returned from the hook. This would look something like this:
export const useOurApi = (url, initialData) => { // URL passed directly through removes the need for a specific internal url `useState`
// const [url, setUrl] = useState(initialUrl); // No longer used
// ...
useEffect(() => {
// Handle fetch
}, [url]);
return { isLoading, hasError, data: fetchedData }; // No more `setUrl`
};
This may not always be what you want though, sometimes you may not want to re-fetch all the data on every url change, for example if the URL is empty, you may not want to update the url. In that case you could just add a useEffect to the useOurApi custom hook to update the internal url and re-fetch:
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
// ...
useEffect(() => {
// Handle fetch
}, [url]);
useEffect(() => {
// ... do some permutation to the URL or validate it
setUrl(initialUrl);
}, [initialUrl]);
return { isLoading, hasError, data: fetchedData }; // No more `setUrl`
};
If you still sometimes want to re-fetch the data, unrelated to the URL, you could output some function from the hook to trigger the data fetching. Something like this:
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [fetchedData, setFetchedData] = useState(initialData);
const refetch = useCallback(() => {
// Your fetch logic
}, [url]);
useEffect(() => {
refetch(); // In case you still want to automatically refetch the data on url change
}, [url]);
return { isLoading, hasError, refetch, data: fetchedData };
};
Now you can call refetch whenever you want to trigger the re-fetching. You may still want to be able to internally change the url, but this gives you another a bit more flexible access to the fetching and when it occurs.
you confuse the difference between simple function and function component
Function Component are not just simple function. It means that the have to return a component or a html tag
I think you should turn four function to simple function like so
export const useOurApi = (initialUrl, initialData) => {
let url = initialUrl, fetchedData = initialData,
isLoading= true, hasError = false, unmounted = false;
const handleFetchResponse = response => {
if (unmounted) return initialData;
hasError = !response.ok;
isLoading = false;
return response.ok && response.json ? response.json() : initialData;
};
const fetchData = () => {
isLoading = true;
return fetch(url, { credentials: 'include' })
.then(handleFetchResponse)
.catch(handleFetchResponse);
};
if (initialUrl && !unmounted)
fetchData().then(data => {
if(!unmounted) fetchedData =data;
unmounted = true;
});
return { isLoading, hasError, url, data: fetchedData };
};

Resources