Ideal method to make API calls in useEffect in strict mode - javascript

In React 18, there is mount -> unmount -> mount in strict mode for various reasons. I am trying to place an API call in the useEffect hook and I am trying to figure out what is the best way to write a cleanup such that the API call is not triggered again when it mounts again.
function componentName(){
useEffect(() => {
fetchCall(); // Want to call this once
},[]);
return(<div></div>)
}
Using a component level state to check if a call was made does not work, because the state does not update between in this process.
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function fetchData(){
setIsLoading(false);
const apiResponse = await getAPI(); //gets called twice
}
if(isLoading){
fetchData();
}
},[])
I don't want to use some AbortController to abort the previous call because that is just ugly. I cannot figure out just how to tell in the mounting process that this call has been made. At first, I thought maybe there was some race condition between the state update and API request but the state is set before we are waiting for the API response.
Basically, What is the ideal way to ensure the call is triggered just once? I found solutions like using a cache in between or using react-query or some just saying to remove the strict mode but there has to be a clean way to do this.

My 2 cents:
Create a custom hook:
function useEffectively(fnToRun, deps = []) {
const refRanOnce = useRef(false);
const refDeps = useRef(deps);
useEffect(() => {
let cleanup;
const depsChanged = !isEqual(refDeps.current, deps); // isEqual can be a custom code or import it from lodash
if (!refRanOnce.current || depsChanged) {
refRanOnce.current = true;
refDeps.current = deps;
cleanup = fnToRun();
}
return () => {
// you may reset refRanOnce.current to false here, if required
cleanup?.(); // clean up for the function that you may pass, if it exists
}
}, deps);
}
This custom hook will trigger when dependencies actually change in between renders or at the time of initial mount.
Now use this custom hook for api calls:
useEffectively(fetchCall) // add dependencies if required
It may NOT cover all your use-cases, but you may modify it as per your need.

Related

React Hook useEffect has a missing dependency?

