A Reactrospective: A React Retrospective

James Bradley Angular, Development Technology, HTML5, JavaScript, React Leave a Comment

Most, if not all, of my experience has been with .NET and .NET Core, but I’ve also worked with most of the front-ends throughout history including Classic ASP, Code Behind, Model View Presenter, MVC, Backbone, and, over the last few years, Angular 1… Angular 6…  not Angular 2… AngularJS. To me, AngularJS is that old t-shirt that you put on to sleep in; comfortable. My first experience with React was an interesting one.

At first, most of us on the team were a bit apprehensive about moving to React. AngularJS was very familiar, and Angular 2 seemed like the natural next step. I could learn TypeScript, and as a primarily C#-based developer, I really liked the idea of a little typing. I like shiny new things, and I’m always game to learn, so challenge accepted. Since then, I’ve had a hand in writing three applications in React and have learned more than a few lessons.

These are their stories… dun dun.

The Crime

Our first attempt at a React was rudimentary and hastily done for a hack-a-thon. We mixed the JS, state, and HTML all in the same page. It was messy. If I’m being honest, this application was too complicated for our first time using the new language, and I’m not sure if that set us back or propelled us forward kicking and screaming. 

We used JSON schema to create a dynamic form that a user could fill out, and then that whole thing persisted into a Document Database. It was complicated, and, coming directly from AngularJS, we didn’t know how to manage our own state. We had a few components, and I was starting to be able to see the value in using React. 

The clues were piling up. 

Disclaimer: The following code is fictional and does not depict any actual working code or events. This is roughly what the code started out to be, but picture three times this amount. Pretty messy in the end.

search.jsx

import React, { Component } from "react";

export class Search extends Component {
  constructor(props) {
    super(props);

    this.state = {
      results: [],
      companyName: "",
      invoiceNumber: 0,
      paymentTypeId: 0,
      paymentTypes: [],
      showAdvance: false,
    };

    this.handleInputChange = this.handleInputChange.bind(this);
    this.fetchPaymentTypes = this.fetchPaymentTypes.bind(this);
    this.fetchResults = this.fetchResults.bind(this);
  }

  componentDidMount() {
    this.fetchPaymentTypes();
  }

  fetchPaymentTypes = () => {
    fetch("url to api/")
      .then((r) => r.json)
      .then((data) => {
        this.setState({ paymentTypes: data });
      });
  };

  fetchResults = () => {
    fetch("url to api/")
      .then((r) => r.json)
      .then((data) => {
        this.setState({ results: data });
      });
  };

  handleShowAdvance = () => {
    this.setState({ showAdvance: !this.state.showAdvance });
  };
  handleInputChange = (e) => {
    this.setState({
      [e.target.name]: e.target.value,
    });
  };

  render() {
    return (
      <div className="container">
        <div className="row">
          <h5>Search</h5>
        </div>
        <div className="row">
          <div className="col-sm-6">
            <label>Company Name</label>
            <input
              type="text"
              value={companyName}
              onChange={this.handleInputChange}
            />
          </div>
          <div className="col-sm-3">
            <button className="btn btn-sm" onClick={this.fetchResults}>
              Search
            </button>
          </div>
        </div>
        {showAdvance ? (
          <div className="row">
            <div className="col-sm-6">
              <label>Invoice Number</label>
              <input
                type="text"
                value={invoiceNumber}
                onChange={this.handleInputChange}
              />
            </div>
            <div className="col-sm-6">
              <label>Payment Types</label>
              <select value={paymentTypeId} onChange={this.handleInputChange}>
                {paymentTypes.map((x) => (
                  <option key={x.id + x.name} value={x.id}>
                    {x.name}
                  </option>
                ))}
              </select>
            </div>
          </div>
        ) : null}

        <div className="row">
          <div className="col align-self-center advance-tab">
            <a className="advance-tab" onClick={this.handleShowAdvance}>
              {showAdvance ? "Hide Advanced Search" : "Show Advanced Search"}
            </a>
          </div>
        </div>
        <div className="row">
          <div className="col-sm-12">
            {/* results is some kind of format. I really like react-table but .
               mostly for the way it looks. Most business use excel more than
               they should and react-table kind of looks and feels like excel to me.
               It doesn't support type script in the new version so beware. */}
          </div>
        </div>
      </div>
    );
  }
}

