Part 6C of FSO is about TanStack Query and useContext (finally!)

TanStack Query

TanStack Query is another way to manage state data, except unlike Zustand or Redux, it is specifically created to manage states storing data retrieved from the server.

TanStack Query requires you to wrap the entire app in its QueryClientProvider wrapper, and also pass in a queryClient object created from the QueryClient() class.

import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.jsx'

const queryClient = new QueryClient()

createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)

Fetching data from the server is done with a fetch request wrapped in a useQuery function:

 const result = useQuery({
    queryKey: ['notes'],
    queryFn: async () => {
      const response = await fetch('http://localhost:3001/notes')
      if (!response.ok) {
        throw new Error('Failed to fetch notes')
      }
      return await response.json()
    }
  })

  const notes = result.data

Looks insane, but the useQuery function internally creates states to hold the data that was fetched and returns it, cutting out the need to use a useState and a useEffect to cascade render the page when the server responds with data.

For PUT/DELETE requests, useMutation() is used instead:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getNotes, createNote } from './requests'

const App = () => {
  const queryClient = useQueryClient()

  const newNoteMutation = useMutation({
    mutationFn: /* function that makes the http request here */,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['notes'] })
    }

  const addNote = async (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.reset()
    newNoteMutation.mutate({ content, important: true })
  })
}

useMutation() is a little strange, as it never knows which queryKey that it’s mutating, it simply shoots an async request. It is only with callback options like onSuccess where there can be methods called on queryClient to modify specific keys.

Using queryClient.invalidateQueries({ queryKey: ['notes'] }) causes TanStack Query to mark the query as stale and triggers a re-fetch whenever a new note is added. Which could be optimised away by directly writing to the cache instead like so:

 const newNoteMutation = useMutation({
    mutationFn: createNote,
    onSuccess: (newNote) => {
      const notes = queryClient.getQueryData(['notes'])
      queryClient.setQueryData(['notes'], notes.concat(newNote))
    }
  })

TanStack Query is rather flexible and powerful.

Context API

This is Facebook’s answer to prop drilling. Everything else I’ve noted so far (TanStack Query, Zustand, etc) are advanced state management tools, they weren’t necessarily made to fix prop drilling.

Here’s how it works:

import { createContext } from 'react'
const CounterContext = createContext()
export default CounterContext
import CounterContext from './components/CounterContext'

const App = () => {
  const [counter, setCounter] = useState(0)

  return (
    <CounterContext.Provider value={{counter, setCounter}}>
      <SomeComponent />
    </CounterContext.Provider>
  )
}

The Context API requires you to wrap the entire app in its context provider wrapper, and then pass in the states you wish to pass down into the inner components. This is very similar to TanStack Query, and that’s because TanStack Query uses the Context API to pass down states as well.

Then, in a child component, to extract states, it’s simple to just

import { useContext } from 'react'
import CounterContext from './CounterContext'

const Display = () => {
  const { counter } = useContext(CounterContext)

  return <div>{counter}</div>
}

Though, this means importing useContext and the created context object (in this case CounterContext) each time the child uses a state from the context. It’s easier to move it all into its own file:

import { useContext, createContext, useState } from 'react'

const CounterContext = createContext()
export const useCounter = () => useContext(CounterContext)

export const CounterContextProvider = (props) => {
  const [counter, setCounter] = useState(0)
  return (
    <CounterContext.Provider value={{counter, setCounter}}>
      {props.children}
    </CounterContext.Provider>
  )
}

Now children components can easily extract states like so:

const { counter, setCounter } = useCounter()

And the wrapper has been collapsed into this:

const App = () => {
  return (
    <CounterContextProvider>
      <SomeComponent />
    </CounterContextProvider>
  )
}

Context is surprisingly easy to grasp. Though it’s a ridiculous workaround for a ridiculous problem, ah well.

While Redux has been deprecated from the Full Stack Open course, it’s still available as part 6D. I will continue and learn anyway, as older corporations still utilize redux.


Previous Post Next Post