Site of James

Placing my knowledge on a web page

One Year of React Experience in Review

2020-10-22 Web Programming James

Recently, I passed my one-year anniversary of using React. It’s time to take some time to write a blog post about my experiences, and my transition into React and beyond from earlier tools. This will be a rather long post, so get some water.

Bronze Age Web Development

I’ve been no stranger to web development, even with my background as a native-code backend developer. I used Flask with Python, JQuery, Bootstrap, and Apache to put together some REST-API-consuming side projects, but it was never my main focus.

Even though this was around 2016, what I made (just look at the stack) would be considered pretty old school even then. In college (2015), I used PHP and basic MySQL as my first web tools for projects because I didn’t know anything else was really out there. It was a dark time.

Enter Vue and the Modern Age

My first experience into “new-age” reactive web frameworks was Vue at my previous startup, Hyperion Data. With my other cofounder, we built two applications, a desktop-based dashboard, and a phone-based PWA, both interacting with Google Firebase. Naturally in the beginning I had trouble understanding how to make things reactive and my code was very naive, and I can’t say I grasped the core concepts of Vue while working with it.

But! It was there I had my first interactions with TypeScript over plain JavaScript, as well as JWTs. Noting the benefits and being a strong-proponent for strongly-typed code, this ultimately would translate to me bringing TypeScript to Ensurem in late 2019 when we started our company’s first React projects, and using JWTs for Rest API security.

Starting React

At Ensurem, the startup fire was starting to burn, and we were beginning to migrate legacy PHP tools (LAMP applicatons in 2019, I’m still surprised, and we’re still trying to migrate to this day). My manager approached me to building our first React project, which was a linear survey-like data-collection app.

I googled and started with Create React App, thankfully, and had a pretty good setup from the start instead of trying to something naive from scratch, which would have been a disaster.

First React App: Early Mistakes

Naturally, hindsight is 20-20, and it’s clear now I could have done better, but there was almost no way I was going to anticipate the problems I’m now painfully familiar with when I was just starting. But it’s good to look back. So let’s go:

The App TL;DR

  • Bootstrap
  • Originally no router
  • No state management library or use of context
  • Mutable data passed through all levels of app (🤮)
  • Written originally with highly-OOP classes, refactored over time
  • No pre-defined code structure
  • Used SCSS, but poorly

Trying to Make it into C

My biggest mistake was treating React like an object-oriented programming language and over-using inheritence.

My early code looked like this (which was before hooks):

interface LoraApplicatonBaseProps {
  userData: UserDetails;
  onComplete: (result: IResult) => void;
  onFailed: () => void;
}

class LoraApplicatonBase<TState> extends React.Component<
  LoraApplicatonBaseProps,
  TState
> {
  helpfulBaseClassMethod() {
    // ...
  }
}

/// Somewhere else...

interface DerivedClassDtcState {
  // ...
}

class DerivedClassDtc extends LoraApplicatonBase<DerivedClassDtcState> {
  render() {
    //...
  }
}

After all, to me with a C++, C#, and Go background, this seemed natural: I was tasked with building a reusable React library that would be used for other applications down the road, so why not try to define some base classes?

This might work in C#, but becomes awkward in TypeScript and React where the language is first of all more functional than object-oriented, and component composition is the way to compose applications.

It was hard to make component trees reactive. It was hard to navigate the self-imposed needless abstractions of layers of props and classes. It suffered from all the classic pitfalls of OO, which was exacerbated by trying to do these things in a functional language.

Thankfully, when hooks came along and I was more experienced, I slowly cleaned these inheritance structures up.

Being Afraid of Global Application State

AKA, All Application State as Component State (AASaCS). This also shows early issues I had with “inventing” what I thought was “reactive routing” (why store a route if we can calculate the current route off of what hasn’t been completed yet?)

Take this example, which is almost exactly what the early application looked like (which we are still paying the cost for):

interface TopLevelComponentState {
  usedLoggedIn: boolean;
  application: ILoraDtcApplicatonObject;
  isVoiceSignature: boolean;
  devToolsOn: boolean;
}

