Chris Padilla/Blog

My passion project! Posts spanning music, art, software, books, and more
You can follow by RSS! (What's RSS?) Full archive here.

    From the Other Side

    🌑

    We have bobcats and coyotes on the other side of the lake near our home. You can hear them at night. Every now and then, I see one looking back at me 🐺


    Optimistic UI in Next.js with SWR

    I remember the day I logged onto ye olde facebook after a layout change. A few groans later, what really blew me away was the immediacy of my comments on friends' posts. I was used to having to wait for a page refresh, but not anymore! Once I hit submit, I could see my comment right on the page with no wait time.

    That's the power of optimistic UI. Web applications maintain a high level of engagement and native feel by utilizing this pattern. While making an update to the page, the actual form submission is being sent off to the server in the background. Since this is more than likely going to succeed, it's safe to update the UI on the page.

    There are a few libraries that make this process a breeze in React. One option is Vercel's SWR, a React hook for data fetching.

    Data Fetching

    Say I have a component rendering data about several cats. At the top of my React component, I'll fetch the data with the useSWR hook:

    const {data, error, isLoading, mutate} = useSWR(['cats', queryArguments], () => fetchCats(args));

    If your familiar with TanStack Query (formerly React Query), this will look very familiar. (See my previous post on data fetching in React with TanStack Query for a comparison.)

    To the hook, we pass our key which will identify this result in the cache, then the function where we are fetching our data (a server action in Next), and optionally some options (left out above.)

    That returns to us our data from the fetch, errors if failed, and the current loading state. I'm also extracting a bound mutate method for when we want to revalidate the cache. We'll get to that in a moment.

    useSWRMutation

    Now that we have data, let's modify it. Next, I'm going to make use of the useSWRMutation hook to create a method for changing our data:

    const {mutate: insertCatMutation} = useMutation([`cats`, queryArguments], () => fetchCats(args)), {
            optimisticData: [generateNewCat(), ...(data],
            rollbackOnError: true,
            revalidate: true
        });

    Note that I'm using the same key to signal that this pertains to the same set of data in our cache.

    As you can see, we have an option that we can pass in for populating the cache with our optimistic data. Here, I've provided an array that manually adds the new item through the function generateNewCat(). This will add my new cat data to the front of the array and will show on the page immediately.

    I can then use the mutate function in any of my handlers:

    const {error: insertError} = await insertCatMutation(generateNewCat());

    Bound Mutate Function

    Another way of accomplishing this is with the mutate method that we get from useSWR. The main benefit is we now get to pass in options when calling the mutate method.

    const handleDeleteCat = async (id) => {
        try {
            // Call delete server action
            deleteCat({id});
            
            // Mutate the cache
            await mutate(() => fetchCats(queryArguments), {
                // We can also pass a function to `optimistiData`
                // removeCat will return the current cat data after removing the targeted cat data
                optimisticData: () => removeCat(id),
                rollbackOnError: true,
                revalidate: true
            })
        } catch (e) {
            // Here we'll want to manually handle errors
            handleError(e);
        }
    }

    This is advantageous in situations like deletion, where we want to sequentially pass in the current piece of data targeted for removal. That can then be passed both to our server action and updated optimistically through SWR.

    For even more context on using optimistic UI, you can find a great example in the SWR docs


    Antônio Carlos Jobim – Once I Loved... Continued!

    Listen on Youtube

    Finishing out this bossa 🏝️


    Waiting to Be Picked

    🎃

    To be someone's pumpkin...


    3 Types of Software Requirements

    This week I'm synthesizing notes from Michael Pogrebinsky's course on Software Architecture and Design. If you like what you read below, you'll love the course!


    When designing a system, especially at higher scales of complexity, it's vital to take the carpenters' motto to heart: "Measure twice, cut once."

    Smaller scale projects, such as feature requests for an already existing system, have very clear restraints. The programming language, platform, and infrastructure of the project have already been laid. It only takes a few questions to get clear on what needs to happen when adding a widget to an existing app.

    Larger solutions, however, can be daunting with the sheer vastness of options. Any software choice can solve any problem. And, in spite of what the discourse on forums may lead you to believe, there isn't one correct solution for any problem.

    So how do you narrow down your choices? Through requirement gathering!

    Requirements As Part of the Solution

    A paradigm shift for anyone pivoting from a small scale environment to thinking broadly is understanding that question asking is part of the solution.

    The client, sometimes not technical, will have an idea of the problem they want to solve, but will be unclear on the solution. Even if they have an idea of what solution they would like to see implemented, you as the technical authority in the room have to ask clarifying questions to find out if their suggestion will work in your system.

    Types of Requirements

    To help ensure time is spent asking the right questions, it's helpful to know that there are three types of requirements you can gather:

    • Functional Requirements: Features of the system. Think inputs and outputs. Listen for "the system must do this"
      • Generally, these do not determine the architecture. Any architecture can solve any problem.
    • Quality Attributes: Non-functional requirements. Deals with performance of the application. Listen for "the system must have".
      • Ex: Scalability, availability, reliability, performance, security. A more comprehensive list here.
      • These dictate the software architecture of our system.
    • System Constraints: Limits and boundaries.
      • Ex: Time, staffing, resources. Can also drive architecture design.

    Example: CD Dad

    Say you're working with a client that is looking to build out an e-commerce platform for musicians. A possible portion of requirements may include the following:

    "CD Dad will host albums from independent musicians. When a customer purchases an album, the customer will receive a digital download of the album and the musician will be paid."

    So far we've heard the Functional requirements. With given inputs, the system hosts the music. When a customer submits a purchase, they receive the digital downloads.

    "Processing uploads should be take no longer than 5 minutes. When an album is purchased, a link should be made available immediately through email."

    The statement above relates to performance of the app, so this is a quality attribute.

    "The system should support mp3, wav, and aif file formats. A team of a dozen full time engineers will be responsible for maintaining the system."

    Now we're talking file formats and engineering support, so we're looking at System Constraints.

    The Joy of Constraints

    For many engineers, an ideal world is one without constraints. However, limitation breeds creativity. A limit on resources, hands on deck, and time are what enables us to ship code regularly.

    In the event that any of this information is missing in your understanding of a system, it's an opportunity to gather more info and clarify what's being built. Doing so will help clear the fog for the next best step.