How to use the useQueries hook in react-query v4 - reactjs

So I'm perfectly able to retrieve matches from a competition with the Football-data.org API and display them in my react/typescript application using the useQuery hook from react-query:
import {CompetitionProps} from "../App";
import {getMatchesFromApi, Match} from "../api/GetMatchesFromApi";
import {List, ListItem} from "#mui/material";
import {useQuery} from "#tanstack/react-query";
function MatchesList({competitions}: CompetitionProps) {
const { isLoading, error, data, isFetching} = useQuery(["matches"], async () => {
return await getMatchesFromApi(competitions);
});
if (isLoading || isFetching) {
return (<div>Loading</div>);
} else {
return (
<List>
{data?.map((match: Match) => {
return (
<ListItem key={match.id}>{match.homeTeam.shortName} - {match.awayTeam.shortName}</ListItem>
);
})}
</List>
);
}
}
export default MatchesList;
However I want all matches from a list of competitions (the competitions can be different based on user preferences). When reading the docs of react-query, the useQueries hook should do the trick. These docs unfortunately don't show how to handle the results of the useQueries hook:
https://tanstack.com/query/v4/docs/reference/useQueries
I tried to use it like this:
import {
useQueries,
UseQueryOptions,
UseQueryResult
} from "#tanstack/react-query";
import { getMatchesFromApi, Match } from "./GetMatchesFromApi";
const allCompetitions = [2003, 2021];
function MatchesList() {
const results = useQueries({
queries: allCompetitions.map<UseQueryOptions<Match[]>>(
(competition: number) => {
return {
queryKey: ["competition", competition],
queryFn: async () => await getMatchesFromApi(competition)
};
}
)
});
return <div>{results.length}</div>;
}
export default MatchesList;
Even though I'm not even attempting to display the data yet, just using this code that only prints the length of the results array, will cause the code to fetch every few seconds. This will result in 429 (too many requests) responses of the football-data.org API fairly quickly.
This behavior doesn't match at all with the default staleTime and cacheTime settings explained in:
https://medium.com/doctolib/react-query-cachetime-vs-staletime-ec74defc483e
The question: How do I stop this infinite fetching loop? The reason I wanted to use react-query at all is to lazy fetch the match data only once.
Full project to reproduce this problem:
https://codesandbox.io/s/serene-raman-47n2uz
(If you want to reproduce it, you'll have to register on football-data.org for free and generate an API key. Put your key in the Key.ts file. I don't want to put mine on the internet).
Thanks!

Related

React Query does not work with Astro Framework

