Swizec Teller - a geek with a hatswizec.com

    You can use React Query for slow computation, not just API

    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.

    Swizec Teller published ServerlessHandbook.dev avatarSwizec Teller published ServerlessHandbook.dev@Swizec
    PSA: you can use React Query for heavy client-side computation. It doesn't have to be an API call.

    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.

    Swizec Teller published ServerlessHandbook.dev avatarSwizec Teller published ServerlessHandbook.dev@Swizec
    Writing an article about using React Query for client-side computation and something clicked:

    you can use fetch() to parse data urls ๐Ÿคฏ
    no HTTP request
    Tweet media

    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>
    1. Define a new state that starts with an empty array
    2. create a function to call on user action
    3. run the operation (Papa.parse + sort for us)
    4. 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:

    Smol delay

    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 ๐Ÿ˜‚

    Modern computers are too fast for this demo

    But if you throttle CPU in devtools, now we're talking.

    Loading state with CPU slowdown

    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. โœŒ๏ธ

    useQuery for slow computation

    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

    Did you enjoy this article?

    Published on November 8th, 2021 in React, Frontend, React Query

    Learned something new?
    Want to become an expert?

    Here's how it works ๐Ÿ‘‡

    Leave your email and I'll send you thoughtfully written emails every week about React, JavaScript, and your career. Lessons learned over 20 years in the industry working with companies ranging from tiny startups to Fortune5 behemoths.

    Join Swizec's Newsletter

    And get thoughtful letters ๐Ÿ’Œ on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.

    "Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. ๐Ÿ‘Œ"

    ~ Ashish Kumar

    Join over 14,000 engineers just like you already improving their careers with my letters, workshops, courses, and talks. โœŒ๏ธ

    Have a burning question that you think I can answer?ย I don't have all of the answers, but I have some! Hit me up on twitter or book a 30min ama for in-depth help.

    Ready to Stop copy pasting D3 examples and create data visualizations of your own? ย Learn how to build scalable dataviz components your whole team can understand with React for Data Visualization

    Curious about Serverless and the modern backend? Check out Serverless Handbook, modern backend for the frontend engineer.

    Ready to learn how it all fits together and build a modern webapp from scratch? Learn how to launch a webapp and make your first ๐Ÿ’ฐ on the side with ServerlessReact.Dev

    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ย โค๏ธ

    Created by Swizec with โค๏ธ