Lessons from a Weekend of Building a React / Node App

Tech

July 16, 2019 7 min read

Author coding on the patio

This past weekend I spent hours building a travel budget planner app in React and Node and deploying to Heroku, and learned a lot in the process. These are some of the lessons I learned that may be helpful to my future self or to others undertaking such an end-to-end task for the first time.

I used the following libraries and packages for this project:

  • Create React App
  • Node
  • Express
  • MongoDB / Mongoose
  • Passport for OAuth
  • Unsplash API
  • Heroku
  • React Redux
  • React Router
  • Styled Components
  • AirBnB React Dates
  • React Testing Library
  • Jest
  • Babel
  • Git / GitHub

Working with Git

Having to enter git credentials when pushing to remote

Every time I tried to push to remote (or any other interaction with remote), I had to enter my git credentials into the terminal. I checked my ssh folder (C: > Users > [user] > .ssh on Windows) to ensure I had the id_rsa and id_rsa.pub files. Since I did, I knew it wasn't an issue with SSH keys not being set. After a bit of Googling, I found that the remote URL I was using was https rather than ssh (when creating a git repository, Github provides both URLs and I had grabbed the https one). I went ahead and changed my remote url to ssh:

$ git remote set-url origin git@github.com:USERNAME/REPOSITORY.git

Deploying to Create React App with Express Server to Heroku

Heroku + Node.js Error: Web process failed to bind to $PORT within 60 seconds of launch

I got this error while trying to deploy the app to heroku: "Web process failed to bind to $PORT within 60 seconds of launch)". This was due to an error in the Node server where I had not handled the environment variable PORT. Instead of listening to a static port, it should have listened to process.env.PORT OR 3001. This was simply an oversight as I always add this bit in and of course the issue did not appear until deploying to Heroku.

Instead of:

app.listen(3001, function() {});

It should be:

app.listen(process.env.PORT || 3001, function() {});

CRA public template file used instead of final build file

There were a couple of issues here. One, I needed the express server to serve up either the index.html template file in the public directory while in development or the index.html final build file while in production. Secondly, I needed the express server to handle static files properly so that static files like the favicon file didn't end up pulling from the wrong location.

Originally, this was the code I had to handle files on the express server:

const publicPath = path.join(__dirname, "..", "public");

app.use(express.static(publicPath));

I replaced it with this:

if (process.env.NODE_ENV === "production") {
  app.use(express.static(path.join(__dirname, "..", "build")));
  app.get("/*", (req, res) => {
    res.sendfile(path.join((__dirname, "..", "build/index.html")));
  });
} else {
  app.use(express.static(path.join(__dirname, "..", "public")));
  app.get("/*", (req, res) => {
    res.sendFile(path.join(__dirname, "..", "public/index.html"));
  });
}

This code would check for the NODE_ENV environment variable, and if it was production, all static files would come from the build directory and any routes not defined above it would be served index.html from the build folder.

Testing with React Testing Library and Jest

Snapshots and randomly generated IDs

I ran into an issue when setting up snapshot tests using react testing library and jest. I was generating random IDs in some of my componets to bind labels to form fields for accessibility. When running the tests, my snapshots were always different because of the random IDs being generated.

Originally, I was generating my IDs like this:

const ID =
    "_" +
    Math.random()
      .toString(36)
      .substr(2, 9);

I installed uniqid (npm install uniqid or yarn add uniqid) and used the package to generate my IDs:

import uniqid from "uniqid";
...
const ID = uniqid();

Then in my tests, I mocked uniqid to generate incrementing IDs for the snapshots so they would be consistent every time the test was run:

jest.mock("uniqid", () => {
  let value = 1;
  return () => value++;
});

Error: Cannot read property 'createLTR' of undefined when running snapshot tests on components using react-dates

This one seems like an obvious one, but I did need to Google it. When running snapshot tests on components using airbnb/react-dates, I got the error "Cannot read property 'createLTR' of undefined". The fix was to import react-dates/initialize into my test. It was imported into the root of my application, but the tests did not have access to it.

