-
What Is Tanstack Query?
- A library that helps with sending HTTP requests
- & helps with keeping your frontend UI in sync with your backend data
-
Why Would You Use It?
- You don't need Tanstack Query, but
- it can simplify your code (and your life as a developer)
- it is able to get rid of a bunch of code like state management or some other code as well
- it gives you some advanced features, like caching, behind the scenes data fetching, etc
-
Fetching & Mutating Data
-
Configuring Tanstack Query
-
Advanced Concepts: Cache Invalidation, Optimistic Updating & More
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- @vitejs/plugin-react uses Babel for Fast Refresh
- @vitejs/plugin-react-swc uses SWC for Fast Refresh
- in your terminal, run
cd backend
&npm install
&npm start
- open a new terminal & run
npm install
&npm run dev
- in the terminal, run
npm install @tanstack/react-query
- in
src\components\Events\NewEventsSection.jsx
, use Tanstack Query:- cut the
fetchEvents()
function inside theuseEffect()
& paste it in a new file insrc/util/http.js
- export this function so that you can use it outside of that file with Tanstack Query
- go back to
NewEventsSection.jsx
and get rid of the state management code & the useEffect() code - import the
{useQuery}
hook from@tanstack/react-query
- use this
useQuery
hook inside theNewEventsSection
component to send a HTTP request behind the scenes, etc - you must configure it by adding inside of it an object with:
- first step:
- a
queryFn
property which is a function that defines the actual code that will send the actual request because:- Tanstack Query doesn't send HTTP requests, at least not on its own
- you have to write the code that sends the actual HTTP request
- Tanstack Query then manages the data, errors, caching & much more!
- import the outsourced
fetchEvents()
& point at it as a value of thequeryFn
property
- a
- second step:
- a
queryKey
property - as a value to it, set an array of values that are internally stored by React Query so that it can reuse existing data
- a
- first step:
- you get an object back from
useQuery
from where you can pull out the elements you need, like:- the
data
property which holds the actual response data as a value, so the data that is returned byfetchEvents()
- the
isPending
property - the
isError
property - the
error
property - & more
- the
- use now
isPending
instead of the oldisLoading
state to show theLoadingIndicator
whilst you're waiting for a response - check for
isError
instead of the olderror
used with the state management to show theErrorBlock
- and in that
ErrorBlock
instead of just hardcoding themessage
, use theerror
object & itsinfo
& itsmessage
- output the
data
if we did successfully fetch the events - in
App.js
, wrap the components that use React Query withQueryClientProvider
&QueryClient
imported from@tanstack/react-query
- cut the
- you can control the behaviour of React Query, for example by setting a
staleTime
on your queries- this controls after which time React Query will send the behind the scenes request to get updated data if it found data in your cache
- the default value is
0
, which means it will usedata
from the cache, but always send this behind the scenes request to get updated data - if you set it to
5000
, it will wait for 5 seconds before sending another request
- you can also set the
gcTime
property- this controls how long the
data
in the cache will be kept around - the default value is 5 minutes
- this controls how long the
- in
src\components\Events\FindEventSection.jsx
import{useQuery}
from@tanstack/react-query
- executes
useQuery()
- in
http.js
, tweak a little bit thefetchEvents()
function by adding a query parameter to the request - go back to
FindEventSection.jsx
and configure theuseQuery
object & manage some state to update the query when thesearchTerm
changes - get back the object from
useQuery()
& in there get back thedata
,isPending
,isError
&error
- use this pieces of information to dynamically & conditionally output the
content
in this component
- React Query & the
useQuery()
hook passes some default data to the query function when assigned toqueryFn
- in
http.js
,console.log(searchTerm)
to see that data - this is an object that gives information about:
- the
queryKey
- a
signal
needed for aborting the request if you navigate away from the page before the request was finished
- the
- in
- therefore, in
http.js
, we should accept infetchEvents()
such an object and pull out for example- the
signal
then pass thissignal
to thefetch()
function as a second argument searchTerm
- the
- now, in
FindEventSection.jsx
, make sure you:- pass an object to
fetchEvents()
& set a property namedsearchTerm
- forward that
signal
fromhttp.js
via asignal
object to the anonymous function & set it as an argument offetchEvents()
- pass an object to
- in
FindEventSection.jsx
, disable the query until a search term has been entered with help of theenabled
property- initially, show no events
- but, if a user searched something then clear the input, show all the events
- the
searchTerm
set should be initially undefined by not passing any value at all touseState()
- set
searchTerm !== undefined
as a value ofenabled
- so if if
searchTerm
isundefined
, which is the initial value, the query will be disabled - but, if it's anything else, including
''
(which would be the case if the user cleared the input field manually), the query will be enabled
- so if if
- use
isLoading
instead ofisPending
to get rid of the initial loading spinner which displayed because when a query is disabled, React treats it asisPending
- in
NewEvent.jsx
, useuseQuery()
:- to send data
- to collect that data
- the data is already collected in
EventForm.jsx
- to send the data, in
NewEvent.jsx
, use theuseMutation
hook which:- is optimized for such data changing queries
- for example, by making sure that those requests are not sent instantly when the component renders unlike
useQuery
- but, that instead requests are only sent when you want to send them, for example, from inside the
handleSubmit
function
- use
useMutation()
& configure its object:- set a
mutationFn
function - for that, add a new
createNewEvent
function inhttp.js
- set this function as a value for the
mutationFn
property
- set a
useMutation
returns an object that you can destructure to pull out some useful properties, like the:mutate
property which is a function that you can call anywhere in this component to send this requestisPending
propertyisError
propertyerror
property
- therefore, inside the
handleSumbit()
function, callmutate()
& pass theformData
to it as a value - handle errors & show some loading text whilst the request is on its way with help of the returned
useMutation
object properties
- in
NewEvent.jsx
, display a list of images in the<EventForm>
component - so, in
EventForm.jsx
, you need to fetch that list of images from the backend - to do that, use
useQuery()
& configure it - in
http.js
, add a newfetchSelectableImages
function - in
EventForm.jsx
, set thisfetchSelectableImages
function as a value for thequeryFn
property - set a value of
['events-images']
for thequeryKey
property - use that object returned by
useQuery()
to get holddata
,isPending
&isError
- set the
data
(so, that list of images) as a value for theimages
prop on the<ImagePicker>
component - show conditionally the list of images if we have
data
- show a loading text if
isPending
is true - show an the
<ErrorBlock>
component ifisError
is true
- in
NewEvent.jsx
, when creating a new event, wait for this mutation to be finished until navigating away- to do that, add a new
onSuccess
property to theuseQuery()
configuration object - it wants a function as a value that will be executed once this
mutationFn
succeeded - inside of this method, call
navigate('/events')
- to do that, add a new
- when creating a new event, the new event must be rendered straight away in the UI without switching to a different page then coming back to refetch data behind the scenes
- React Query should immediately refetch data & update your data in the UI
- in
NewEventsSection.jsx
, thedata
in the query should be marked as stale & refetch is triggered - you can achieve this by calling a method provided by React Query that allows us to invalidate one or more queries
- before that, in
App.jsx
, with help of theQueryClient
object, you will force this invalidation of a query - therefore, cut
QueryClient
& add it inhttp.js
so that you can import it from multiple files - now, in
App.jsx
, import thisqueryClient
constant fromhttp.js
- now, in
NewEvent.jsx
, in theonSuccess
method before navigating away, callqueryClient.invalidateQueries()
- to target specific queries,
invalidateQueries()
takes an object as an input where you have to define thequeryKey
which you want to target - set
queryKey: ['events']
to invalidate all queries that include theevents
key even if it is not exactly the same key (if you don't useexact: true
)
- update the
util/http.js
file so that you add to it thefetchEvent()
&deleteEvent()
functions - use the
fetchEvent()
function together with React Query'suseQuery()
hook to fetch the event details inEventDetails.jsx
& output the event details, like the event title, the image- in order to fetch the data for a single event, you'll need the ID of that event
- you can get that in the
EventDetails
component via React Router'suseParams
hook
- make the delete button work by using the
deleteEvent()
function together with React Query'suseMutation()
inEventDetails.jsx
so that you get a mutation which you can execute when this button is clicked
- use React Query's
useQuery()
hook to fetch data:- in
EventDetails.jsx
, importuseQuery
& execute it in theEventDetails
component function - configure the query by adding:
queryFn
which hasfetchEvent()
as a value & get access to theid
withuseParams()
& pass it to the functionqueryKey
which has['events', params.id]
as a value
- get the object from
useQuery()
& pull out from itdata
,isPending
,isError
&error
properties - use this pieces of data to output different content on the screen depending on the current state of this query
- formate the
date
in a nice way
- in
- use ReactQuery's
useMutation()
hook to delete data:- in
EventDetails.jsx
, importuseMutation()
& execute it in theEventDetails
component function - configure the query by adding
mutationFn
& settingdeleteEvent
to it as a value - get the object from
useMutation()
& pull out from it themutate
property - trigger the
mutate()
fonction when thedelete
button is pressed by callig it inside ahandleDelete()
function- pass an object to
mutate()
- add an
id
property which has the id of that to be deleted event (params.id
) as a value
- pass an object to
- connect the
handleDelete()
function to thisdelete
button with help of theonClick
prop - define what should happen after the mutation succeeds
- add the
onSuccess
property to the mutation configuration object - set to it an anonymous function & inside of it navigate away with the React Router DOM's
useNavigate()
hook - invalidate your event related queries because
- the data should be marked as outdated after deletion of the event with
queryClient
&invalidateQueries
- and React Query should be forced to fetch data again
- the data should be marked as outdated after deletion of the event with
- add the
- in
- in
EventDetails.jsx
, since we invalidate all queries, React Query immediately triggers a refetch for this details query & provokes an error in the console - to avoid this behaviour, you must:
- add a second
refetchType
property to this configuration object forqueryClient.invalidateQueries()
- and set its value to
'none'
- which makes sure that when you call
invalidateQueries()
, these existing queries (queryKey: ['events]
) will not automatically be triggered again immediately - instead they will just be invalidated & the next time they will be required, they will run again
- add a second
- in
EventDetails.jsx
, add a confirmation modal before we trigger this deletion mutation- manage some
isDeleting
state with theuseState()
hook initially set tofalse
that tells us whether the user started the deletion process or not - change this to
true
once the user clicks thisDelete
button - open up a modal where the user has to click another button to start this deletion mutation
- add a new
handleStartDelete()
function in which you setsetIsDeleting
totrue
- add a new
handleStopDelete()
function in which you setsetIsDeleting
totrue
(if the user cancels this delete process) - connect
handleStartDelete
to theDelete
button in this UI instead ofhandleDelete
- but, now this UI should also contain another component that can be displayed
- in your
return
statement, add the<Modal>
component - inside this component, show some confirmation text and
Cancel
&Delete
buttons - this
Delete
button, when is clicked, should trigger thehandleDelete
function you used before to delete the event - this
Cancel
button should trigger thehandleStopDelete
function to stop the deletion mutation & close this modal again - because this modal should be display conditionally if
isDeleting
istrue
- this
<Modal>
component takes anonClose
prop which triggershandleStopDelete
when this modal is closed
- in your
- manage some
- having to wait for a short while before the event is being deleted is not ideal because you should give the user some feedback that this deletion was initiated
- in the
useMutation
object, pull out:isPending
property & set it toisPendingDeletion
to avoid a name clashisError
& name itisErrorDeleting
error
& name itdeleteError
- use these properties to show
- some loading text whilst the request is on its way
- and some error output if the deletion should fail
- in the
- when clicking on the
Edit
button from theEventDetails
page and the modal is opened, prepopulate the modal with the event data to which it belongs - in
EditEvent.jsx
, load the data that should be filled into this form as a default & pass it to theinputData
prop in the<EventForm>
component with help ofuseQuery
- execute
useQuery()
& configure it - get back the object from
useQuery()
& pull out the needed properties from it, likedata
,isPending
,isEror
&error
- use
data
to prepopulate this form by settinginputData={data}
- use
isPending
to show a loading indicator (LoadingIndicator
) if we're still waiting for a response - use
isError
&error
to show an error block (<ErrorBlock>
) if we get an error - use
data
to show the<EventForm>
work on this update functionality so that you can update an event from the modal after clicking the Edit
button
- update the
http.js
file which now includes anupdateEvent
function - in
EditEvent.jsx
, you need a mutation to send a request to the backend that changes the event data- create a mutation with
useMutation()
- trigger it by calling the
mutate()
property & targetting theupdateEvent
function from inside thehandleSubmit()
- in
mutate()
, pass this object with the to be forwarded data toupdateEvent
to thismutate()
function- an object that has
id
&event
properties - hence pass
{id: params.id, event: formData}
- an object that has
- call
navigate()
right aftermutate()
insidehandleSubmit()
- create a mutation with
- Do optimistic updating so that, when you press the
Update
button, the UI is updated instantly without waiting for the response of the backend - & if the update fails, roll back the optimistic update you performed
- in
EditEvent.jsx
, add a newonMutate
property to thisuseMutation()
configuration object- this property wants a function as a value which will be executed right when you call
mutate()
, so before this process is done & before you got back a response - in this function, update the data that is cached by React Query, this event data that is stored behind the scenes so to say
- import
queryClient
fromhttp.js
to change the cached data (and not to invalidate queries this time) - inside the
onMutate
function, get the currently stored data so that we can manipulate it & edit it without waiting for a response with help ofqueryClient.setQueryData()
setQueryData()
needs 2 arguments- the key of the query that you want to edit
['events', params.id]
- the new
data
you want to store under that query key, which is, in this case, that updated event dataformData
which you also sent to your backend that you can store in anewEvent
constant
- inside
onMutate()
, usequeryClient.cancelQueries()
to cancel all active queries for a specific key
- import
- this property wants a function as a value which will be executed right when you call
- to make sure you can roll back your optimistic update if it fails on the backend
- you also need to get the old data & store it somewhere so that you can roll back to that old data
- do that before you update the data (
queryClient.setQueryData()
) with help ofqueryClient.getQueryData()
which gives you the currently stored query data - store it in a
previousEvent
constant
- do that before you update the data (
- roll back to
prevousEvent
if your update mutation failed by adding a newonError
property to thisuseMutation()
configuration object- this property wants a function which will be executed if this
mutationFn: updateEvent
function fails - it receives a couple of inputs that are passed in automatically by React Query
error
,data
&context
objects- this
context
object can contain thispreviousEvent
- in order for this
previousEvent
to be part of thiscontext
,- you should return an object in this
onMutate
function, because this object will be thiscontext
- and store this
previousEvent
inside of this object
- you should return an object in this
- on
onError
, callqueryClient.setQueryData()
again,- to again manually update the stored data for this query with this key
['events', params.id]
- but now, set it back to that old event
previousEvent
which was previously stored withcontext.previousEvent
- to again manually update the stored data for this query with this key
- this property wants a function which will be executed if this
- you also need to get the old data & store it somewhere so that you can roll back to that old data
- make sure that whenever this mutation finished you still fetch the latest data from the backend and the data are always into sync by forcing React Query to refetch the data behind the scenes
- add a
onSettled
property to thisuseMutation()
configuration object - this property also wants a function as a value
- this function will be called whenever this
mutationFn: updateEvent
function is done no matter if it failed or succeeded - in that case, to be sure that you got the same data in your frontend as you have in your backend,
- you should use
queryClient.invalidateQueries(['events'], params.id)
- to invalidate all the
events
queries that use this specificparams.id
- you should use
- add a
It would be better if we would only see some events below "Recently added events" instead of all events
- in the backend
app.js
file, the backend code supports already this feature thanks to themax
query parameter inside the get/events
route- so you must set such a
max
query parameter on that ongoing URL to limit the number of items you're retrieving & to get the most recent items - therefore, in the frontend
http.js
, you have to tweak thisfetchEvents()
function- pull out a
max
property - tweak the URL depending on whether
max
&searchTerm
are set, or one of the two, or none of them
- pull out a
- so you must set such a
- in
NewEventsSection.jsx
, tweak that query so that it doesn't fetch all events but instead just some events- set this
max
property in thequeryFn: fetchEvents
function & abortsignal
- update the
queryKey
by adding to it{max: 3}
so you have a dedicated query key for this query (like you did inFindEventSection.jsx
with thesearchTerm
) - but, you can also use an alternative approach to avoid repetition which is
- passing
queryKey
as an argument to thequeryFn
anonymous function - and using the spread operator as an argument of
fetchEvents()
to spread the object{max: 3}
from thequeryKey
property, like this...queryKey[1]
- passing
- set this
- use this alternative approach in
FindEventSection.jsx
for thesearchTerm
- As you can see in
App.jsx
, this application uses React Router, and specifically, a version of React Router that has built-in data fetching (loader
) & data mutation (action
) capabilities so to say - you can also combine those React Router features with React Query, for example at the
EventEdit
route
- in
EditEvent.jsx
, export aloader()
function so that you can use it to tell React Router to execute the code in this function before it loads and renders this component and then allows you to fetch data before the component even appears on the screen- to do that with React query, get access to the
queryClient
because now you won't load data through theuseQuery()
hook, but instead, since you are outside of a component function, directly with help of thatqueryClient.fetchQuery
method that can be used to trigger a query programmatically, so without using theuseQuery()
hook- it takes the same configuration as
useQuery()
, soqueryKey
&queryFn
- the
queryFn
should be the same as the one you have above in theuseQuery()
- this
loader()
function receives an object which includes aparams
property which gives access to the route parameters of this active route
- it takes the same configuration as
- return this
queryClient.fetchQuery
so that theloader
gets this promise returned byfetchQuery()
& waits for that promise to resolve before React Router goeas ahead and renders the component - you could think that you should now remove
useQuery()
from that component because you're using React Router now, but this is actually not the case, because whilst you could useuseLoaderData()
(a hook provided by React Router), it is better to keepuseQuery()
around, because when you usefetchQuery()
in theloader
, React Query will go ahead and send that request and will then store that response data in the cache. Therefore, whenuseQuery()
is executed again in the component, it's this cached data that will be used - the only thing you should get rid of is this
isPending
state because you don't need this<LoadingIndicator>
now - in
App.js
, import thisloader
function & connect it to this<EditEvent>
route
- to do that with React query, get access to the
- when you want to use React Router, you are not limited to fetching data, instead you can also use it for editing data, for performing mutations
- back in
EditEvent.jsx
, export anaction()
async function which will be triggered by React Router when a form on this page is submitted- in this function, you will get an object passed in automatically by React Router that will have a
request
& aparams
property - inside this function, you can extract the
formData
that was submitted & get theupdatedEventData
with help ofObject.fromEntries(formData)
- send this
updatedEventData
to the backend by directly callingupdateEvent()
imported fromhttp.js
without creating a mutation where you will pass to it an object where you set{ id: params.id, event: updatedEventData }
& await it to only continue once this process is completed - invalidate all queries with
queryClient.invalidateQueries(['events'])
to make sure that the updated data is fetched again, but with this alternative approach you will not perform optimistic updating anymore - return a
redirect()
- in this function, you will get an object passed in automatically by React Router that will have a
- tweak the component
- in
handleSubmit
, make sure that you no longer callmutate
&navigate()
- instead, make sure that the form is submitted so that this
action()
function is triggered, as React Router requires it- submit a form programmatically by using the
useSubmit()
hook provided by React Router which gives asubmit()
function - use this
submit()
function inhandleSubmit()
instead ofmutate
&navigate()
to submit this form - configure this
submit()
function & set to it{method: 'PUT'}
- this
submit()
function doesn't send an HTTP request, but only trigger this client-sideaction()
function - so now, you can remove the
useMutation()
code
- submit a form programmatically by using the
- in
- go back to
App.jsx
& import thisaction()
function & assign it to the<EditEvent>
route
- back in
- in
EditEvent.jsx
, use theuseNavigation()
provided by React Router hook to get the user some feedback that the request is on its way - in
Header.jsx
, use theuseIsFetching()
hook provided by React Query to show some feedback with a progress bar when fetching or sending data - in
EditEvent.jsx
, avoid redundant HTTP requests when using React Router in conjunction with React Query by using thestaleTime
property in theuseQuery()
function