Client stories: React App Groundwork

Laying the foundation for an internal-use cloud tool

Tags: Client stories


I have another client story to share today. This was actually my first project as Matthew Cardarelli Solutions, which I completed back in October of this year. This client has requested confidentiality, but they authorized me to discuss the project in generic terms, which I greatly appreciate.

Any and all quotes, excerpts, and code snippets you read here have been modified to prevent disclosure of confidential client information.

The situation

This client needed to replace a cloud software solution they were paying to use. Instead of buying a new tool, they chose to invest in their own internal application. The application’s primary purpose is to perform scheduled data imports and exports. This process is executed by a server-side service connected to a relational database. The service exposes configuration operations via a REST API, and a web interface allows authorized users to easily configure and administer the system.

The client already contracted a pair of developers to build the back-end service, but they needed someone with front-end experience to build the web interface.

The pitch

During our initial consultation, I learned about the nature of the business problem, the vision for the solution, and the planned technology stack. When the client mentioned that they had bootstrapped the user interface with React, I knew I was the right developer for the job. While I consider myself a full-stack web developer, I spent most of my last full-time role building and maintaining React-based web browser interfaces, and more specifically, building table-and-form administration workflows. My skillset was a great match for their needs.

Donning my project manager cap, I identified the most essential pages and features discussed during the consultation, translated it into a list of acceptance criteria statements, and composed a proposal document.

Here is a sample of the acceptance criteria, which includes some of the more generic features that one would expect from any web interface. The phrasing of the first sentence is intended to keep the focus on the client outcomes that will result from my work, rather than the work to be done.

The Developer is responsible for ensuring that the web application meets the following acceptance criteria:

  • Users can attempt to log in to the application.
  • Users can successfully log in and receive an access token.
  • The user is prompted if their attempt to log in results in an error.
  • etc…

In addition to the requirements, my proposal included an estimated timeline and a fixed price quote. Together, these three elements communicate a clear value proposition, which simplifies a client’s cost-benefit analysis. My proposal is a success if a client can easily ask themselves, “In approximately X days/weeks/months, progress towards my business goals will be furthered by A, B, and C. Is that worth the fee of Y dollars to me?”

In this case, my client agreed to the initial proposal, without a need for further negotiation. That is a win-win for both of us! The “starter project” has quickly become my preferred model for all new clients, because it’s low risk to them, and allows me to demonstrate my competence far better than a resume or even a portfolio can.

Dev environment setup

I wasn’t quite starting from square one. As I mentioned, the other contractors had configured a very basic React app starter. In order to test the project end-to-end, I also needed to run the API server locally, which meant setting up a database.

It was clear to me already that, as this was an enterprise cloud application, I would not be the only developer working in this repository forever. Therefore, as I set up my environment I made small contributions to simplify new contributor onboarding, without hiding everything behind some magical do-it-all setup script.

(as it turns out, this benefitted me directly when I purchased a new work laptop and needed to set up the repository again)

Locking down the node version

For collaborative projects, it’s important to maintain consistency between local and production environments, as well as different contributor’s development environments. To that end, I added an .nvmrc file to the repo and added a few lines to the README reminding developers to set up a version manager for their node runtime. My tool of choice these days for node version management is actually asdf , rather than nvm. I prefer asdf for a couple reasons:

  • It’s easier and less invasive to install and uninstall.
  • Its plugin architecture supports many languages, not just nodejs.

Containing the database

Up until recently, when I needed a server-side package such as Nginx or PostgreSQL, I’d simply use my package manager to install whatever version my operating system currently supported. This of course led to conflicts when different projects called for different versions of a system package. As an independent developer working with multiple clients, I realized this problem would only grow over time. I took this opportunity to finally learn some container basics.

Instead of installing the database directly to my local machine, I setup Podman on my machine, pulled the official Docker image, and booted up a container. I mounted the folder containing the database seeding scripts so seed the database with test data with a couple quick commands. And, of course, I documented all the commands in the README in case other developers wanted to follow the same strategy.