import "react-dates/initialize";

Error: Invariant Violation: You should not use <Route> or withRouter() outside a <Router>

When running snapshot tests on components with react router dom's <Link> component, I got the error "Invariant Violation: You should not use <Route> or withRouter() outside a <Router>". I imported Router and createMemoryHistory and added a function to render the component with Router. Since I also needed to render it with redux, I ended up creating a function that did both, explained in the next section.

import { Router } from "react-router-dom";
import { createMemoryHistory } from "history";

...

function renderWithRouter(
  ui,
  {
    route = "/",
    history = createMemoryHistory({ initialEntries: [route] })
  } = {}
) {
  return {
    ...render(<Router history={history}>{ui}</Router>),
    history
  };
}

...

test("can render with router", () => {
  const { asFragment } = renderWithRouter(
    <Trip />
  );
  expect(asFragment()).toMatchSnapshot();
});

Handling testing with react redux and a store

My application uses react redux, so I needed a way to test with a store and global state. I imported what I needed from redux and react-redux as well as my reducer and the default state of the reducer, and then added a function to my test suite to handle rendering with redux:

import { createStore } from "redux";
import { Provider } from "react-redux";
import tripsReducer, { tripsReducerDefaultState } from "../../reducers/trips";

I also created a file that exported mock data to use instead of the store and imported it:

import {
  trips,
  budgetCategories,
  budgetItems,
  expenses,
  auth
} from "../../testUtils/fixtures/mockStoreData";

The function for rendering with redux:

function renderWithRedux(
  ui,
  {
    initialState,
    store = createStore(tripsReducer, tripsReducerDefaultState)
  } = {}
) {
  return {
    ...render(<Provider store={store}>{ui}</Provider>),
    store
  };
}

And finally, the test rendering with redux and a mock store:

test("can render with redux with custom store", () => {
  // this is a silly store that can never be changed
  const store = createStore(() => ({
    trips,
    budgetCategories,
    budgetItems,
    expenses,
    auth
  }));
  const { asFragment } = renderWithRedux(
    <Trip />,
    {
      store
    }
  );
  expect(asFragment()).toMatchSnapshot();
});

Since I also needed to be able to render the component with react router (previous section), I created a function that combined renderWithRedux() and renderWithRouter() and used that throughout my tests instead:

import React from "react";
import { render, cleanup } from "@testing-library/react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import tripsReducer, { tripsReducerDefaultState } from "../../reducers/trips";
import { Router } from "react-router-dom";
import { createMemoryHistory } from "history";
import Trip from "../../components/Trip";
import {
  trips,
  budgetCategories,
  budgetItems,
  expenses,
  auth
} from "../../testUtils/fixtures/mockStoreData";

function renderWithReduxAndRouter(
  ui,
  {
    initialState,
    store = createStore(tripsReducer, tripsReducerDefaultState),
    route = "/",
    history = createMemoryHistory({ initialEntries: [route] })
  } = {}
) {
  return {
    ...render(
      <Provider store={store}>
        <Router history={history}>{ui}</Router>
      </Provider>
    ),
    store,
    history
  };
}

afterEach(cleanup);

test("can render with redux with custom store", () => {
  // this is a silly store that can never be changed
  const store = createStore(() => ({
    trips,
    budgetCategories,
    budgetItems,
    expenses,
    auth
  }));
  const { asFragment } = renderWithReduxAndRouter(
    <Trip />,
    {
      store
    }
  );
  expect(asFragment()).toMatchSnapshot();
});

Testing with match params

Some of my components reference props.match.params.someId, and I needed to be able to mock the parameters. In my test, I passed the parameters to the components as props:

const { asFragment } = renderWithReduxAndRouter(
    <Trip match={{ params: { id: "1" } }} />,
    {
      store
    }
  );

Share

Think others might enjoy this post? Share it!

Comments

I'd love to hear from you, let me know your thoughts!