I'm trying to use React-Query with Astro to fetch data from my Django Rest Framework backend. Astro has been a great way to organize my react-based frontend but I am worried it might not be compatible with React-Query.
Whenever I try to make a query to my backend I get an 'isLoading' value of true (and an isError of false). I never manage to recover the data from my endpoints however.
I have been following a variety of tutorials with the same results. Here is the code where I'm stuck:
import { QueryClient, useQueryClient, QueryClientProvider, useQuery } from '#tanstack/react-query';
import { gettestApi } from "../../api/testApi";
function MyComponent(props) {
const queryClient = useQueryClient();
const {
isLoading,
isError,
error,
data: test
} = useQuery('test', gettestApi)
let content
if (isLoading) {
content = <p>Loading...</p>
} else if (isError){
content = <p>{error.message}</p>
} else {
content = JSON.stringify(test)
}
As you can see, I import an axios function from /api/testAPI.js which looks like this:
import axios from "axios"
const testApi = axios.create({
baseURL: "http://127.0.0.1:8000"
})
export const gettestApi = async () => {
return await testApi.get("/api/endpoint/").then(response => response.data)
}
That's how most tutorials I have seen and the official documentation wrap up their examples, however my backend server which should be triggered by this endpoint records absolutely no hits from react-query, which is curious to me. I understand that nothing 'calls' my react-query or my gettestApi() function, but it seems to be unnecessary for other people to retrieve their data.
Maybe it would be useful to point out that contrary to other framework with React, Astro does not have an App.js root to surround with
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>
Instead, I have added these QueryClientProvider brackets to the highest React component I could.
I feel like I'm missing some intuition about Tanstack Query/ React-Query. Could anybody point me in the right direction? Thanks a lot for the help.
From what I've seen in the astro docs:
The most important thing to know about Astro components is that they render to HTML during your build. Even if you run JavaScript code inside of your components, it will all run ahead of time, stripped from the final page that you send to your users. The result is a faster site, with zero JavaScript footprint added by default.
So it seems all react code only runs on the server, where data fetching via useEffect or useSyncExternalStore subscriptions just doesn't run. But this is exactly what react-query is doing, so yeah I think they don't work well together. I'm also not sure what the purpose of react-query in a setting would be where there is no client side javascript.
You can set client:only on your Astro component so the React component doesn't run on the server. There are shared-state limitations but still React Query feels better than just fetch + useEffect + own-code even if its not in a complete React app. In this example I'm also using an init function that reads cookies from the client's browser which is another case for when to use client:only.
Astro:
---
import Layout from "../../layouts/Layout.astro";
import ClientPanel from "../../components/client/ClientPanel";
---
<Layout title={ 'Client' }>
<ClientPanel client:only></ClientPanel>
</Layout>
React:
// imports
const queryClient = new QueryClient()
/** client:only component */
const ClientPanel = () => (
<QueryClientProvider client={queryClient}>
<ClientData />
</QueryClientProvider>
)
const ClientData = () => {
const { getUser, getSession } = useSession(); // read cookies functions
const [ user, setUser ] = useState(getUser);
const { isLoading, error, data } = useQuery({
queryKey: ['patientData'],
queryFn: () => getSession() // validate or refresh token
.then(session => fetchPatientData(session.tokens.token))
.catch(error => error === 'INVALID_SESSION' ? null : undefined)
})
if (!user || data === null) window.location.replace('/login')
// return statement, etc.

How to render [slug].js page after fetching data Next.js

I am trying to create a logic for my blog/:post page in Next.js but I cannot seem to figure out how.
The idea is to:
Fetch the url (using useRouter)
Call API (it is a headless CMS) to get the info of the post
Render the post
What I have right now is:
[other imports ...]
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
function BlogPost(props) {
const router = useRouter()
const { slug } = router.query
const [blogPost, setBlogPost] = useState({})
// Function to the blog post
function fetchBlogPost() {
butter.post.retrieve(slug)
.then(response => {
const blogPostData = response.data.data
setBlogPost(blogPostData)
})
}
useEffect(() => {
// We need to add this if condition because the router wont grab the query in the first render
if(!router.isReady) return;
fetchBlogPost()
}, [router.isReady])
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
But this is not rendering everything (the image is not being rendered for example). I believe it is because of the pre-render functionality that Next.js has. Also I have been reading about the getStaticProps and getStaticPaths but I am unsure on how to use them properly.
Any guidance will be welcome. Thanks!
If you're using next.js then you are on track with getStaticProps being your friend here!
Essentially getStaticProps allows you to take advantage of ISR to fetch data on the server and create a static file of your page with all of the content returned from the fetch.
To do this you'll need to make an adjustment to your current architecture which will mean that instead of the slug coming in from a query param it will be a path parameter like this: /blogs/:slug
Also this file will need to be called [slug].js and live in (most likely) a blogs directory in your pages folder.
Then the file will look something like this:
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
export const getStaticPaths = async () => {
try {
// You can query for all blog posts here to build out the cached files during application build
return {
paths:[], // this would be all of the paths returned from your query above
fallback: true, // allows the component to render with a fallback (loading) state while the app creates a static file if there isn't one available.
}
} catch (err) {
return {
paths: [],
fallback: false,
}
}
}
export const getStaticProps = async ctx => {
try {
const { slug } = ctx.params || {}
const response = await butter.post.retrieve(slug)
if(!response.data?.data) throw new Error('No post data found') // This will cause a 404 for this slug
return {
notFound: false,
props: {
postData: response.data.data,
slug,
},
revalidate: 5, // determines how long till the cached static file is invalidated.
}
} catch (err) {
return {
notFound: true,
revalidate: 5,
}
}
}
function BlogPost(props) {
const {isFallback} = useRouter() // We can render a loading state while the server creates a new page (or returns a 404).
const {postData} = props
// NOTE: postData might be undefined if isFallback is true
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
In any case, though if you decide to continue with rendering on the client instead then you might want to consider moving your fetch logic inside of the useEffect.

What is the correct way to pass parameters to a React-query useQuery method that uses Axios

I am currently building a Ruby on Rails Webpacker application with a React front end. I am at the point where I would like to create all the quires I need to make calls to my Rails API. I was loosely following this tutorial https://www.youtube.com/watch?v=0bKc_ch6MZY (https://github.com/daryanka/react-query-tutorial/blob/master/src/containers/Post.js, https://github.com/daryanka/react-query-tutorial/blob/master/src/Queries.js), in order to write some axios based query functions that I could use with react-query. I had no problem with getting the queries to behave as expected when the url for the endpoint was a hard coded string. When I attempted to pass in a parameter to make dynamic urls I ran into the issue of not having access to said parameter; specifically the "prodId" parameter. I did however notice that the "prodId" was inside the "key" parameter array like so:
queryKey: Array(2)
0: "product"
1: "1"
length: 2
enter code here
I could just access it from there but that approach does seem a little off, I also did not find any examples or documentation that attempted to access a parameter from the query key array. I would like to know what it is I am doing incorrectly with regards to passing in parameters? Were there some syntax changes in react-query that I am not taking into account?
react-query#^3.17.2
webpacker (5.2.1)
axios#^0.21.1
//Product.js
import axios from "axios"
import { getProduct } from "../../queries/products"
import { useQuery } from "react-query"
const prodId= '1'
const { data } = useQuery(['product', prodId], getProduct)
//queries/products.js
import axios from 'axios'
export const getProduct = async (key, { prodId }) => {
console.log(opid)
const { data } = await axios.get(`/api/v1/products/${prodId}`)
return data
}
The query function that you pass to react-query gets a queryContext injected, which is an object that consists of the queryKey (and some more information if you are using an infinite query). So yes, one correct way to access dependencies is through the queryKey:
export const getProduct = async ({ queryKey }) => {
const [_, prodId] = queryKey
const { data } = await axios.get(`/api/v1/products/${prodId}`)
return data
}
const { data } = useQuery(['product', prodId], getProduct)
Another way is to use inline anonymous functions, which is well documented in the docs in: If your query function depends on a variable, include it in your query key
export const getProduct = async (prodId) => {
const { data } = await axios.get(`/api/v1/products/${prodId}`)
return data
}
const { data } = useQuery(['product', prodId], () => getProduct(prodId))
I'm using the following (typescript) to send parameters to my custom useQuery hook.
import { useQuery } from 'react-query'
import service from '../api'
const queryKey = 'my-query-key'
type useProductsParams = Parameters<typeof service.listProducts>
const useProducts = (...params: useProductsParams) => {
return useQuery(queryKey, () => service.getProduct(...params))
}
export default useProducts

Best Practice how to fetch data initially and update it later on using React Hooks etc.?

There exist a lot of exmaples how to fetch data in functions using custom hooks. In my scenario I'm using the react async hook library (but it could be any other similar hook) and I fetch data (in my example a list of teams) initially. When this list is empty, I show a button which provides the means to create some teams by triggering a backend request. Within this request the now created teams are returned and I want to show them.
This looks like this (very simplified):
import {useAsync} from "react-async-hook";
import React, {useEffect, useState} from "react";
import { fetchTeams, generateTeams } from 'somewhere';
import TeamsList from 'somewhere';
const Teams = () => {
const asyncResult = useAsync(fetchTeams); // Async backend request
const [teams, setTeams] = useState(); // This is my workaround...
useEffect(() => {
if (asyncResult.loading === false && asyncResult.result && asyncResult.result.teams) {
setTeams(asyncResult.result.teams); // This is how I try to react when the fetchTeams promise is resolved...
}
}, [asyncResult.loading, asyncResult.result]);
const handleGenerateTeams = async () => {
const teamGenerationResult = await generateTeams(); // Async backend request
setTeams(teamGenerationResult.teams);
};
if (asyncResult.loading || (!asyncResult.error && !teams)) { return <div>Loading</div>; }
if (asyncResult.error) { return <div>{asyncResult.error.message}</div>; }
const teamsNotExisting = teams.length === 0;
return (
{ teamsNotExisting && <button onClick={handleGenerateTeams}>Generate</button> }
<TeamsList teams={teams} />
);
};
This is how I ended up to deal with this use case.... but I am wondering if this is really a good solution?
Most examples which are using custom hooks just fetch data and pass it directly for rendering. But this is not sufficient for me, due to I have my click handler which may also generate the data (if not yet existing)... this is the reason why I introduced the teams-state.
So, one other basic question: Is this the way to pass the results from an async fetch hook into my own state?
I know that one answer to my whole question would probably be Redux. But I often read, that Redux might not be needed after all, and for my application I'm quite happy for now without Redux....
I think react-async-hook is a pretty good library to handle tasks related to asynchronous, by its pretty hight starred on Github, weekly downloads, so I think somehow its core was implemented with best practice.
So, our job is to implement our business logic or our specific use-case here with "Best Practice", so I think the "Best Practice" here is about how we're confident and happy in our code, and others who will read, or maintain this code in future is also happy. So I think I can help you with a little refactor it as below:
import {useAsync, useAsyncCallback} from "react-async-hook";
import React, {useEffect, useState} from "react";
import {fetchTeams, generateTeams} from 'somewhere';
import TeamsList from 'somewhere';
const Teams = () => {
const {result, loading, error} = useAsync(fetchTeams);
const {execute, result: generatedTeamsResult} = useAsyncCallback(generateTeams);
const handleGenerateTeams = () => {
execute();
};
if (loading) { return <div>Loading</div>; }
if (error) { return <div>{error.message}</div>; }
const shouldGenerateTeam = !result && !result.teams;
const teams = result && result.teams ? result.teams : generatedTeamsResult.teams;
return (
{shouldGenerateTeam && <button onClick={handleGenerateTeams}>Generate</button>}
{/* Should validate data and avoid passing null or undefined */}
{teams && <TeamsList teams={teams || []} />}
);
};

How to customize data provider request call?

I'm new to react and react-admin.
I'm, using jsonServerProvider (in my App.js I have the following):
import jsonServerProvider from 'ra-data-json-server';
I'd like to create a custom bulk action. In a list, select many items and click a button to "connect" them. I tried to use UPDATE_MANY, but this calls my endpoint multiple times, so it's not suitable. Ideally I need the request to call my endpoint like so: url.to.myendpoint?ids=1,2,3 or even better pass an array of IDs in the body and use a PUT request.
Just to understand how things work and debug network calls, I tried also the GET_MANY, in the dataproviders page, the request seems to get the IDs like so: { ids: {mixed[]}, data: {Object} }
But the request is sent to the server like so: url.to.myendpoint?id=1&id=2&id=3 which in my python/flask backend is not nice to parse.
I've spent a bunch of time reading the docs, e.g.:
https://github.com/marmelab/react-admin/blob/master/docs/Actions.md
https://react-admin.com/docs/en/actions.html
https://marmelab.com/react-admin/Actions.html
I tried different approaches and I could not achieve what I want. So again please help me to make my custom bulk button work.
My bulk button is called ConnectItemsButton and the code looks like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Button, crudUpdateMany } from 'react-admin';
import { showNotification, GET_MANY } from 'react-admin';
import dataProvider from './dataProvider';
class ConnectItemsButton extends Component {
handleClick = () => {
const { selectedIds } = this.props;
dataProvider(GET_MANY, 'items/connect', { ids: selectedIds })
.then(() => {
showNotification('Connected!');
})
.catch((e) => {
showNotification('Error.', 'warning')
});
};
render() {
return (
<Button label="Associate" onClick={this.handleClick} />
);
}
}
export default connect(undefined, { crudUpdateMany })(ConnectItemsButton);
Note that the contents of ./dataProvider (it's the same provider used in the App.js file and passed to the <Admin> in the props):
import jsonServerProvider from 'ra-data-json-server';
export default jsonServerProvider('http://127.0.0.1:5000/api');
In my list I created it, the button is displayed properly, so here I share the code snippet:
const PostBulkActionButtons = props => (
<Fragment>
<ConnectItemsButton {...props} />
</Fragment>
);
...
export const ItemsList = props => (
<List {...props} bulkActionButtons={<PostBulkActionButtons />}>
...
In my backend endpoint items/connect I simply need to get a comma separated list of IDs to parse, that's it.
A simple working solution would be awesome, or at least point me in the right direction. Thanks for your help.
The way I would do this is by using react-admin's dataActions. Your action would be something like this:
crudCreate('items/connect', { selectedIds: selectedIds }, basePath , redirectUrl)
I recommend using a custom dataProvider (e.g. if you use jsonDataProvider, in your App.js import where you see ra-data-json-server: if you use WebStorm Ctrl + click on it and copy the code e.g. to customJsonDataProvider.js and fix eventual warnings, e.g. import lines should be moved at the top) and pass it as props to your Admin component. In your customJsonDataProvider you will have a convertDataRequestToHTTP, or something similar, which manages the CRUD actions and returns the url and HTTP method that you want to use.
An example of what you want to do would be:
const convertDataRequestToHTTP = (type, resource, params) => {
let url = '';
const options = {};
switch (type) {
...
case CREATE: {
...
if (type === 'items/connect') {
const { data: { selectedIds } } = params;
url = `${apiUrl}?${selectedIds.reduce((acc, id) => `${acc};${id}`)}`
options.method = 'GET';
}
...
break;
}
...
}
return { url, options };
}
In your dataProvider.js, modify the import to use the custom provider you created, e.g.:
import jsonServerProvider from './customJsonServer';
The code in your AssociateTradesButton.js should work fine as is.
You can find the documentation for creating you own dataProvider here.
I think using Promise.all will solve the issue, see this link for reference https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/src/index.js#L140

Resources