class TopLevelComponent extends React.Component<TopLevelComponentState> {
  constructor() {
    // Load TopLevelComponentState from session storage
  }
  render() {
    return (
      <DerivedClassDtc
        applicaton={this.state.application}
        usedLoggedIn={this.state.userLoggedIn}
      />
    );
  }
  // ...
}

/// Somewhere else...

interface DerivedClassDtcProps {
  usedLoggedIn: boolean;
  application: ILoraDtcApplicatonObject;
}

interface DerivedClassDtcState {
  // 10 different objects here, in one object
  applicatonState: IApplicatonState;
}

class DerivedClassDtc extends React.Component<
  DerivedClassDtcProps,
  DerivedClassDtcState
> {
  constructor() {
    // Load DerivedClassDtcState from session storage
  }
  render() {
    if (!applicatonState.secondScreenComplete) {
      return (
        <SecondScreen
          application={this.props.application}
          onComplete={(data) =>
            this.setState({
              applicatonState: {
                ...this.state.applicatonState,
                secondScreenResult: data,
              },
            })
          }
        />
      );
    } else if (!applicatonState.firstScreenComplete) {
      return (
        <FirstScreen
          application={this.props.application}
          onComplete={(data) =>
            this.setState({
              applicatonState: {
                ...this.state.applicatonState,
                firstScreenResult: data,
              },
            })
          }
        />
      );
    }
  }
}

The FirstScreen and SecondScreen also had their local states, loaded from session storage.

My thought and programming mindset was to avoid global variables wherever possible, so I was against scoping any state where others that didn’t need to see it could see it. Generally good advice, but not really in the context of complex stateful React applications, as I learned later.

As the above example shows, the problem is that now the application all the way up at the top of the tree needs to get down to the lowest levels of each screen, so it can either be read or updated as the user changes information.

This is worse: Now, each intermediate component needs to know about the dependencies of what it renders, and needs to connect those dots from props or state.

I knew this at the time, but at first I thought this was good, as it shows explicit data flow, which is another one of my likes. The problem becomes scalability: The app is dozens and dozens of components, and now there are wires—which can and will break, and are hard to reason about—through the whole app just to pass data, sometimes the same pieces where.

Which gives rise to basically the entire app being a cloud:

  • Who owns what data?
  • Where is the prop coming from? Whose state is it?
  • When is it updated, and how?
  • What session storage key does it use? When it is loaded?
  • And the worst, did this change break the data flow to the top-level container, or its container?

These questions are still almost impossible to answer in this app today.

Thankfully, a colleague of mine re-did the “clever routing” above to use React Router, which allowed us to navigate to each screen at will for development purposes, via a “Dev Box” among other huge features.

Second React App: A Pretty Good App

The 2nd React project at Ensurem (and also my second) was a portal for internal applications. This was to be a growing place for various internal tools as they were developed, so as to not set up and deploy dozens of small apps, each duplicating each other in some way most likely (like we already had done in some of the PHP tools).

The App TL;DR

  • Bootstrap, later a mix with Ant Design
  • Still no state management library
  • Routing from the start
  • Functional components / hooks from the start
  • Data fetching libraries
  • Used SCSS well
  • Handles user auth via JWT and permissions/roles well

This app was for internal users, and had to come with a permissions system where each section of apps (finance, marketing, administration) only allowed certain employees access. At the same time we developed a from-scratch roles-permissions system in our database. All in all we did pretty well.

The app had to:

  • Login/logout/stay logged in using the company’s Azure active directory
  • CRUD permissions and roles for each employee
  • Support dozens of apps under a bunch of sections, all requiring different permissions
  • Have a consistent look and error handling

Improvements learned

Understanding and use a router

The app was router-based from the start, learning from the last app. I don’t have much to say here, besides I realized implementing this yourself was not the Right Way™

Using OpenAPI And Swagger

One of the biggest improvements this app brought, which we would then use everywhere, was using NSwag and Swagger for interop between our ASP.NET Core backends and TypeScript frontends. This eventually was adopted in the first app and replaced a lot of the manually-created models.