I have the below code and as far as I'm concerned it only relies upon [page] yet, I am getting the error
React Hook useEffect has a missing dependency
I've seen similar questions about commenting out a line in your eslint file but
I don't have an eslint file
I would rather understand and resolve the issue
const fetchStarWarsInfo = async () => {
const response = await getData(
`https://swapi.dev/api/people/?page=${dontReturnZero(page)}`
);
dispatch(setCurrentCharacters(response.results));
};
useEffect(() => {
fetchStarWarsInfo();
}, [page]);
Actually it might also ask you to put dispatch in dependencies and other stuff, so it's not so critical not to list all of the dependencies that he ask for.
Try to remove dispatch from your fetchStarWarsInfo and check. If error doesn't disappear, so try to remove also getData function.
You might also want to configure eslint for React, so those errors would have more information about error
If fetchStarWarsInfo is declared within the functional component, everytime state changes, the function is re-constructed and re-declared, therefore, it changes. Therefore the error message is notifying you that although the function is changing, it's not declared as a dependency. The problem though, is if you declare a function as a dependency, you can potentially create an infinite loop, since if you change state within the fetchStarWarsInfo, the useEffect hook will have to continuously keep running, as a new instance of fetchStarWarsInfo will be created.
The solutions to this are pretty straightforward. Declare the fetchStarWarsInfo within the useEffect hook or split up the logic within the function, so you can declare it outside the functional component completely. Then handle state changes within the useEffect hook. That way the function is declared only once, and state changes are handled within the hook, thereby causing no unwanted side effects
After doing this, you need to verify that all the functions used within fetchStarWarsInfo are not continuously changing either. (i.e dontReturnZero,setCurrCharacters, etc). You can check this by commenting them out and seeing if the error disappears. If it does disappear, you'll need to include them in your dependency array for useEffect.
Example 1:
useEffect(() => {
const fetchStarWarsInfo = async () => {
const response = await getData(
`https://swapi.dev/api/people/?page=${dontReturnZero(page)}`
);
dispatch(setCurrentCharacters(response.results));
};
fetchStarWarsInfo();
}, [page, dispatch]);
Example 2:
const fetchStarWarsInfo = async (page) => {
const response = await getData(
`https://swapi.dev/api/people/?page=${dontReturnZero(page)}`
);
return response
};
const FunctionalComponent=() => {
const dispatch = useDispatch()
useEffect(() => {
fetchStarWarsInfo(page).then((data)=> dispatch(setCurrentCharacters(data.results));
}, [page, dispatch]);
}

How to avoid double network requesting per one fetch in React 18? [duplicate]

hi I was studying react and I've got something curious when I made this example.
this code is that sending get request to newsapi, and receive the result. and I used custom hook for it.
// *** this is where I use custom hook. "usePromise" is custom hook. *** //
function NewsList({ category }) {
const [loading, response, error] = usePromise(() => {
console.log("how many will it run?")
const query = category === 'all' ? '' : `&category=${category}`;
return axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=044c3c345f28417380cd3ea946ac8641`
);
},[category]);
console.log(loading);
...
I wrote console.log("how many will it run?") to check how many this function will run.
and also console.log(loading) to check how many times loading will be changed
// *** this is custom hook's code *** //
export default function usePromise(promiseCreator, deps) {
const [loading, setLoading] = useState(true);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const process = async () => {
await promiseCreator()
.then((res) => {
setResponse(res);
})
.catch((e) => {
console.log('catch');
setError(e);
});
setLoading(false);
};
process();
}, deps);
return [loading, response, error];
}
I wrote console.log('catch') to check how many this promise function will start.
(Btw, cause of sending too many request, this code appears "error" when I send request)
// *** this is the console when I run this project on browser *** //
NewsList.jsx:31 true
NewsList.jsx:24 how many will it run?
NewsList.jsx:24 how many will it run?
usePromise.js:15 catch
NewsList.jsx:31 false
usePromise.js:15 catch
NewsList.jsx:31 false
as you can see. usePromise is excecuted twice. I knew loading will be consoled twice, "true" and "false" cause the function in usePromise is async.
but I don't know why usePromise is executed twice. I expected it will be excecuted once when it is declared.
if you know the answer please comment for me. it would help me a lot
thank you
solved: JakubKotrs said on my post. "StrictMode" has been designed to run effects twice. and when I removed "StrictMode" on my index.js. it runs only once.
Are you using a <StrictMode>? If yes, then Strict mode has been designed to run effects twice.
Strict mode is for detecting bugs you may have inside your effects but also for detecting other issues with your code that would prevent you to easily upgrade to the next version of React. Your effects should be resilient to it, calling them twice should not cause bugs in your code, even it means your API is called twice. In production, it doesn't do anything, it's a development only tool.
In this case, it's trying to point out you have an issue in your effect that it doesn't have a cleanup phase for example. If the effect has been cleaned up because the dependencies changed or because the component unmounted, you would have an issue (like calling setState on an unmounted component).
Example of cleanup:
useEffect(() => {
// use local flag
let cleanup = false
const process = () => {
Promise.resolve(true).then(() => {
// check if cleanup has been run
if (!cleanup) {
setState(true)
 }
})
}
process()
// set the flag
return () => {
cleanup = true
}
}, [])

How can I update a state variable from a promise?

I am trying to determine if a customer has an active subscription or not. To do this I am utilizing the following code:
const stripe = require('stripe')('some-api-key');
export default function Example(){
// the user will automatically be considered non-subbed by default
const [isSubscriber, setIsSubscriber] = useState(false)
// grab the customer id from stripe
async function get_customer_id() {
const customers = await stripe.customers.search({
query: `metadata[\'some-meta-data-key\']:\'some-meta-data-value\'`
});
return customers.data[0]['id']
}
// grab the list of active subscriptions from stripe
async function customer_is_subscriber(){
const subs = await stripe.subscriptions.list({
status: 'active',
});
return subs
}
// determine if the customer id is in the list of active subscriptions.
// return true if so, false otherwise
async function test_equality(){
const customer_id = await get_customer_id();
const subbed = await customer_is_subscriber();
const answer = subbed.find(sub => sub.customer === customer_id)
return !!answer;
}
useEffect( () => {
async function load_result() {
const promise_function_return = await test_equality()
setIsSubscriber(promise_function_return)
}
load_result();
}, [isSubscriber]);
return (
// some react code
)
}
I have been able to successfully get all of my other functions where I am doing the comparisons for if a user is a subscriber but where I am having an issue is updating the state value (e.g. true if they are subbed, false otherwise).
I found some good past questions on this specific topic such as:
here The useState set method is not reflecting a change immediately
here: setState inside Promise in React
and here: setState inside a Promise function in a useEffect with hooks?
but I just have not been able to get it working correctly. This is currently the closest I have been able to get to solving this problem.
Currently your code says that, when isSubscriber changes, it should check if the user is a subscriber and update the isSubscriber state... so it's a chicken and egg problem. It won't set isSubscriber until isSubscriber gets set.
I think you want to change }, [isSubscriber]); to }, []); so that that code executes when the component first loads (not when isSubscriber changes).
The useEffect hook will always run on mount regardless of if there is anything in its dependency array. This means that your useEffect will work as is, and will run onMount as well as when isSubscriber changes:
useEffect( () => {
async function load_result() {
const promise_function_return = await test_equality()
setIsSubscriber(promise_function_return)
}
load_result();
}, [isSubscriber]);
To verify this, you can check out this codesandbox example. The useEffect looks just like yours, and you will notice that isSubscriber is initially set to false, but is updated to true after 3 seconds.
There's still an adjustment you may want to make even though that part appears to work ok. With isSubscriber in the dependency array, the function in your useEffect will be called any time isSubscriber changes. This probably not what you want, since this function doesn't actually depend on isSubscriber, but actually sets isSubscriber. In this case, that means test_equality() will be run on initial mount and then one more time after it sets isSubscriber, which is unnecessary.
This blog post explains the useEffect dependency array really well.
You can fix this by removing isSubscriber from the dependency array, like this:
useEffect(() => {
console.log("in useEffect");
async function load_result() {
const promise_function_return = await test_equality();
setIsSubscriber(promise_function_return);
}
load_result();
}, [isSubscriber]);
Since you mentioned the state value is not getting updated, there must be another issue going on in either get_customer_id() or customer_is_subscriber(). It would be good to double check and make sure the stripe api calls are working as expected.

