React Query brings the ergonomics of GraphQL clients to RESTful APIs. It scratched my itch.
Getting a whole company onto a new way of writing the API layer is hard and there's real work to do. React Query offers the perfect middle ground.
Keep the RESTful API you're used to, get the request deduping, data caching, loading states, re-fetching, and hooks ergonomics you dream of. 😍
You can use those same ergonomics for any slow operation. Doesn't have to be an API call.
PSA: you can use React Query for heavy client-side computation. It doesn't have to be an API call.
— Swizec Teller (@Swizec) November 5, 2021
and yes it makes your life easier
A slow client-side operation
Say you're parsing a CSV client-side.
You could run a file upload, parse and validate on the server, respond with any errors, and tell the user what happened. That's the usual approach and it's okay.
A better approach is to use a library like Papa Parse. Load file into browser memory or use a link, parse and validate, show the user what happened, ask for confirmation, send parsed JSON to your API.
Here's an example that parses and displays Taylor Swift Spofity data from Kaggle.
We're using a trick to load the CSV – Webpack imports it as a data URL and fetch()
parses that data into a File format. This is okay for a demo, but bloats your bundle size. You should use client-side file reading for a serious project.
Writing an article about using React Query for client-side computation and something clicked:
— Swizec Teller (@Swizec) November 6, 2021
you can use fetch() to parse data urls 🤯
no HTTP request pic.twitter.com/1cjX8tOjqf
Do thing, set state, yawn
You have to write your bazillionth state setter. Boilerplate you could write in your sleep, if it wasn't so easy to write a bug.
const [taylorSwiftSongs, setTaylorSwiftSongs] = useState([])
async function parseCSV() {
const file = await dataURLToFile(csv, "spotify_taylorswift.csv")
// parse the file
Papa.parse(file, {
header: true,
complete: (data) =>
setTaylorSwiftSongs(
// sort by popularity
data.data.sort((a, b) => Number(b.popularity) - Number(a.popularity))
),
})
}
// ...
;<Button onClick={parseCSV}>Click Me</Button>
- Define a new state that starts with an empty array
- create a function to call on user action
- run the operation (Papa.parse + sort for us)
- set state
No wonder people are eyeing Svelte where a compiler writes much of this code for you. 😅
Render the data
Because this is a demo, we show a list of Taylor Swift songs by popularity. Your project would do something super cool here.
{
taylorSwiftSongs ? (
<ol>
{taylorSwiftSongs.map((song) => (
<li key={song.id}>
{song.name} – {song.album}
</li>
))}
</ol>
) : null
}
If songs are loaded, iterate through and render a list. Otherwise show nothing.
What about loading states?
Parsing our CSV is pretty fast on a modern computer. You almost don't have to think about web performance these days.
But we're treating it as a slow operation. Would be nice if the user knew their click didn't fall into the void.
There is a small delay when you click the button:
Okay it's very small. Remember when I said a large CSV would bloat bundle size? Pretend this is a 200MB monster that the user imported instead.
To add a loading state, you need to write yet more boilerplate.
First, you add a loading state to your button:
import Loading from "./loading.gif"
function Button({ loading, onClick, children }) {
return (
<button
onClick={onClick}
style={{ padding: "0.5rem 1rem" }}
disabled={loading}
>
{loading ? <img src={Loading} alt="loading" /> : children}
</button>
)
}
Shows a loading gif and disables the button when loading=true
. You'll need this part no matter what.
Then you set the loading state while your slow operation runs:
const [taylorSwiftSongs, setTaylorSwiftSongs] = useState([])
const [loadingSongs, setLoadingSongs] = useState(false)
async function parseCSV() {
setLoadingSongs(true)
const file = await dataURLToFile(csv, "spotify_taylorswift.csv")
// parse the file
Papa.parse(file, {
header: true,
complete: (data) => {
setTaylorSwiftSongs(
// sort by popularity
data.data.sort((a, b) => Number(b.popularity) - Number(a.popularity))
)
setLoadingSongs(false)
},
})
}
And you find out that modern computers are really fast. A gif doesn't have enough frames to show the flash of loading state 😂
But if you throttle CPU in devtools, now we're talking.
PS: where's the error handling? What if the CSV fails? 🤔
Simplify your code with React Query
Writing all that boilerplate was tedious. Instead, we can use React Query.
const taylorSwiftSongs = useMutation(async () => {
const file = await dataURLToFile(csv, "spotify_taylorswift.csv")
// we need to wrap Papa in a promise
return new Promise((resolve, reject) => {
// parse the file
Papa.parse(file, {
header: true,
complete: (data) => {
const sorted = data.data.sort(
(a, b) => Number(b.popularity) - Number(a.popularity)
)
resolve(sorted)
},
})
})
})
We kept the same core of our code – turn CSV into a file, ask Papa to parse.
Because Papa is an older library, we had to wrap the code in a promise. Makes it work better with async/await.
But look there's no boilerplate!
No fiddly states, no loading, and we even get error handling. It's all in the taylorSwiftSongs
object.
You call taylorSwiftSongs.mutateAsync()
and React Query handles the rest. taylorSwiftSongs.data
will hold your data (whatever your async method returns), taylorSwiftSongs.isLoading
tells you loading states, taylorSwiftSongs.isError
for errors, ...
It's a mutation because we want to run this code when users click a button. useQuery
for when you want to replace an awkward useEffect
that runs on mount. ✌️
Okay but that code was fast
You are right, that code was fast. How about we try to find all factors of a number instead? Using the simplest algorithm possible.
async function getFactors(number) {
return new Promise((resolve) => {
setTimeout(() => {
// the algorithm
const factors = []
for (let i = 2; i < number; i++)
if (number % i === 0) {
factors.push(i)
}
resolve(factors)
}, 0)
})
}
We wrap the algorithm in a Promise and timeout so our calculation doesn't block the UI. Even better would be moving this to a Web Worker, but that's an article for another day.
Once you have a slow function, useQuery
is the easiest way to hold it.
function Factors({ number }) {
const factors = useQuery(["factors", number], async () => {
return getFactors(number)
})
if (factors.isLoading) {
return <img src={Loading} alt="loading" />
} else if (factors.isError) {
return <p>Oh no something went wrong</p>
} else if (factors.data) {
return (
<p>
{number} has {factors.data.length} factors, they are [
{factors.data.join(", ")}]
</p>
)
}
}
We set the cache key to ["factors", number]
and use getFactors
in the query function. Then we let React Query handle our loading and error states.
The end result is a neat UI that caches slow computations for reuse. ✌️
Try it for yourself. Make sure you don't use crazy numbers because your browser will hang.
Cheers,
~Swizec
PS: in the future, React useTransition will make this pattern a first-class citizen
Continue reading about You can use React Query for slow computation, not just API
Semantically similar articles hand-picked by GPT-4
- How React Query gives you almost everything you thought you needed GraphQL for
- Async React with NextJS 13
- React 18 and the future of async data
- Prefetch data with React Query and NextJS – CodeWithSwiz 8, 9
- How GraphQL blows REST out of the water
Learned something new? Want to become a React expert?
Learning from tutorials is easy. You follow some steps, learn a smol lesson, and feel like you got dis 💪
Then comes the interview, a real world problem, or a question from the boss. Your mind goes blank. Shit, how does this work again ...
Happens to everyone. Building is harder than recognizing and the real world is a mess! Nothing fits in neat little boxes you learned about in tutorials.
That's where my emails come in – lessons from experience. Building production software :)
Leave your email and get the React email Series - a series of curated essays on building with React. Borne from experience, seasoned with over 12+ years of hands-on practice building the engineering side of growing SaaS companies.
Get Curated React Essays
Get a series of curated essays on React. Lessons and insights from building software for production. No bullshit.
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️