The internal apps portal relies on dozens of REST APIs, all with their own models and URLs, which are changed, removed, and added over time. Using Swagger allowed us to have compilation errors if someone changed a backend model, rather than it going undetected for weeks until we get a report or something not working.

This also allowed for great data integrity from the frontend, by marking certain fields as required, which would automatically be a 400 response if something was not supplied: No chance of getting NULLs or 1900-01-01 SQL dates from a bad data conversion or something.

Example:

import {
  ApiServerService,
  ICertification,
  UserClient,
} from "common/services/ApiServerService";

// The NSwag-generated ICertification model from ApiServerService:

// export interface ICertification {
//   ens_user_certification_tid: number;
//   sql_created_by: string;
//   sql_created_datetime: Date;
//   sql_modified_by: string | undefined;
//   sql_modified_datetime: Date | undefined;
//   ens_user_certification_type_tid: number;
//   user_certification_name: string;
//   expiration_date: Date | undefined;
//   complete_fl: boolean;
// }

function App() {
  const [certs, setCerts] = useState<ICertification[]>();

  useEffect(() => {
    authenticatedFetch(ApiServerService.endpoint, UserClient, (c) => {
      return c.getUserCertifications(agent.ens_user_tid);
    }).then((certs) => {
      setCerts(certs);
    });
  }, [agent]);
  // ...
}

The UserClient and ICertification are generated from the C# models and controllers, which ensure they’re never out of sync. Before this, the only way was to manually copy/paste the models, and pray they don’t change.

I first found Reinforced Typings before Swagger, which generated TypeScript models with much manual effort, and didn’t touch controllers, which was a early step, but was ripped out for NSwag.

Use a Data Fetching Library (then don’t use it)

In days of the first project, I wrote from scratch a JsonApiService that wrapped fetch to handle data fetching. It had almost no error handling, failed if the response was not json, failed if the response was not something “normal”, and didn’t really do anything besides set a few Fetch options and deserialize the response for you.

I anticipated and saw the need for component states for loading, failed, and success for each data call, and found react-refetch and convinced my team why we should adopt it. Mostly because it encapsulates those promise states to easily display the UI, and “eliminated” manual API calls. It served the apps portal well, even with auth, because it was customizable with the JWT from Azure.

Patterns like this were and are common:

interface AppOuterProps {
  user: UserClass;
  agent: EnsUser;
}

interface AppInnerProps {
  appointmentsFetch: (
    ens_user_tid: number
  ) => PropsMap<AppointmentMapInnerProps>;
  appointmentsFetchResult?: PromiseState<Appointment[]>;
}

function App(props: AppInnerProps) {
  // More Stuff Here...
  return (
    <>
      {props.appointmentsFetchResult.pending && <Loader />}
      {props.appointmentsFetchResult.rejected && <Loader />}
      {props.appointmentsFetchResult.fulfilled && (
        <DataDisplayer data={props.appointmentsFetchResult.data} />
      )}
    </>
  );
}

export default connect<AppointmentMapOuterProps, AppointmentMapInnerProps>(
  () => ({
    appointmentsFetch: (ens_user_tid: number) => ({
      appointmentsFetchResult: {
        value: authenticatedFetch(
          ApiServerService.endpoint,
          UserClient,
          (c) => {
            return c.getUserAppointments(ens_user_tid);
          }
        ),
        force: true,
      },
    }),
  })
)(App);

Although providing the loading and error states automatically were good in principle, today we don’t use this. The boilerplate is far too much with TypeScript, plus it is written using older HoC-style instead of hooks, which makes it awkward to use. We typically just use something like this in the portal today:

interface AppProps {
  user: UserClass;
  agent: EnsUser;
}

export default function App(props: AppProps) {
  const [data, setData] = useState<Appointment[] | undefined | null>(undefined);

  useEffect(() => {
    authenticatedFetch(ApiServerService.endpoint, UserClient, (c) =>
      c.getUserAppointments(ens_user_tid)
    )
      .then((data) => setData(data))
      .catch(() => setData(null));
  }, []);

  return (
    <>
      {data === undefined && <Loader />}
      {data === null && <Error />}
      {data && <DataDisplayer data={data} />}
    </>
  );
}