how to set an useEffect to fetch data from API at first render with eslint-react-hooks?

I need to call two functions to fetch data only on the first render. I'm using react-hooks on this project. So the code would look something like this:
const Component = (props) => {
// init
useEffect(() => {
const fetchA = async () => {
await props.fetchA()
};
const fetchB = async () => {
await props.fetchB()
};
fetchA();
fetchB();
}, []);
}
fetchA and fetchB are actions performed by redux to make requests and save data on the reducers.
Then I added eslint-react-hooks to the project. Now eslint warns me that
Blockquote
React Hook useEffect has a missing dependency: 'props'. Either include it or remove the dependency array. However, 'props' will change when any prop changes, so the preferred fix is to destructure the 'props' object outside of the useEffect call and refer to those specific props inside useEffect.
Is the only way to do this by applying // eslint-disable-next-line react-hooks/exhaustive-deps onto the line before the useEffect dependencies?
It's pointing out that if props.fetchA and props.fetchB change, your code is not set up to update with it. If you're absolutely certain you want to ignore changes to props.fetchA and props.fetchB, then you can add an eslint-disable.
If you want to make your code perform updates when props.fetchA or props.fetchB change, then follow the instructions in the lint warning and do something like this:
const { fetchA, fetchB } = props;
useEffect(() => {
// i renamed these so as not to conflict with the outer variables. Feel free to choose different names.
const a = async () => {/* fetchA() */};
const b = async () => {/* fetchB() */};
a();
b();
}, [fetchA, fetchB]);
Depending on what fetchA and fetchB are doing, it's possible you'll need some cleanup logic to undo what was done the first time, but i can't tell you precisely what since i don't know what fetchA and fetchB do.

