UseRef in Suspense data fetching - reactjs

I'm trying to use the experimental new React feature Suspense for data fetching.
Here's my simple useApi hook which (if I understand Suspense correctly) either returns the result of an fetch call or throws the suspender promise. (slightly modified the documented example)
function useApi(path) {
const ref = React.useRef({ time: +new Date() });
if (!ref.current.suspender) {
ref.current.suspender = fetch(path).then(
data => ref.current.data = data,
error => ref.current.error = error,
);
}
if (ref.current.data) return ref.current.data;
if (ref.current.error) return ref.current.error;
throw ref.current.suspender;
}
I'm using this hook simply like this:
function Child({ path }) {
const data = useApi(path);
return "ok";
}
export default function App() {
return (
<Suspense fallback="Loading…">
<Child path="/some-path" />
</Suspense>
);
}
It never resolves.
I think the problem is that useRef isn't quite working as it's supposed to.
If I initialize the ref with a random value, it doesn't retain that value, and instead gets reinitialized with another random value:
const ref = React.useRef({ time: +new Date() });
console.log(ref.current.time)
1602067347386
1602067348447
1602067349822
1602067350895
...
There's something weird about throwing the suspender that causes the useRef to reinitialize on every call.
throw ref.current.suspender;
If I remove that line useRef works as intended, but obviously Suspense doesn't work.
Another way I can make it work is if I use some sort of custom caching outside of React, like:
const globalCache = {}
function useApi(path) {
const cached = globalCache[path] || (globalCache[path] = {});
if (!cached.suspender) {
cached.suspender = ...
}
if (cached.data) ...;
if (cached.error) ...;
throw cached.suspender;
}
This also makes it work, but I would rather use something that React itself provides in terms of caching component-specific data.
Am I missing something on how useRef is supposed to, or not supposed to work with Suspense?
Repro: https://codesandbox.io/s/falling-paper-shps2