Import SCSS Directly

I learned one could import SCSS into .tsx files directly, via webpack, instead of compiling to CSS manually beforehand.

Yes, in the DTC app, the project’s SCSS is built via NPM script and node-sass, then imported as one huge style.css in the app. This makes it almost impossible to see where styles are coming from. Today, the SCSS/CSS is one of the most fragile parts of the app. Whoops.

Limit Data Wires Through The App

To be fair, this app was not nearly as huge, so there weren’t as many data wires of death, but from the start a “core” and “common” section were written before any pages, and it worked out well.

  • The “core” was the first few pages where internal state was kept, like the applications select screen, the initial auth, and such. This was hidden from the app basically everywhere and just rendered routes as needed, without each route needing to check auth and things themselves
  • The “common” was a small set of components that were useful in isolation, like <Loader/>s, modals, and the shared User class (which should have been Redux or something)

The Third App: Redemption

Between the 2nd and 3rd apps, we used Next.js for a bunch of websites that I was not too involved in, so I will skip them here. We use Next static export and host the result on S3 buckets.

The third app (which at this point was not the company’s third React app) was another survey-based data collection app, but far less complex than the first. It was basically a “maze” of screens that would help the user decide on Medicare coverage, and included some of our now-developed technology such as our quoting tools in-app, links to our other apps to finish the process, and provide a way to enter forms to be called by an insurance Agent.

The App TL;DR

  • Another CRA SPA
  • CSS-in-JS via Chakra UI over Bootstrap
  • Redux from the start
  • React router connected in Redux
  • Tests with Jest
  • Formik
  • Swagger and OpenAPI, abstracted in hooks for data fetching
  • App structure decided beforehand, separation of data and rendering code
  • Used Framer Motion for a slick interface (not my doing)

With more knowledge in my pocket, I suggested these tools to the team. This is starting to sound more modern and controlled. I debated hard on using Next, but I wasn’t comfortable enough with it, and I didn’t think it needed it. As this was another survey-based data collection app, and it didn’t make sense to have multiple server pages in order to keep the data collected intact and not worry about page refreshes.

Hail Chakra

The biggest and most successful change on our side was switching to use Chakra UI. This allowed us to develop the app’s screens very quickly, and have almost not a single stylesheet in the app, even though it looked exactly like our creative team’s mockups. It also eliminated a number of issues like CSS stylesheet conflicts, and well, all the normal stuff CSS-in-JS provides.

I wasn’t fully aware that the style Chakra used was called “CSS-in-JS”: I thought it was unique at first, but then learned this has been done before with notably Emoticon and others.

I think this is absolutely the way to go with Modern web applications: Everything JS-related now has the ability to be component-ized and isolated, except for traditional CSS which is always global without libraries to make it not. This is a side effect of the traditional webpage model, and I’m glad we can avoid that now.

Style conflicts were some of the worst things about developing React, which the first application had, and less so the 2nd.

Consequences of Not Being Afraid of App State

For the past two applications, I tried to avoid things like Redux because (1) thinking I didn’t need something like it and (2) waiting to avoid a set of “global variables” the entire app can change.

I think (2) also was my strong conditioning of coming from a procedural, object-oriented native code background.

I already listed why thinking this way made my first two apps difficult to work with, but after finally giving in, I found these benefits

  • The issue of “having everything being global variables” doesn’t really matter, as Redux (1) makes it difficult to change state by design, (2) has great dev tools to easily see what is changing what, when, and how, and (3) the state is immutable! You have to update it explicitly through dispatches, so there’s no chance of something accidentally grabbing and changing a reference.

  • Having all app state in one place allows it to easily be hydrated (fancy word) to and from a server at once, instead of trying to inject each piece separately into the app, praying all of it loads, and having no way to see what owns and changes what. Not to mention making sure the app can survive all those pieces loading, each of the components then possibly rendering other trees based on the data which could cause strange data undefined issues.

  • Allowed data to easily flow from early parts of the app to later parts without any worry about render tree structure. The first screen sets a “first name” when then is pulled as needed throughout the app; the screen that sets this state doesn’t need to care how this data gets to other components.