How can we implement componentWillUnmount using react hooks?

The method componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. If we use useEffect with an empty array ([]) as the second argument and put our function in return statement it will be executed after the component is unmounted and even after another component will be mounted. This is done for performance reasons as far as I understand. In order not to delay rendering.
So the question is - how can we call some function using hooks before a component gets unmounted?
What I am trying to do is an application which saves user's input as he types (without submitting form). I use setInterval to save updated text every N seconds. And I need to force save updates before the component will unmount. I don't want to use prompt by react router before navigating. This is an electron application. I appreciate any thoughts or advice on how to implement such functionality.
Update
Unfortunately, Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?. It basically means that cleanup is run after a component is unmounted and it is not the same as executing code in componentWillUnmount(). I can clearly see the sequence of calls if I put console.log statements in the cleanup code and in another component. The question is whether we can execute some code before a component is unmounted using hooks.
Update2
As I can see I should better describe my use case. Let's imagine a theoretical app which holds its data in a Redux store. And we have two components with some forms. For simplicity, we don't have any backend or any async logic. We use only Redux store as data storage.
We don't want to update Redux store on every keystroke. So we keep actual values in the local component's state which we initialize with values from the store when a component mounts. We also create an effect which sets up a setInterval for 1s.
We have the following process. A User types something. Updates are stored in the local component state until our setInterval callback is called. The callback just puts data in the store (dispatches action). We put our callback in the useEffect return statement to force save to store when the component gets unmounted because we want to save data to store in this case as soon as possible.
The problem comes when a user types something in the first component and immediately goes to the second component (faster than 1s). Since the cleanup in our first component will be called after re-rendering, our store won't be updated before the second component gets mounted. And because of that, the second component will get outdated values to its local state.
If we put our callback in componentWillUnmount() it will be called before unmounting and the store will be updated before the next component mounts. So can we implement this using hooks?
componentWillUnmount can be simulated by returning a function inside the useEffect hook. The returned function will be called just before every rerendering of the component. Strictly speaking, this is the same thing but you should be able to simulate any behaviour you want using this.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
})
Update
The above will run every time there is a rerender. However, to simulate the behaviour only on mounting and unmounting (i.e. componentDidMount and componentWillUnmount). useEffect takes a second argument which needs to be an empty array.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
}, [])
See a more detailed explanation of the same question here.
Since the introduction of the useLayoutEffect hook, you can now do
useLayoutEffect(() => () => {
// Your code here.
}, [])
to simulate componentWillUnmount. This runs during unmount, but before the element has actually left the page.
The question here is how do you run code with hooks BEFORE unmount? The return function with hooks runs AFTER unmount and whilst that doesn’t make a difference for most use cases, their are some where it is a critical difference.
Having done a bit of investigation on this, I have come to the conclusion that currently hooks simply does not provide a direct alternative to componentWillUnmount. So if you have a use case that needs it, which is mainly for me at least, the integration of non-React libs, you just have to do it the old way and use a component.
Update: see the answer below about UseLayoutEffect() which looks like it may solve this issue.
I agree with Frank, but the code needs to look like this otherwise it will run only on the first render:
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
This is equivalent to ComponentWillUnmount
Similar to #pritam's answer, but with an abstracted code example. The whole idea of useRef is to allow you to keep track of the changes to the callback and not have a stale closure at the time of execution. Hence, the useEffect at the bottom can have an empty dependency array to ensure it only runs when the component unmounts. See the code demo.
Reusable hook:
type Noop = () => void;
const useComponentWillUnmount = (callback: Noop) => {
const mem = useRef<Noop>();
useEffect(() => {
mem.current = callback;
}, [callback]);
useEffect(() => {
return () => {
const func = mem.current as Noop;
func();
};
}, []);
};
After a bit of research, found that - you could still accomplish this. Bit tricky but should work.
You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
DEMO
However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
I got in a unique situation where the useEffect(() => () => { ... }, []); answers did not work for me. This is because my component never got rendered — I was throwing an exception before I could register the useEffect hook.
function Component() {
useEffect(() => () => { console.log("Cleanup!"); }, []);
if (promise) throw promise;
if (error) throw error;
return <h1>Got value: {value}</h1>;
}
In the above example, by throwing a Promise<T> that tells react to suspend until the promise is resolved. However, once the promise is resolved, an error is thrown. Since the component never gets rendered and goes straight to an ErrorBoundary, the useEffect() hook is never registered!
If you're in a similar situation as myself, this little code may help:
To solve this, I modified my ErrorBoundary code to run a list of teardowns once it was recovered
export default class ErrorBoundary extends Component {
// ...
recover() {
runTeardowns();
// ...
}
// ...
}
Then, I created a useTeardown hook which would add teardowns that needed to be ran, or make use of useEffect if possible. You'll most likely need to modify it if you have nesting of error boundaries, but for my simple usecase, it worked wonderfully.
import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";
const teardowns: (() => void)[] = [];
export function runTeardowns() {
const wiped = teardowns.splice(0, teardowns.length);
for (const teardown of wiped) {
teardown();
}
}
type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);
/**
* Guarantees a function to run on teardown, even when errors occur.
*
* This is necessary because `useEffect` only runs when the component doesn't throw an error.
* If the component throws an error before anything renders, then `useEffect` won't register a
* cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
*
* This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
* However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
*/
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
// We have state we need to maintain about our teardown that we need to persist
// to other layers of the application. To do that, we store state on the callback
// itself - but to do that, we need to guarantee that the callback is stable. We
// achieve this by memoizing the teardown function.
const teardown = useMemo(onTeardown, deps);
// Here, we register a `useEffect` hook to run. This will be the "happy path" for
// our teardown function, as if the component renders, we can let React guarantee
// us for the cleanup function to be ran.
useEffect(() => {
// If the effect gets called, that means we can rely on React to run our cleanup
// handler.
teardown.registered = true;
return () => {
if (isDebugMode) {
// We want to ensure that this impossible state is never reached. When the
// `runTeardowns` function is called, it should only be ran for teardowns
// that have not been able to be hook into `useEffect`.
if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
}
teardown();
if (isDebugMode) {
// Because `teardown.registered` will already cover the case where the effect
// handler is in charge of running the teardown, this isn't necessary. However,
// this helps us prevent impossible states.
teardown.called = true;
}
};
}, deps);
// Here, we register the "sad path". If there is an exception immediately thrown,
// then the `useEffect` cleanup handler will never be ran.
//
// We rely on the behavior that our custom `ErrorBoundary` component will always
// be rendered in the event of errors. Thus, we expect that component to call
// `runTeardowns` whenever it deems it appropriate to run our teardowns.
// Because `useTeardown` will get called multiple times, we want to ensure we only
// register the teardown once.
if (!teardown.pushed) {
teardown.pushed = true;
teardowns.push(() => {
const useEffectWillCleanUpTeardown = teardown.registered;
if (!useEffectWillCleanUpTeardown) {
if (isDebugMode) {
// If the useEffect handler was already called, there should be no way to
// re-run this teardown. The only way this impossible state can be reached
// is if a teardown is called multiple times, which should not happen during
// normal execution.
const teardownAlreadyCalled = teardown.called;
if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
}
teardown();
if (isDebugMode) {
// Notify that this teardown has been called - useful for ensuring that we
// cannot reach any impossible states.
teardown.called = true;
}
}
});
}
}
It does not matter wether the returned function from useEffect gets called before or after the component unmounted: You still have access to the states valuey through the closure:
const [input, setInput] = useState(() => Store.retrieveInput());
useEffect(() => {
return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
}, []);
If you don't manage the input in the components state, your whole structure is broken and should be changed to manage state at the right place. In your case, you should lift the shared input state of the components to the parent.
ReactJS docs on hooks specify this:
Effects may also optionally specify how to “clean up” after them by
returning a function.
So any function you return in your useEffect hook, will be executed when the component unmounts, as well as before re-running the effect due to a subsequent render.

Categories

Resources