Conditionally calling an API using React-Query hook - reactjs

I am using react-query to make API calls, and in this problem case I want to only call the API if certain conditions are met.
I have an input box where users enter a search query. When the input value is changed, a search server is called with the contents of the input as the search query ... but only if the input value is more than 3 chars long.
In my react component I'm calling:
const {data, isLoading} = useQuery(['search', searchString], getSearchResults);
And my getSearchResults function will, conditionally, make an API call.
const getSearchResults = async (_, searchString) => {
if (searchString.length < 3)
return {data: []}
const {data} = await axios.get(`/search?q=${searchString}`)
return data;
}
We can't use a hook inside a conditional - so I put the condition into my API calling function.
This almost works. If I enter a short query string, there is no API request made and I get an empty array back for the data. Yay!
But - isLoading will flip to true briefly - even though there is no HTTP request being made. So my loading indicator shows when there is no actual network activity.
Am I misunderstanding how to best solve my use case, is there a way to enure that isLoading will return false if there is no HTTP activity?

The key was to use Dependent Queries
So, in my main component, I create a boolean and pass that to the enabled option of the useQuery hook:
const isLongEnough = searchString.length > 3;
const {data, isLoading} = useQuery(['search', searchString], getSearchResults, {enabled: isLongEnough});
and the API calling method is simply the API call - not any conditional:
const getSearchResults = async (_, searchString) => {
const {data} = await axios.get(`/search?q=${searchString}`);
return data;
}
The docs describe dependent queries as a solution for loading data from subsequent API endpoints, but the enable option can accept any boolean. In this case - if the search query string is long enough.

There's another option which is to use queryClient.fetchQuery API, which gives you the ability to conditionally call the query to fetch the data.
function Example2() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error;
return (
<div>
<button
onClick={async () => {
try {
setIsLoading(true);
const posts = await queryClient.fetchQuery(
["postsUsingFetchQuery"],
{
queryFn: () =>
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((res) => res.data)
}
);
setData(posts);
} catch (e) {
setError(e);
}
setIsLoading(false);
}}
>
Fetch posts using fetchQuery{" "}
</button>
<h1>Posts</h1>
{data?.map((post) => {
return (
<div style={{ display: "flex" }}>
<span>{post.id}- </span>
<div>{post.title}</div>
</div>
);
})}
</div>
);
}
On the button click handler, we’ve added the implementation to fetch the posts using queryClient.fetchQuery.
You can read more from this link.

Related

Custom useAxios hook in react

I am using axios with react, so I thought to write a custom hook for this which I did and it is working fine like below
const useAxios = () => {
const [response, setResponse] = useState([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true); //different!
const [controller, setController] = useState();
const axiosFetch = async (configObj) => {
const { axiosInstance, method, url, requestConfig = {} } = configObj;
try {
const ctrl = new AbortController();
setController(ctrl);
const res = await axiosInstance[method.toLowerCase()](url, {
...requestConfig,
});
setResponse(res.data);
} catch (err) {
console.log(err.message);
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
console.log(controller);
// useEffect cleanup function
return () => controller && controller.abort();
}, [controller]);
return [response, error, loading, axiosFetch];
};
I have also created one axiosInstance to pass BASE_URL and headers.
Now calling useAxios to fetch data from api like below
const [data, error, loading, axiosFetch] = useAxios();
const getData = () => {
axiosFetch({
axiosInstance: axios,
method: "GET",
url: "/url",
});
};
useEffect(() => {
getData();
}, []);
My Question is
When I need to call one api I am doing above.
But what if I have to call three or four APIs in a single page.
Shall I replicate the code like this const [data1, error1, loading1, axiosFetch]=useAxios();
Or is there any other way to minimize the code.
Edit / Update
I ran above code to get data from /url, what if I want to hit different route to get one more data from server for other work, the base url remains the same
So if the second route is /users
const [data, error, loading, axiosFetch] = useAxios();
const getUsers = () => {
axiosFetch({
axiosInstance: axios,
method: "GET",
url: "/users",
});
};
useEffect(() => {
getUsers();
}, [on_BTN_Click]);
THe above codeI want to run in same file, one to get data and one to get users, how should I write my axios, as I think this const [data, error, loading, axiosFetch] = useAxios(); should gets called only once, Don't know how to do this or what is the correct way, shall I need to change my useAxios hook?
What you could do is pass the endpoint to the hook or properly call the axiosFetch callback with the different endpoints. But I have another opinion about what you are trying to do and here are my 5 cents on why this "axios hook" might not be a good idea.
A good rule of thumb on React Hooks is to use a custom hook if you need to encapsulate component logic that uses React Hooks.
Another important thing that is described in the React Hooks docs is:
Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
So, eventually, if 2 different components call the fetch for the same endpoint, they both are going to execute the call to the Backend. How to prevent that? Well, you could use a lib such as React Query, that creates some kind of "cache" for you (and a bunch of other nice features!)
And last but not least: API calls are much more related to a Service/Module than a React Hook (isolate component logic), as a concept. I hardly advise you to create a service for making API calls and using that service inside your hook instead of coupling that logic to your hook and having to handle all kinds of issues such as Caching and multiple instances of the same hook or even multiple instances of this hook calling multiple different endpoints that eventually could or could not be dependant of themselves.
How about a generic useAsync hook that accepts any asynchronous call? This decouples axios specifics from the the hook.
function useAsync(func, deps = []) {
const [state, setState] = useState({ loading: true, error: null, data: null })
useEffect(
() => {
let mounted = true
func()
.then(data => mounted && setState({ loading: false, error: null, data }))
.catch(error => mounted && setState({ loading: false, error, data: null }))
return () => { mounted = false }
},
deps,
)
return state
}
Here's a basic example of its usage -
function UserProfile({ userId }) {
const user = useAsync(
() => axios.get(`/users/${userId}`), // async call
[userId], // dependencies
})
if (user.loading)
return <Loading />
if (user.error)
return <Error message={user.error.message} />
return <User user={user.data} />
}
The idea is any asynchronous operation can be performed. A more sophisticated example might look like this -
function UserProfile({ userId }) {
const profile = useAsync(
async () => {
const user = await axios.get(`/users/${userId}`)
const friends = await axios.get(`/users/${userId}/friends`)
const notifications = await axios.get(`/users/${userId}/notifications`)
return {user, friends, notifications}
},
[userId],
)
if (profile.loading) return <Loading />
if (profile.error) return <Error message={profile.error.message} />
return <>
<User user={profile.data.user} />
<Friends friends={profile.data.friends} />
<Notifications notifications={profile.data.notifications} />
</>
}
In the last example, all fetches need to complete before the data can begin rendering. You could use the useAsync hook multiple times to get parallel processing. Don't forget you have to check for loading and error before you can safely access data -
function UserProfile({ userId }) {
const user = useAsync(() => axios.get(`/users/${userId}`), [userId])
const friends = useAsync(() => axios.get(`/users/${userId}/friends`), [userId])
const notifications = useAsync(() => axios.get(`/users/${userId}/notifications`), [userId])
return <>
{ user.loading
? <Loading />
: user.error
? <Error message={user.error.message }
: <User user={user.data} />
}
{ friends.loading
? <Loading />
: friends.error
? <Error message={friends.error.message} />
: <Friends friends={friends.data} />
}
{ notifications.loading
? <Loading />
: notifications.error
? <Error message={notifications.error.message} />
: <Notifications notifications={notifications.data} />
}
</>
}
I would recommend you decouple axios from your components as well. You can do this by writing your own API module and even providing a useAPI hook. See this Q&A if that sounds interesting to you.

Infinite scrolling and updating it with newly fetched data in React

I'm trying to render an infinite scroll component and update its data with newly fetched news upon scrolling to the end of it. It works on the first scroll, but gets stuck at Loading... after that. I don't understand what is going on that makes it stop fetching after first scroll.
Code that's supposed to get new data looks like this:
const [latestNews, setLatestNews] = useState<any[]>([]);
const [page, setPage] = useState<number>(1);
const getLatestNews = async (page: number) => {
let url = `https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}&page=${page}`;
const response = await fetch(url);
const data = await response.json();
setLatestNews(data.response.docs);
setPage(page + 1);
}
And my infinite scroll component looks like this
<InfiniteScroll dataLength={latestNews.length} next={() => getLatestNews(page)} hasMore={true} loader={<h4>Loading...</h4>} scrollableTarget="scrollableDiv">
{
latestNews.map((article, index) => (
<div className="latest-news-article flex flex-col gap-3 mb-4" key={index}>
<p className="latest-news-article-date">
{article.pub_date.slice(11, 16)}
</p>
<h1>
{article.headline.main}
</h1>
</div>
))
}
</InfiniteScroll>
I think I found the solution for now.
In my words, that scrolling was "too fast" and it updated the state quickly, much faster than API can take request and I was getting 429 Too Many Requests.
Solution:
const getLatestNews = async () => {
setTimeout(async () => {
const response = await fetch(`https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}&page=${page}`);
const data = await response.json();
setLatestNews((prev) => [...prev, ...data.response.docs]);
setPage(page + 1);
}, 5000);
}
I added setTimeout to 5000ms, that way API requests are not sent instantly, and state changes just fine. It also renders new news accordingly.
Your solution works but there is another solution that is more logical.
You can add a ref using useRef and change its value when you're sending your request in order to prevent from sending another request at the same time:
const sendRequestRef = useRef(true);
const getLatestNews = async () => {
if(sendRequestRef.current === true) {
sendRequestRef.current = false;
const response = await fetch(`https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}&page=${page}`);
const data = await response.json();
setLatestNews((prev) => [...prev, ...data.response.docs]);
setPage(page + 1);
sendRequestRef.current = true;
}
}

Rendering different elements based on promise's result