The Evidence

My second and third attempts were for the same application. I worked on this application by myself, and it was our first production application using React. It is a simple request form that has a complicated workflow depending on the fields selected. It’s an iceberg project. It looks simple and small from the surface, but in reality, 90 percent of it is hidden beneath the water, and you don’t see it or even know it’s there. Like the first application, I started out writing it in ReactJS, but then, more evidence came to light. 

As a rule of thumb, if you create a prototype, it will at some point become a production application. The first application became just that, and we had a mess on our hands. Sometimes the application worked great, but other times it would just break. We were having asnc problems, and I was tasked with cleaning up. 

To get it to work, I started adding promises to ensure that tasks finished before setting the state. This did help the code to work better, but it also made the already messy code even messier. 

Time to talk to the Prosecutors.  

The Trial

Having learned some hard lessons, I went back and refactored the second application using Redux, TypeScript, Thunk and React. If I’m being honest, it was a complete rewrite – the only thing that really survived was the html. 

This time around, I went to the opposite extreme. Now, all the state was managed using Redux, and all events were in Thunk, wrapped in nice promises. Redux has its own learning curve, and I found it difficult to comprehend the immutability of the store. How I started with Redux was not how I finished. 

Mid way into the rewrite, I was pulled to write another application. We started this one with Redux, Thunk, and Typescript. It was fairly complicated, and it used some of the similar principles of dynamic forms that the first one did, but only a small part. This one felt less gross than the first two, but with Redux, there is a lot more wiring. 

There’s still a crucial piece of missing evidence; time to talk to more suspects.

A Continued Investigation

Disclaimer: The following code is fictional and does not depict any actual working code or events. Now, most of the code is broken into nice little typescript pieces. Let’s look at the models.

models/SearchCriteria.ts

export default interface SearchCriteria {
    companyName:string;
    invoiceNumber?: number;
    paymentTypeId?: number;
    showAdvance: boolean;
}

models/SearchResult.ts

export default interface SearchResult{
    company: string;
    invoiceNumber: number;
    paymentType: string;
}

Next is the redux store code. For me, this part is more than a little tedious.

See Also:  Maps and Entities and JPA, OH MY!

store/search/constants.ts

 
export const HANDLE_CRITERIA_CHANGE = "HANDLE_CRITERIA_CHANGE";
export const SET_RESULTS = "SET_RESULTS";
export const SET_PAYMENT_TYPES = "SET_PAYMENT_TYPES";

store/search/types.ts

import {
  HANDLE_CRITERIA_CHANGE,
  SET_RESULTS,
  SET_PAYMENT_TYPES,
} from "./constants";
import SearchResult from "../../models/SearchResult";
import PaymentType from "../../models/PaymentType";

interface HandleCriteriaChange {
  type: typeof HANDLE_CRITERIA_CHANGE;
  name: string;
  value: string | number | boolean | Date | null;
}

interface SetResults {
  type: typeof SET_RESULTS;
  results: SearchResult[];
}

interface SetPaymentTypes {
  type: typeof SET_PAYMENT_TYPES;
  results: PaymentType[];
}

export type SearchTypes =
| HandleCriteriaChange
| SetResults
| SetPaymentTypes;

store/search/actions.ts

import { SearchTypes } from "./types";
import {
  HANDLE_CRITERIA_CHANGE,
  SET_RESULTS,
  SET_PAYMENT_TYPES,
} from "./constants";
import SearchResult from "../../models/SearchResult";
import PaymentType from "../../models/PaymentType";

export function handleCriteriaChange(
  name: string,
  value: string | number | boolean | Date | null
): SearchTypes {
  return {
    type: HANDLE_CRITERIA_CHANGE,
    name: name,
    value: value,
  };
}

export function setResult(results: SearchResult[]): SearchTypes {
  return {
    type: SET_RESULTS,
    results: results,
  };
}

export function setPaymentTypes(paymentTypes: PaymentType[]): SearchTypes {
  return {
    type: SET_PAYMENT_TYPES,
    results: paymentTypes,
  };
}

store/search/reducer.ts

import SearchState from "../../models/SearchState";
import { SearchTypes } from "./types";
import {
  HANDLE_CRITERIA_CHANGE,
  SET_RESULTS,
  SET_PAYMENT_TYPES,
} from "./constants";

