-
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.jsxand get rid of the state management code & the useEffect() code - import the
{useQuery}hook from@tanstack/react-query - use this
useQueryhook inside theNewEventsSectioncomponent to send a HTTP request behind the scenes, etc - you must configure it by adding inside of it an object with:
- first step:
- a
queryFnproperty 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 thequeryFnproperty
- a
- second step:
- a
queryKeyproperty - 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
useQueryfrom where you can pull out the elements you need, like:- the
dataproperty which holds the actual response data as a value, so the data that is returned byfetchEvents() - the
isPendingproperty - the
isErrorproperty - the
errorproperty - & more
- the
- use now
isPendinginstead of the oldisLoadingstate to show theLoadingIndicatorwhilst you're waiting for a response - check for
isErrorinstead of the olderrorused with the state management to show theErrorBlock - and in that
ErrorBlockinstead of just hardcoding themessage, use theerrorobject & itsinfo& itsmessage - output the
dataif we did successfully fetch the events - in
App.js, wrap the components that use React Query withQueryClientProvider&QueryClientimported from@tanstack/react-query
- cut the
- you can control the behaviour of React Query, for example by setting a
staleTimeon 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 usedatafrom 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
gcTimeproperty- this controls how long the
datain the cache will be kept around - the default value is 5 minutes
- this controls how long the
- in
src\components\Events\FindEventSection.jsximport{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.jsxand configure theuseQueryobject & manage some state to update the query when thesearchTermchanges - get back the object from
useQuery()& in there get back thedata,isPending,isError&error - use this pieces of information to dynamically & conditionally output the
contentin 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
signalneeded 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
signalthen pass thissignalto 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
signalfromhttp.jsvia asignalobject 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 theenabledproperty- initially, show no events
- but, if a user searched something then clear the input, show all the events
- the
searchTermset should be initially undefined by not passing any value at all touseState() - set
searchTerm !== undefinedas a value ofenabled- so if if
searchTermisundefined, 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
isLoadinginstead ofisPendingto 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 theuseMutationhook 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
handleSubmitfunction
- use
useMutation()& configure its object:- set a
mutationFnfunction - for that, add a new
createNewEventfunction inhttp.js - set this function as a value for the
mutationFnproperty
- set a
useMutationreturns an object that you can destructure to pull out some useful properties, like the:mutateproperty which is a function that you can call anywhere in this component to send this requestisPendingpropertyisErrorpropertyerrorproperty
- therefore, inside the
handleSumbit()function, callmutate()& pass theformDatato it as a value - handle errors & show some loading text whilst the request is on its way with help of the returned
useMutationobject 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 newfetchSelectableImagesfunction - in
EventForm.jsx, set thisfetchSelectableImagesfunction as a value for thequeryFnproperty - set a value of
['events-images']for thequeryKeyproperty - use that object returned by
useQuery()to get holddata,isPending&isError - set the
data(so, that list of images) as a value for theimagesprop on the<ImagePicker>component - show conditionally the list of images if we have
data - show a loading text if
isPendingis true - show an the
<ErrorBlock>component ifisErroris 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
onSuccessproperty to theuseQuery()configuration object - it wants a function as a value that will be executed once this
mutationFnsucceeded - 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, thedatain 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 theQueryClientobject, you will force this invalidation of a query - therefore, cut
QueryClient& add it inhttp.jsso that you can import it from multiple files - now, in
App.jsx, import thisqueryClientconstant fromhttp.js - now, in
NewEvent.jsx, in theonSuccessmethod before navigating away, callqueryClient.invalidateQueries() - to target specific queries,
invalidateQueries()takes an object as an input where you have to define thequeryKeywhich you want to target - set
queryKey: ['events']to invalidate all queries that include theeventskey even if it is not exactly the same key (if you don't useexact: true)
- update the
util/http.jsfile 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
EventDetailscomponent via React Router'suseParamshook
- make the delete button work by using the
deleteEvent()function together with React Query'suseMutation()inEventDetails.jsxso 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 theEventDetailscomponent function - configure the query by adding:
queryFnwhich hasfetchEvent()as a value & get access to theidwithuseParams()& pass it to the functionqueryKeywhich has['events', params.id]as a value
- get the object from
useQuery()& pull out from itdata,isPending,isError&errorproperties - use this pieces of data to output different content on the screen depending on the current state of this query
- formate the
datein a nice way
- in
- use ReactQuery's
useMutation()hook to delete data:- in
EventDetails.jsx, importuseMutation()& execute it in theEventDetailscomponent function - configure the query by adding
mutationFn& settingdeleteEventto it as a value - get the object from
useMutation()& pull out from it themutateproperty - trigger the
mutate()fonction when thedeletebutton is pressed by callig it inside ahandleDelete()function- pass an object to
mutate() - add an
idproperty which has the id of that to be deleted event (params.id) as a value
- pass an object to
- connect the
handleDelete()function to thisdeletebutton with help of theonClickprop - define what should happen after the mutation succeeds
- add the
onSuccessproperty 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
refetchTypeproperty 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
isDeletingstate with theuseState()hook initially set tofalsethat tells us whether the user started the deletion process or not - change this to
trueonce the user clicks thisDeletebutton - 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 setsetIsDeletingtotrue - add a new
handleStopDelete()function in which you setsetIsDeletingtotrue(if the user cancels this delete process) - connect
handleStartDeleteto theDeletebutton in this UI instead ofhandleDelete - but, now this UI should also contain another component that can be displayed
- in your
returnstatement, add the<Modal>component - inside this component, show some confirmation text and
Cancel&Deletebuttons - this
Deletebutton, when is clicked, should trigger thehandleDeletefunction you used before to delete the event - this
Cancelbutton should trigger thehandleStopDeletefunction to stop the deletion mutation & close this modal again - because this modal should be display conditionally if
isDeletingistrue - this
<Modal>component takes anonCloseprop which triggershandleStopDeletewhen 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
useMutationobject, pull out:isPendingproperty & set it toisPendingDeletionto avoid a name clashisError& name itisErrorDeletingerror& 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
Editbutton from theEventDetailspage 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 theinputDataprop 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
datato prepopulate this form by settinginputData={data} - use
isPendingto show a loading indicator (LoadingIndicator) if we're still waiting for a response - use
isError&errorto show an error block (<ErrorBlock>) if we get an error - use
datato 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.jsfile which now includes anupdateEventfunction - 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 theupdateEventfunction from inside thehandleSubmit() - in
mutate(), pass this object with the to be forwarded data toupdateEventto thismutate()function- an object that has
id&eventproperties - 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
Updatebutton, 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 newonMutateproperty 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
queryClientfromhttp.jsto change the cached data (and not to invalidate queries this time) - inside the
onMutatefunction, 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
datayou want to store under that query key, which is, in this case, that updated event dataformDatawhich you also sent to your backend that you can store in anewEventconstant
- 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
previousEventconstant
- do that before you update the data (
- roll back to
prevousEventif your update mutation failed by adding a newonErrorproperty to thisuseMutation()configuration object- this property wants a function which will be executed if this
mutationFn: updateEventfunction fails - it receives a couple of inputs that are passed in automatically by React Query
error,data&contextobjects- this
contextobject can contain thispreviousEvent
- in order for this
previousEventto be part of thiscontext,- you should return an object in this
onMutatefunction, because this object will be thiscontext - and store this
previousEventinside 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
previousEventwhich 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
onSettledproperty to thisuseMutation()configuration object - this property also wants a function as a value
- this function will be called whenever this
mutationFn: updateEventfunction 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
eventsqueries 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.jsfile, the backend code supports already this feature thanks to themaxquery parameter inside the get/eventsroute- so you must set such a
maxquery 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
maxproperty - tweak the URL depending on whether
max&searchTermare 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
maxproperty in thequeryFn: fetchEventsfunction & abortsignal - update the
queryKeyby adding to it{max: 3}so you have a dedicated query key for this query (like you did inFindEventSection.jsxwith thesearchTerm) - but, you can also use an alternative approach to avoid repetition which is
- passing
queryKeyas an argument to thequeryFnanonymous function - and using the spread operator as an argument of
fetchEvents()to spread the object{max: 3}from thequeryKeyproperty, like this...queryKey[1]
- passing
- set this
- use this alternative approach in
FindEventSection.jsxfor 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
EventEditroute
- 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
queryClientbecause now you won't load data through theuseQuery()hook, but instead, since you are outside of a component function, directly with help of thatqueryClient.fetchQuerymethod that can be used to trigger a query programmatically, so without using theuseQuery()hook- it takes the same configuration as
useQuery(), soqueryKey&queryFn - the
queryFnshould be the same as the one you have above in theuseQuery() - this
loader()function receives an object which includes aparamsproperty which gives access to the route parameters of this active route
- it takes the same configuration as
- return this
queryClient.fetchQueryso that theloadergets 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
isPendingstate because you don't need this<LoadingIndicator>now - in
App.js, import thisloaderfunction & 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& aparamsproperty - inside this function, you can extract the
formDatathat was submitted & get theupdatedEventDatawith help ofObject.fromEntries(formData) - send this
updatedEventDatato the backend by directly callingupdateEvent()imported fromhttp.jswithout 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 thestaleTimeproperty in theuseQuery()function