I'm trying to make a login/logout button based on my user authentication status. since I'm fetching this data from my api, I cant seem to return the data itself, only the promise and since in reactjs the .then() function cant be used I don't know how to access the data I need.
this is the function that fetches the data from api, I want it to return "data.success" which is a boolean, but instead a promise is returned.
let checkAuth = () => {
return fetch('/v1/checkLogin')
.then(response => response.json())
.then(data => {
return data.success
})
}
react code calling above function :
{
(checkAuth())
? <button>logout</button>
: <button>login</button>
}
any help is appreciated =)
Due to the asynchronous nature of requests, the outer code will have already returned before the promise is resolved. That is why it returns a promise instead of your data.
A better approach to get through this is to use "useState" "useEffect" hooks
use "useEffect" to fetch the data when the component renders for the first time
use "useState" store the fetched data to a variable and use it in ways you want
export default LoginComponent = () => {
const [authenticated, setAuthenticated] = useState(false); // False initially
let checkAuth = () => {
const result = await fetch("/v1/checkLogin") // Wait for promise to resolve
const data = result.json();
setAuthenticated(data.success); // Set Data. (You can set just "data" if you want the whole data object)
};
// useEffect which fires once when the component initially renders
useEffect(() => {
checkAuth()
}, []);
return (
<div>
{authenticated ? <button>logout</button> : <button>login</button
</div>
);
};
You can use a state with useEffect to update the UI accordingly
const YourComponent = () => {
const [isAccess, setIsAccess] = useState(); // initialize your state
let checkAuth = () => {
return fetch("/v1/checkLogin")
.then((response) => response.json())
.then((data) => {
setIsAccess(data.success); //set the value for your state
});
};
useEffect(() => {
checkAuth()
}, [])
//after the state update, your UI will be re-rendered with the latest value which you expected
return (
<div>{isAccess ? <button>logout</button> : <button>login</button>}</div>
);
};

How to display custom Nextjs error page when api call fails?

I created a custom Nextjs error page that I would like to display when the api call fails. What is currently happening is even if the api call fails, it still displays the same page as a successful route. For example, I have a route that is companies/neimans that pulls data from an api to display certain text on the page. If I type, companies/neiman I want my custom error page to show, but it is displaying the same page as if going to companies/neimans just without the data coming from the api. I do get a 404 in the console when visiting a url that is invalid but it doesn't display the custom error page or the default next js 404 page.
In my file system I have a pages directory and inside that a directory called companies with a file [companydata].tsx and one called 404.tsx. [companydata].tsx is the page that dynamically displays information about the company from the api.
This is what my api call currently looks like:
export const getCompanies = async (routeData: string): Promise<Company> => {
const client = getApiClient();
const response = await client.get<Company>(`api/companies/${routeData}`);
if (response) {
return response.data;
}
return {} as Company;
In the [companydata].tsx, I tried to do a check if the object was empty to then redirect to companies/404 but doing so makes it always display the 404 page.
if (Object.keys(company).length === 0) {
return <Redirect to="/company/404"/>;
}
If I console.log the company, it is rendering multiple times. The first 6 times, it is an empty array so that would explain why the 404 page is always showing. The data doesn't come through until after the 6th render. I am not sure why that is.
I am calling getCompanies inside another function,
export const getData = async (companyName: string): Promise<[Company, Sales]> => {
if (companyName) {
return (await Promise.all([getCompanies(companyName), getSales()])) as [
Company,
Sales
];
}
return [{} as Company, {} as Sales];
};
I am calling getData inside a useEffect within [companydata].tsx.
const Company: NextPage = (): JSX.Element => {
const [selectedCompany, setSelectedCompany] = useState<Company>({} as Company);
const [salesAvailable, setSalesAvailable] = useState<boolean>(false);
const [sales, setSales] = useState<Sales>({} as Sales);
const router = useRouter();
const {companydata} = router.query;
useEffect(() => {
const init = async (companyName: string) => {
const [companyData, salesData] = await getData(companyName);
if (companyData) {
setSelectedCompany(companyData);
}
if (salesData) {
setSalesAvailable(true);
setSales(salesData);
} else {
setSalesAvailable(false);
}
}
};
init(companydata as string);
};
}, [companydata]);
// returning company page here
You currently do not have a method to check the status of the API call. There are four possible outcomes of most API calls - data, no data, error, and loading. You should add the status checks in your API calls
Below are two examples of how this can be achieved.
get companies hook
export const useGetCompanies = async (path: string) => {
const [data, setData] = useState<Company>();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
try {
setError(false);
setLoading(true);
const client = getApiClient();
const response = await client.get(`api/companies/${path}`);
setData(response.data);
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
return {data, error, loading};
};
Since your data isn't related you also do a generic API fetch call something like
export async function useFetchData<T>(path:string){
const [data, setData] = useState<T>();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
try {
setError(false);
setLoading(true);
const client = getAPIClient();
const response = await client.get<{ data: T }>(path);
if(response) setData(response.data);
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
return { data, error, loading };
};
Example use.
const Company = async () =>{
const { query } = useRouter();
const company = await useFetchData<Company>(`api/companies/${query.companydata}`);
const sales = await useFetchData<Sales>(`api/companies/${query.companydata}/sales`);
if (company.loading || sales.loading) return <p>Loading...</p>;
if (company.error || sales.error) return <p>Error or could show a not found</p>;
if (!company.data || !sales.data) return <Redirect to="/company/404"/>;
return "your page";
}
It would be best to render the data independently of each other on the page and do the if checks there. This is beneficial because you don't have to wait for both calls to complete before showing the page.
I'd create two separate components (company and sales) and place the corresponding API call in each.
Typically assigning empty objects ({} as Company or {} as Sales) to defined types is bad practice because it makes TS think the object's values are defined when they are not - defeating the purpose of using TS.
They should be left undefined, and there should be a check to see if they are defined.
Lastly, I can't test the code because I don't have access to the original code base so there might be bugs, but you should get a pretty good idea of what's happening.

React custom fetch hook is one step behind

I am creating my custom fetch hook for both get and post data. I was following official React docs for creating custom fetch hooks, but it looks like my hook-returned state variables are one step behind behind due to useState asynchronous behaviour. Here is my custom useMutation hook
export const useMutationAwait = (url, options) => {
const [body, setBody] = React.useState({});
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
const fetchData = async () => {
setError(null);
setIsLoading(true);
console.log("...fetching");
try {
const response = await axiosInstance.post(url, body, options);
setData(response.status);
} catch (error) {
console.error(error.response.data);
setError(error.response.data);
}
setIsLoading(false);
};
fetchData();
}, [body]);
return [{ data, isLoading, error }, setBody];
};
And I am using it in my component like this (simplified) - when user presses register button I want to be able immediately decide if my post was successful or not and according to that either navigate user to another screen or display fetch error.
const [email, setEmail] = React.useState('');
const [password, setPassword] React.useState('');
const [{ data: mutData, error: mutError }, sendPost] =
useMutationAwait("/auth/pre-signup");
const registerUser = async () => {
sendPost({
email,
password,
}); ---> here I want to evaluate the result but when I log data and error, the results come after second log (at first they are both null as initialised in hook)
Is this even correct approach that I am trying to achieve? Basically I want to create some generic function for data fetching and for data mutating and I thought hooks could be the way.
Your approach isn't wrong, but the code you're sharing seams to be incomplete or maybe outdated? Calling sendPost just update some state inside your custom hook but assuming calling it will return a promise (your POST request) you should simply use async-await and wrap it with a try-catch statement.
export const useMutationAwait = (url, options) => {
const sendPost = async (body) => {
// submit logic here & return request promise
}
}
const registerUser = async () => {
try {
const result = await sendPost({ login, password });
// do something on success
} catch (err) {
// error handling
}
}
Some recommendations, since you're implementing your custom hook, you could implement one that only fetch fetch data and another that only submit requests (POST). Doing this you have more liberty since some pages will only have GET while others will have POST or PUT. Basically when implementing a hook try making it very specific to one solution.
You're absolutely correct for mentioning the asynchronous nature of state updates, as that is the root of the problem you're facing.
What is happening is as follows:
You are updating the state by using sendPost inside of a function.
React state updates are function scoped. This means that React runs all setState() calls it finds in a particular function only after the function is finished running.
A quote from this question:
React batches state updates that occur in event handlers and lifecycle methods. Thus, if you update state multiple times in a handler, React will wait for event handling to finish before re-rendering.
So setBody() in your example is running after you try to handle the response, which is why it is one step behind.
Solution
In the hook, I would create handlers which have access to the data and error variables. They take a callback (like useEffect does) and calls it with the variable only if it is fresh. Once it is done handling, it sets it back to null.
export const useMutationAwait = (url, options) => {
const [body, setBody] = React.useState({});
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const handleData = (callback) => {
if (data){
callback(data);
setData(null);
}
}
const handleError = (callback) => {
if (error){
callback(error);
setError(null);
}
}
React.useEffect(() => {
const fetchData = async () => {
setError(null);
setIsLoading(true);
console.log("...fetching");
try {
const response = await axiosInstance.post(url, body, options);
setData(response.status);
} catch (error) {
console.error(error.response.data);
setError(error.response.data);
}
setIsLoading(false);
};
fetchData();
}, [body]);
return [{ data, handleData, isLoading, error, handleError }, setBody];
};
We now register the handlers when the component is rendered, and everytime data or error changes:
const [
{
data: mutData,
handleData: handleMutData,
error: mutError,
handleError: handleMutError
}, sendPost] = useMutationAwait("/auth/pre-signup");
handleMutData((data) => {
// If you get here, data is fresh.
});
handleMutError((error) => {
// If you get here, error is fresh.
});
const registerUser = async () => {
sendPost({
email,
password,
});
Just as before, every time the data changes the component which called the hook will also update. But now, every time it updates it calls the handleData or handleError function which in turn runs our custom handler with the new fresh data.
I hope this helped, let me know if you're still having issues.

Resources