Let's review some facts on React.Suspense:
The children elements of React.Suspense won't mount until the thrown promise resolved.
You must throw the promise from function body (not from a callback like useEffect).
Now, you throwing a promise from your custom hook, but according to 1. the component never mounts, so when the promised resolves, you throwing the promise again - infinite loop.
According to 2., even if you try saving the promise in a state or ref etc. still it wont work - infinite loop.
Therefore, if you want to write some custom hook, you indeed need to use any data-structure (can be managed globally {like your globalCache} or by React.Suspense parent) which indicates if the promise from this specific React.Suspense has been thrown (thats exactly what Relay does in Facebook's codebase).

I've been struggling with the same problem, but I think it's actually possible to achieve what you want. I looked at the implementations of react-async and SWR and noticed that react-async actually doesn't throw on the first render, but it uses useEffect(...) to start the async operation, combined with a setState which triggers another render and then throws the promise on subsequent renders (until it resolves). I believe SWR actually behaves the same, with one minor difference; SWR uses useLayoutEffect (with fallback to useEffect for server side rendering), which has one major benefit: the initial render without data never happens.
It does mean that the parent component still has to cope with abundance of data. The first render can be used to start the promise, but still has to return without throwing to avoid the infinite loop. Only on second render will the promise be thrown which will actually suspend rendering.

Related

Using variables from react hooks to call another hook

I’ve seen a lot of discussion about conditionally calling hooks and why that is wrong, but is there any issue with using a variable returned by one hook to provide an action or data to another?
Example using graphql:
const form = useForm({ initialValues: { name: ‘’ }})
const { data } = useQuery(query, { onCompleted: (data) => form.setValues(data) })
Or another example using react router dom:
const { name } = useParams();
const { data } = useQuery(query, { variables: { name } })
The main reason for choosing to do this is to get rid of a useEffect if I don’t have to use one. The alternative would be to delay the call of the other hook until the query has finished by rendering it in a separate component which again, isn’t always ideal.
Are there any reasons why this is a bad idea or not best practice?
I generally prefer to keep my queries as the first hook called, but otherwise I can’t see why this would be incorrect.
There is nothing wrong with using the output of one hook as the input of another in a general sense.
For certain hooks, this can lead to potentially unintentional (but not invalid) behavior. For example, if you passed the result of a query hook as the default value of a useState, the state value would not be set to the query result value when it eventually loads. (for that you'd have to useEffect + setState). This is an example of potential developer error though, not a React issue.
As far as React is concerned, passing the output of one hook as an input to another is perfectly fine and valid.

useEffect() in my search Bar component causes an infinite loop, that I cannot understand

I'm new to React-Native and am following along in a course on react-native.
This git hub links to a my repository the code has the problem(infinite loop) I describe in the following question.
I have spent 12+ hours trying to figure this out. Please help me figure this out if you can.
https://github.com/JohnyClash/mealsToGo
useEffect in question
above photo directory: 'src/features/restaurants/components/search.components.js'
useEffect(() => {
search(searchKeyword);
}, []);
The above code creates a feedback loop that causes the app to continuously fetch from a mock api, that returns the location information, loads to the screen and then quickly reloads ad infinitum.Its intended purpose is to run a single time on component mount to cause a single default search. Instead, This useEffect() inside of search.component runs its callback repeatedly. The useEffect is not tracking a dependency that has changed, and is given [] an empty array in place of dependency
useEffect(callback,dependencies)
useEffect(callback,[])
Shouldn't this syntax of useEffect only run once after its mount, and not run again if something is updated? If my understanding is correct how is it possible that this use effect is running in an infinite loop?
this useEffect() is the culprit as the infinite reload loop stops when it is this useEffect() is removed.
all other functionality down this logic chain does not create an infinite loop, as in the search method initiated through onSubmitEditing works well without looping.
The problem of this infinite loop is being caused by this location object here, probably because every time the LocationContext is rerendered (a search is done or state is updated), it creates a new instance of the location object, which makes the useEffect be called again, then it search again, recreates the location object when calling useEffect again, which makes a new search, update some state and recreates the location object...
Code with problem of infinite loop:
useEffect(() => {
console.log(location)
if (location) {
const locationString = `${location.lat},${location.lng}`;
retrieveRestaurants(locationString);
}
}, [location]);
If you do something like this, might solve this problem:
useEffect(() => {
if (location?.lat && location?.lng) {
const locationString = `${location.lat},${location.lng}`;
retrieveRestaurants(locationString);
}
}, [location?.lat, location?.lng]);
Also be careful with setTimeout, and not clearing it on component unmount
Tip: Always avoid object, array or function in useEffect dependency, and if necessary it needs to be memorized with useMemo or useCallback
Try This :
useEffect(() => {
search(keyword)
}, [keyword])

How can I make a function wait for another function?

I have this function:
const submitImport = async (value) => {
const { actions, navigation, account } = this.props;
this.setState({ isImporting: true });
actions.importWallet(value.mnemonicPhrase, value.password, 1);
console.log('acc', account);
actions.showNotification({
message: 'Account has been successfully Imported',
isError: false,
});
};
importWallet is adding new properties to account object but when I call this function the first time the account object is empty but when I click it the second time it is okay. So my guess is importWallet needs time to finish and return the value. I tried to use async await but it did not work. Also, I tried to use return new Promise but it still did not work. Maybe I did it the wrong way idk.
Any suggestions on how to solve this issue please?
I am assuming the importWallet function induces some change in the account prop.
All prop changes require atleast one render cycle for the updated values to get visible as state/prop changes are asynchronous in react. In your case you are trying to access account as soon invoking actions.importWallet. Hence as it is within the same render, it has not yet been updated. But as you mentioned, it will be available from the subsequent renders.
Also you cannot make it synchronous with async-await / promises as the asynchronous nature of react state updates is not exposed.
Your usecase may be achieved by some refractor :
Try obtaining the new value of account in the return statement of account.importWallet. That way you can use it even before the prop updates.
const updatedAccount = actions.importWallet(value.mnemonicPhrase, value.password, 1);
In case you are using React Hooks, you can create an effect with useEffect and add account as a dependency. This will invoke the effect when the dependency value changes (dependency should be a primitive data type for consistent effect invocation).

InfiniteLoop in useEffect when one of the dependency is a function from useContext

2021 UPDATE
Use a library that makes requests and cache them - react-query, swr, redux-toolkit-query
ORIGINAL QUESTION
I've been struggling with this for quite a long time and didn't find an answer.
I have a component that is the last step of some registration process during which I ask a user to enter its data through several forms. In this component, I send collected data to API. If the request is successful I show ok, if not I show error.
I have useEffect that sends the data. The function that performs this task lives in a context
const { sendDataToServer } = useContext(context)
useEffect(() => {
const sendData = async () => {
setLoading(true)
await sendDataToServer(...data)
setLoading(false)
}
sendData()
}, [sendDataToServer, data])
If I include sendDataToServer in the dependencies list this useEffect would go into an infinite loop, causing endless rerendering. I suppose this is because a reference to the function has a different value on every render.
I can of course redesign the app and do not keep the function in the context, but I do like it and don't consider it a bad practice (correct me if I am wrong)
So what are my options here? How do I keep the flow with the context API, but use useEffect with the correct list of dependencies?
You're right with your guess, that's why we got useCallback for referential equality.
You didn't post the sendDataToServer function, but it should look something like this with useCallback:
const sendDataToServer = useCallback(data => {
// your implementation
}, [your, dependencies])
After that you can safely use it in your useEffect.
I highly recommend Kent C. Dodd's blog posts: When to useMemo and useCallback
Smartassing now: If it's only purpose is sending data to the server (and not changing the app's state), I don't know why it should be part of the context. It could be a custom hook or even a static function.
Btw: There could be another problem: If the data dependency in your useEffect is changed when executing sendDataToServer, you will still have an endless loop (e. g. when you fetch the new data after executing sendDataToServer), but we can't see the rest of the code.

useState React hook always returning initial value

locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});

Resources