const initialState: SearchState = {
  criteria: { companyName: "", showAdvance: false },
  results: [],
  paymentTypes: [],
};

export function searchReducer(
  state = initialState,
  action: SearchTypes
): SearchState {
  switch (action.type) {
    case HANDLE_CRITERIA_CHANGE: {
      let model: any = { ...state.criteria };
      model[action.name] = action.value;
      return { ...state, criteria: model };
    }
    case SET_RESULTS: {
      return { ...state, results: action.results };
    }
    case SET_PAYMENT_TYPES: {
      return { ...state, paymentTypes: action.results };
    }
    default:
      return state;
  }
}

store/store.ts

import { combineReducers, applyMiddleware, createStore } from "redux";
import { searchReducer } from "./search/reducer";
import thunkMiddleware from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";

export type AppState = ReturnType<typeof rootReducer>;

export default function configureStore() {
  const middleware = [thunkMiddleware];
  const middleWareEnhancer = applyMiddleware(...middleware);

  const store = createStore(
    rootReducer,
    composeWithDevTools(middleWareEnhancer)
  );
}

export const rootReducer = combineReducers({
  search: searchReducer,
});

This is an example of what the middleware file looks like. This is where I generally put calls to an API, or if there’s some business logic that needs to happen, this is where it goes. Here you can use dispatch to push to redux or getState if you need something from the state.

src/middleware/search.middleware.ts

import { AnyAction } from "redux";
import { AppState } from "../store/store";
import { ThunkAction } from "redux-thunk";
import SearchCriteria from "../models/SearchCriteria";
import { setResult, setPaymentTypes } from "../store/search/actions";

export const fetchSearch = (
  criteria: SearchCriteria
): ThunkAction<Promise<void>, AppState, null, AnyAction> => async (
  dispatch,
  getState
) => {
  return new Promise<void>((resolve, reject) => {
    fetch("url to api/" + criteria)
      .then((r) => r.json)
      .then((r: any) => {
        dispatch(setResult(r));
        return resolve();
      });
  });
};

export const fetchPaymentTypes = (): ThunkAction<
  Promise<void>,
  AppState,
  null,
  AnyAction> => async (dispatch, getState) => {
  return new Promise<void>((resolve, reject) => {
    fetch("url to api/")
      .then((r) => r.json)
      .then((r: any) => {
        dispatch(setPaymentTypes(r));
        return resolve();
      });
  });
};

This next file is the container. I’m not sure if this is the “right” way of doing this, but the idea is that there is a fair bit of wiring to get Redux, Thunk, and React to work together, and I’m containing it at the very top level. Then, the responsibility of this file becomes to pass out the things the React components need.

components/search/SearchContainer.tsx.

import { AnyAction } from "redux";
import { AppState } from "../store/store";
import { ThunkAction } from "redux-thunk";
import SearchCriteria from "../models/SearchCriteria";
import { setResult, setPaymentTypes } from "../store/search/actions";

export const fetchSearch = (
  criteria: SearchCriteria
): ThunkAction<Promise<void>, AppState, null, AnyAction> => async (
  dispatch,
  getState
) => {
  return new Promise<void>((resolve, reject) => {
    fetch("url to api/" + criteria)
      .then((r) => r.json)
      .then((r: any) => {
        dispatch(setResult(r));
        return resolve();
      });
  });
};

export const fetchPaymentTypes = (): ThunkAction<
  Promise<void>,
  AppState,
  null,
  AnyAction
> => async (dispatch, getState) => {
  return new Promise<void>((resolve, reject) => {
    fetch("url to api/")
      .then((r) => r.json)
      .then((r: any) => {
        dispatch(setPaymentTypes(r));
        return resolve();
      });
  });
};

Finally this is what the Search.jsx from above has become. As you can see, there is much more code needed to use Redux, Thunk, TypeScript, and React. Everything is now broken into smaller single responsibilities, which I like.

src/components/search/Search.tsx

import React from "react";
import SearchCriteria from "../../models/SearchCriteria";
import SearchResult from "../../models/SearchResult";
import PaymentType from "../../models/PaymentType";