Project highlights

The scope of work for this project required me to:

  • Test the UI side of the user login functionality, and fix it if it isn’t working.
  • Allow logged-in users to editing details about themselves, such as their name and email.
  • Implement a logout button.
  • Build a “list view” page for each of the primary resource types accessible via the API.
  • Set up automated testing infrastructure for local development.

The actual development of the React application turned out to be relatively straightforward. The starter was incorporated the Material UI component library (MUI), which did a lot of heavy lifting for me. It did take me a while to get familiar with MUI’s patterns and best practices for customizing components, but once I got the hang of things my development pace sped up substantially.

The purpose of this blog post is not to bore you with day-to-day technical minutia. Instead of a step-by-step, I will summarize important aspects of my approach, including the “why” behind my actions. Here are some of the highlights of my work.

Make it work, then make it right

I tend to be a bit of a perfectionist. This can be both a boon and a curse when developing software. One habit I’ve worked hard to break is the temptation to implement each feature pixel-perfect before moving on to the next. For this project, falling prey to perfectionism could put the timeline, and my reputation, at risk. To change my behavior, I chose a motto to repeat in my head: “Make it work, then make it right.”

In practice, I implemented the bare miniumum necessary to check off each of the acceptance criteria included in the proposal. Once a feature functioned, however ugly and fragile, I’d move on to the next feature. Only after I completed the critical feature set, did I go back to refine and improve the code.

This approach proved highly successful. By focusing on the absolute essentials first, I immediately eliminated many potential unknowns, including which of my planned approaches would work and which I would need to reconsider. I also surfaced many questions to the API developers early on, giving them plenty of time to respond or make changes. Best of all, as I neared the end of the project timeline, the work that might need to be cut consisted of small “nice-to-haves”, rather than major “must-haves.”

Flattening the document hierarchy

The existing UI code relied heavily on the generic <Box> MUI component to manage layout and styling. While simple to apply, this approach has drawbacks, such as:

  • Verbose markup that can be hard for a developer to parse.
  • A lack of semantics in the document structure.

I took a different approach for the new code I wrote, as well as the refactors I performed on existing components:

  • I relied on styling components directly where possible, rather than wrapping them in a <Box>.
  • I used the MUI <Stack> and <Grid> components when I needed a flexbox or grid layout, respectively.

I believe this approach results in more legible code, and a more semantically rich application. Though I suppose I am a bit biased!

Fixing the router

The app used React Router for navigation between pages, but the back-end developer who set up the starter mentioned that they couldn’t get it working, though they didn’t have much time to do so. It didn’t take me long to discover the issue. The page header component was rendering outside of the <RouterProvider>. Thus, it couldn’t actually message the provider when a navigation event occurred. It took a bit of refactoring, but I managed to move the header under the router provider without triggering unnecessary re-rendering.

To aid new contributors, I updated the README with instructions for how to create a new route.

Trimming the third-party package web

I have a strong disdain for most project “starter” repositories. create-react-app is the primary offender. My main complaint is that, instead of teaching you how to quickly assemble your tools and development environment, they act as a magical black box that “just works.” As a consequence, it’s painful and sometimes nearly impossible to change how things work when your project outgrows the defaults.

They also tend to bundle a lot of third-party libraries you may or may not need. Dependencies are expensive additions to your project. Yes, they save you time reinventing the wheel, but each dependency needs to be monitored for security patches and maintainer abandonment. Plus, unused dependencies bloat the development environment and slow down fresh installations.

This project used a starter created by one of the back-end dev’s colleagues, and honestly, it wasn’t so bad. I ended up keeping most of the packages, and I trimmed a dozen or so. I also swapped out the moment library for the more actively developed date-fns.

Declarative API data fetching

Given the RESTful nature of the API, and the dynamic nature of the user interface I was building, I imported Vercel’s swr library to implement API fetching. I used SWR at my last job, but not directly. The hooks was hidden under abstraction layers that provided impressive type safety for a large organization, at the cost of obfuscating the behaviors and advantages of the SWR library. Using SWR directly for this project gave me new appreciation for its power and versatility.