Real Testing

My inexperience with JavaScript made it difficult to get any automated tests working. It was just a matter of time, and in this iteration we managed to get Jest working well, with Good Things® like mocking backend calls.

The app was testable because of Redux, allowing the tests to set up a state, and the use of hooks made mocking API calls fairly straightforward.

Our tests started as shotgun “does the app work” tests, with a test for each route, which scored quickly and discovered some issues early on. Later, we were able to simplify the testing setup to bring in QA resources to help write them.

Here’s an example of tests that test that the initial screen’s button

  • Stores the users first name in the Redux store
  • Obtains an access token from the backend and stores it
describe("the agent E welcome screen", () => {
  let app: AgentETestEnvironment;

  // Setup...

  describe("pressing the continue button", () => {
    let mockCreateAppSession: jest.SpyInstance;

    beforeEach(() => {
      mockCreateAppSession = jest
        .spyOn(LoraCommonClient.prototype, "createApplicationSession")
        .mockImplementation(() => {
          return Promise.resolve(new LoraCreateApplicationResponse({}));
        });
    });

    afterEach(() => {
      mockCreateAppSession.mockReset();
    });

    it("stores the users name in the app after entering", async () => {
      await enterFirstNameAndClickContinue("Ensurem");
      expect(app.store.getState().userDetails.firstName).toBe("Ensurem");
    });

    it("creates a new lora application when clicking continue and stores the token", async () => {
      await enterFirstNameAndClickContinue("Ensurem");
      expect(mockCreateAppSession).toBeCalledTimes(1);
      expect(app.store.getState().auth.loraAccessToken).toBe("token");
    });
  });
});

This is great! I’m pretty happy we were able to get this level of testing.

Separation of Data and Rendering

Even though this app was a similar app to the first DTC application, this app was started from a clean slate. As much as I tried to make reusable components, they were, on the contrary, completely unreusable. A big part of this was that the English text and the component was tied to the rendering logic, meaning you basically had to make a new component to swap any of it out.

A stretch goal of this app was to be able to change the app content from a database or perhaps fetch it on a per-US-state or per-client basis, so we did just that;

We hardcoded the content into each component to begin with, with the idea that now we could easily load this content from a database with very minimal component updates:

export default function SorryToSeeYouGo() {
  const content: PageContent = {
    static: {
      content: {
        title: "I’m sorry to see you go,",
        body:
          "But I am always here to help any time, day or night! Feel free to come back and visit me or give one of my licensed agents a call at:",
      },
    },
  };

  const heading = content.static["content"].title;
  const bodyText = content.static["content"].body;

  return (
    <Stack maxW={540}>
      <Heading fontSize="headingSm">{heading}</Heading>
      <Text fontSize="textMd">{bodyText}</Text>
      <Box mt={5}>
        <SpeakToAgent />
      </Box>
    </Stack>
  );
}

This also makes the rendering logic cleaner and easier to look at, without English text interspersed in it.

Closing and Future Progress

And there you have it, my first year of using React has been summarized in a rather long post. I think this was a good exercise, and readers should be able to see the improvements made, and hopefully, read this and not make the same mistakes.

Today, I’m extremely comfortable with React. I’m now looking to the horizon and exploring other frameworks like Svelte. It’s great, and looking at Svelte makes React seem old-school. It’s great to have this different reference point. By reading the philosophy around Svelte, the abstraction leakages (Yes, I would call them that, as pointed out by the Svelte blog) React provides such as useMemo, concurrent mode, and others are just not needed, because it flips the idea of reactivity upside down to way that I think is the future: truly surgically updating the DOM, without a virtual DOM.

But after building some side projects with it, I don’t think Svelte is production-ready for the scale Ensurem needs it, plus the ecosystem is not nearly as huge.

Stay tuned for the “Year of Svelte” post though!