interface SearchProps {
  criteria: SearchCriteria;
  results: SearchResult[];
  handleChange: (
    name: string,
    value: string | number | boolean | Date | null
  ) => void;
  fetchResults: any;
  paymentTypes: PaymentType[];
}

const Search: React.FC<SearchProps> = ({
  criteria,
  results,
  handleChange,
  fetchResults,
  paymentTypes,
}) => {
  return (
    <div className="container">
      <div className="row">
        <h5>Search</h5>
      </div>
      <div className="row">
        <div className="col-sm-6">
          <label>Company Name</label>
          <input
            type="text"
            name="companyName"
            value={criteria.companyName}
            onChange={(e) => handleChange(e.target.name, e.target.value)}
          />
        </div>
        <div className="col-sm-3">
          <button className="btn btn-sm" onClick={() => fetchResults(criteria)}>
            Search
          </button>
        </div>
      </div>
      {criteria.showAdvance ? (
        <div className="row">
          <div className="col-sm-6">
            <label>Invoice Number</label>
            <input
              type="text"
              name="invoiceNumber"
              value={criteria.invoiceNumber}
              onChange={(e) => handleChange(e.target.name, e.target.value)}
            />
          </div>
          <div className="col-sm-6">
            <label>Payment Types</label>
            <select
              value={criteria.paymentTypeId}
              name="paymentTypeId"
              onChange={(e) => handleChange(e.target.name, e.target.value)}
            >
              {paymentTypes.map((x) => (
                <option key={x.id + x.name} value={x.id}>
                  {x.name}
                </option>
              ))}
            </select>
          </div>
        </div>
      ) : null}

      <div className="row">
        <div className="col align-self-center advance-tab">
          <a className="advance-tab"  onClick={() => handleChange("showAdvance", !criteria.showAdvance)} >
            {criteria.showAdvance
              ? "Hide Advanced Search"
              : "Show Advanced Search"}
          </a>
        </div>
      </div>
      <div className="row">
        <div className="col-sm-12">
          {/* results is some kind of format. I really like react-table but
           mostly for the way it looks. Most business use excel more than
           they should and react-table kind of looks and feels like Excel to me.
           It doesn't support type script in the new version so beware. */}
        </div>
      </div>
    </div>
  );
};

export default Search;

Sufficient evidence has been collected, the prosecutors have readied their case, and now, it’s time to bring it in front of the judge.

The Deliberation

Part way through the rewrite, I started reading about Hooks. It was the last piece my puzzle was missing. Hooks allows you to do some of the wiring without having to use Redux. In my opinion, using Hooks allows you to cut down on the amount of code you have to write to manage Redux. Hooks is cleaner and does the state management for you. 

This is how the Search component looks after switching to using Hooks. I still use Thunk to fetch from an API and Redux to dispatch the results, but I no longer need to keep the search criteria in Redux. I can remove that suspect handle change from the Reducer. There is also less wiring in the “container,” which keeps it’s footprint smaller.

Search.tsx

import React, { useState } from "react"; 
import SearchResult from "../../models/SearchResult";
import PaymentType from "../../models/PaymentType";

interface SearchProps {
  results: SearchResult[];
  fetchResults: any;
  paymentTypes: PaymentType[];
}