I organized the fetch hooks in a subfolder called src/hooks/api. Each hook corresponded to a specific API endpoint. Thanks to SWR’s global caching mechanisms, I could pull these hooks in wherever I needed the data from an API call, and not worry about duplicating the same request.

What I found most interesting was how, at least for now, I could manage a user’s authenticated state (logged in or logged out) without needing to use React context. Instead, I defined an SWR hook for the API endpoint responsible for returning the logged in user’s data, and extended the polling interval for refetching. Anywhere in the app that needed the user’s authenticated state or logged in user data could simply pull in the hook. When a user performed an explicit log in or log out, I used SWR’s built in mutation methods to force a cache revalidation.

Here’s a look at the user authentication hook:

// @ts-nocheck

interface AuthenticatedUserResponseBody {
  username: string;
  authInstant: string; // datetime stamp of the instant the user logged in
  userInfo: {
    firstName: string;
    lastName: string;
    email?: string;
  };
  roles: string[];
}

type AuthenticationState =
  | {
      isAuthenticated: false;
    }
  | {
      isAuthenticated: true;
      authInstant: Date;
      permissions: Record<string, string>;
    };

export default function useAuthenticationDetails() {
  const {
    data = { isAuthenticated: false },
    isValidating,
    isLoading,
    mutate,
  } = useSWR<AuthenticationState>(
    '/api/session',
    async (key: '/api/session') => {
      const response = await fetch(key);

      // Assume any failure means the user isn't authenticated
      if (response.status !== 200) {
        return { isAuthenticated: false };
      }

      // Parse the body and return data in a format meaningful to the UI.
      const body: AuthenticatedUserResponseBody = await response.json();
      const result: AuthenticationState = {
        isAuthenticated: true,
        authInstant: parseISODate(body.authInstant),
        permissions: extractPermissions(body.roles),
      };
      return result;
    },
    {
      // Since this is used in many places, we don't want it to refetch too often.
      dedupingInterval: 30000,
    },
  );

  return {
    authState: data,
    isLoading, // true while first fetch is in-flight
    isValidating, // true whenever a fetch or refetch is in-flight

    // Used by login component to fetch user session after successful authentication attempt
    revalidateAuthStatus: () => mutate(),
    // Used by logout component to clear state after successful session logout
    clearAuthStatus: () => mutate({ isAuthenticated: false }),
  };
}

Robust automated testing

I ended up configuring four kinds of automated testing:

  • Cypress end-to-end testing
  • Cypress component testing
  • Vitest unit testing
  • Lighthouse performance and accessibility auditing

For each test method, I added instructions in the README for running the tests, as well as what to test with each tool and how to add a new test. I also added at least one useful end-to-end, component, and unit test that would double as an example.

Areas of improvement: performance and accessibility

I certainly did not neglect accessbility for this project, but I could have done better. My unfamiliarity with Material UI components left me scratching my head when I was performing screen-reader testing, as I couldn’t understand why certain interactions weren’t working as I expected. I also haven’t yet invested the time to really understand the Web Content Accessbility Guidelines , nor am I familiar with the tools available to assist me in testing sites for accessibility.

On a similar note, I have to admit that while I had no problem getting Lighthouse audits working, I had very little understanding of how to interpret the metrics being displayed. I need to make some time for myself to learn performance engineering concepts so I can apply them to the development projects I’m working on.

Final takewaway

I felt great about the results of this project, and so did my client. They have since contracted with me to continue to develop the user interface. I’m also glad to be involved in collaborative development again. It’s been great working together with other capable developers. I’m looking forward to delivering more value for this client in the near future!

Need an experienced developer to deliver innovative web solutions? Contact me today so we can schedule your free consultation.


Have questions or comments about this blog post? You can share your thoughts with me via email at blog@matthewcardarelli.com , or you can join the conversation on LinkedIn .