const Search: React.FC<SearchProps> = ({
  results,
  fetchResults,
  paymentTypes,
}) => {
  const [companyName, setCompanyName] = useState("");
  const [invoiceNumber, setInvoiceNumber] = useState("");
  const [paymentTypeId, setPaymentTypeId] = useState(0);
  const [showAdvance, setShowAdvance] = useState(false);
  return (
    <div className="container">
      <div className="row">
        <h5>Search</h5>
      </div>
      <div className="row">
        <div className="col-sm-6">
          <label>Company Name</label>
          <input
            type="text"
            name="companyName"
            value={companyName}
            onChange={(e) => setCompanyName(e.target.value)}
          />
        </div>
        <div className="col-sm-3">
          <button className="btn btn-sm" onClick={() => fetchResults({companyName: companyName, invoiceNumber: invoiceNumber, paymentTypeId: paymentTypeId})}>
            Search
          </button>
        </div>
      </div>
      {showAdvance ? (
        <div className="row">
          <div className="col-sm-6">
            <label>Invoice Number</label>
            <input
              type="text"
              name="invoiceNumber"
              value={invoiceNumber}
              onChange={(e) => setInvoiceNumber(e.target.value)}
            />
          </div>
          <div className="col-sm-6">
            <label>Payment Types</label>
            <select
              value={paymentTypeId}
              name="paymentTypeId"
              onChange={(e) => setPaymentTypeId(+e.target.value)}>
              {paymentTypes.map((x) => (
                <option key={x.id + x.name} value={x.id}>
                  {x.name}
                </option>
              ))}
            </select>
          </div>
        </div>
      ) : null}

      <div className="row">
        <div className="col align-self-center advance-tab">
          <a
            className="advance-tab"
            onClick={() => setShowAdvance(!showAdvance)}
          >
            {showAdvance ? "Hide Advanced Search" : "Show Advanced Search"}
          </a>
        </div>
      </div>
      <div className="row">
        <div className="col-sm-12">
          {/* results is some kind of format. I really like react-table but
           mostly for the way it looks. Most business use excel more than
           they should and react-table kind of looks and feels like excel to me.
           It doesn't support type script in the new version so beware. */}
        </div>
      </div>
    </div>
  );
};
export default Search;

The Verdict

After a full investigation and multiple trials, these are the lessons I learned and my final verdict on using React. 

Create-React-App

I only bring this one up because it was a pretty big paradigm shift for me. One of the first lessons I learned was that there are differences between Create-React-App done with .NET Core CLI and Create-React-App done with Yarn/Npm. Using the .NET Core CLI to create a project gave us an application that looked and felt like the AngularJS projects we were used to creating in Visual Studio. Each time you start in development mode, it will start a new Instance of the CRA server. This means you’ll have to restart debugging after any changes you make to see those changes. 

We also ran into problems with pushing the application into AWS through OpenShift. It would have a problem with dependencies and was getting an error that was a bit ambiguous. Finally, we used CRA Yarn, copied the client app folder into the new project, and switched to VSCode for development. We have never looked back.

Visual Studio Code

Old habits die hard, and in the beginning, I tried using Visual Studio. It worked, but now, I use Visual Studio Code exclusively for UI development. It works better and is written with modern development in mind. If you haven’t tried it yet, trust me, you should. Install Debugger for Chrome and ESLint.

Yarn vs. NPM

We started off using npm – mostly because none of us knew what Yarn was. Yarn was created to address some of the security issues with NPM. Yarn performs better, is more stable, and manages versions of packages better. It is now the preferred version manager.

TypeScript

This is more based on personal preference, but I prefer a typed language and have always been uncomfortable with the looseness of JavaScript. I’ve found that TypeScript helps catch errors before they happen. It boosts productivity by using static type checking and intelligent code completion. We are moving to a more distributed team and the syntactic sugar will allow for better performance and less technical debt. for others, it may add some complications, but for me, TypeScript is worth any extra trouble.

Redux

You don’t always need to use a state store like redux, but there are times when you absolutely should. Redux is wordy, and there’s a steep learning curve. However, trust me, it’s worth it when you need to have a source of truth for your state and when the data becomes too complex to manage in a top-level React component. It will keep your state more predictable and easier to maintain.

Middleware

Redux should not mutate the state. You send the data to the reducer, set the state, and that’s it. If you need to mutate the data, then you should do that in the middleware. Thunk is a middleware library designed to work with Redux. This is also where you can put calls into your APIs.

Hooks

Hooks are a great alternative for abstracting logic, which reduces the amount of code that it takes to manage state. Hooks have some basic rules and should only be used in the top-level of a React Function. Hooks shouldn’t be in loops, conditions, or nested functions. Adhering to these rules ensures that Hooks are called in the same order every time the component is rendered.

On a Final Note

We have decided to go back to the first project and “refactor” the front-end to use Hooks and Redux, heavily leaning on Hooks. I found some rumblings about a better way to combine those two, so that will be the next thing I figure out: how to use and write my own custom hooks.  

There have been ups and downs, but the last few months have been quite fun learning React. I can see why it’s popular now. I like the idea of components and the fact that I can write frontend code at least a little like I would backend code. I haven’t tried Angular 2, so I don’t know how it compares to React. Maybe the case I investigate for my next blog …

Related Posts

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments