Create a react application with the below command.

npx create-react-app my-app --template pwa-typescript

Rename the project name from my-app as you need. Also, note we use pwa-typescript template which provides the setup for a progressive web app.

Cleanup

Delete App.css and logo.svg.

Clear everything from the App.tsx leaving just the below.

function App() {
return <div>Hello World</div>
}
export default App

Tailwind

Tailwind documentation has a good page showing how to set up tailwind in create-react-app.

The setup includes installing a few dependencies, creating a file, and running a command. I don’t want to copy-paste those lines here.

Install these VSCode Extensions:

  • Tailwind CSS IntelliSense by Brad Cornes
  • Headwind by Ryan Heybourn

Done setting up tailwind? Let's see it in action.

oooh, Intelliscense!
// Run the development servernpm start

The bg-red-600 text-white and p-4 gives the below result.

Storybook

One can’t go back to traditional ways after using a fully-featured component library like storybook.

npx sb init

That sets up everything for us. Go ahead and run npm run storybook and open localhost:6006.

The example shows a good example of how to reuse components’ story arguments into one another. In the below example, the page story passes the arguments of HeaderStories.LoggedOut.

export const LoggedOut = Template.bind({});
LoggedOut.args = {
...HeaderStories.LoggedOut.args,
};

Before we delete src/stories folder, feel free to play around with the given example and understand things for yourself.

Now, for a sample, let's just have one simple component. Create src/components/atoms folder and run the below cmd.

npx rsb-gen atoms/Sample

rsb-gen is a simple utility package that I’ve published in npm that creates the react component, storybook, and testing files for the component.

const Sample = () => {
return <div className='p-4 bg-red-600 text-white'>Hello World!</div>
}
export default Sample

And run the development server for the storybook.

npm run storybook
Storybook needs to be configured for tailwind.

We cannot see the tailwind affecting the styles of the component in the storybook. We need to configure it.

a. Import our styles in .storybook/preview.js

import ‘../src/index.css’

b. Replace the .storybook/main.js file with the below.

And tailwind CSS works with storybook now!

Storybook addons

Visit the addons page and install the required addons as development dependencies. I’m going to install the below addons.

npm i -D @storybook/addon-a11y storybook-addon-designs

Update the addons in .storybook/main.js like this.

...
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/preset-create-react-app',
'storybook-addon-designs',
'@storybook/addon-a11y',
],
...

Restart the storybook dev server. Now we should see our two add-ons.

  • Design addon is to link mock designs from our design tool like Figma.
  • Accessibility is a very useful tool that alerts us about the accessibility issues on the page.

Theming Storybook

The storybook layout above looks very familiar. Let's theme is to our taste. We can theme the storybook according to the actual theme of our application. We don’t have one.

Let's go with blue as primary and shades of gray.

Create the below two files as .storybook/myTheme.js and .storybook/main.js

.storybook/main.js
.storybook/manager.js

And our theme should change!

Ok. Not it is not great but this serves as a base to have our storybook carry our application’s brand color palette!

And the Docs tab needs to be themed separately.

Add docs property to the parameters export in .storybook/preview.js

docs: {
theme: yourTheme,
}

That is it for storybook for now. We will revisit the .storybook folder to wrap its components with redux provider and react-router.

Format, Lint, and Type Check!

Waiting for GitHub Co-pilot? Before that, we have to make sure we use these co-pilots (or pair programmers) to their best.

Prettier

Install the below VSCode extension.

  • Prettier — Code formatter by Prettier

You can go to user settings in VSCode and configure your formating preferences. But we need project-level formatting rules. Let's set them up.

Go to the prettier playground. Select the options in the left panel and click the Copy config JSON button at the bottom.

Then, in the project root folder, create a file called .prettierrc.json and paste the copied content inside it.

Now VSCode will automatically read this prettier configuration over your user settings in VSCode.

Create the below scripts in package.json

...
"prettier": "prettier --ignore-path .gitignore \"**/*.{js,jsx,ts,tsx,json}\"",
"format:check": "npm run prettier -- --check",
"format:fix": "npm run prettier -- --write",
...

Now we can run format:check to check all formating issues in the project. Similarly, we can run format:fix to automatically format all the files.

Note that both the scripts use the common prettier script and extends it with -- followed by the flags --check and --write respectively.

ESLint

Create react app comes configured with eslint. But we need to set up eslint in our project as if we can add and remove additional rules to make the lining stricter to catch more issues!

npx eslint --init

This will start with a questionnaire. I have given the below answers but I suggest you play around giving various answers and see what your .eslintrc.js file brings.

Make sure to commit so you can come back in time to try a different eslint configuration.

Let's create a script in package.json.

"lint": "eslint --ext .ts --ext .tsx ."

And run npm run lint

Woah. So many errors. The majority of them are complaining about the semi-colon we asked prettier not to include! So how to make eslint configured with prettier settings? We have eslint-config-prettier!

npm i -D eslint-config-prettier

And update the extends property in .eslintrc.json as below.

...
extends: ['plugin:react/recommended', 'standard', 'prettier'],
...

Make sure to place the prettier as the last one as it needs to overwrite the rules provided by the other extensions.

Better many formating-related errors that conflicted with prettier are gone.

Add jest to Eslint’s env property.

env: {
browser: true,
es2021: true,
jest: true,
}

Update the below in rules property in the .eslintrc.json file as below.

...
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'no-unused-vars': 'warn',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'error',
},
...

Rules:

  • react/react-in-jsx-scope: With the new transform, you can use JSX without importing React. Hence turned off.
  • react/prop-types: We rely on our types heavily and we don’t use prop-types. Hence turned off.
  • no-unused-vars: This is a helpful rule but developing with no-unused-vars set to error is very hard. Hence I downgraded it to warn.
  • no-use-before-define: I turned off the JS version of it and turned on the typescript version. I found this rule complaining about the key in the typescript index signature.

ESLint is a very powerful tool to have in our project. We may need to include a new extension, plugin, and so on but the rules property is the important one to remember. We can add and remove individual eslint rules here to make out linting so strict.

Also, add this setting in eslintrc.json to satisfy a warning Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration.

settings: {
react: {
version: 'latest',
}
}

Type Checking

We initialized our project having typescript in the template. We already have tsconfig.json file in our root. I’m going to add some compilerOptions.

// compilerOptions in tsConfig.json
...
"baseUrl": "."
"noImplicitAny": true,
...
  • We set the baseUrl to '.' pointing to the location of tsConfig.json (root). This lets us import stuff like src/components/atoms from the root which makes the imports very clean.
  • We set noImplicityAny to true. Because any is bad!

Create the below script in package.json. Note we don’t have to point the tsconfig.json file explicitly.

"type:check": "tsc"

That’s it. Try creating a type issue like

const a = 9
a = 99

and run npm run type:check and see TS catch the error like a dog catching its ball! Or maybe this example is unnecessary. Let's move on.

Validate in parallel!

We have our formating, linting, and type checking scripts ready. Wouldn’t be great if we can run them all in parallel with one command?

Let's do it. First, install the below dev dependency.

npm i -D npm-run-all

And create this script below.

"validate": "npm-run-all --parallel type:check format:check lint"

And run npm run validate to run our formatting, linting, and type checking scripts in parallel. How cool is that!

Note:

To set the baseURL for the storybook’s webpack configuration, Add the below inside webpackFinal (.storybook/main.js) before returning the config object.

config.resolve.modules = [...(config.resolve.modules || []),path.resolve(__dirname, '../')]

Defend Early

Ok, now we have our validate script. When should we run it? We can run it in our CI/CD pipeline. But I would like to have the script run on every commit.

The formating, linting, and type issues will never leave the developer’s machine!

I’m going to use lint-staged. Run the below command that uses mrm@2.

npx mrm@2 lint-staged

That does the below things.

  • Installs husky and lint-staged.
  • Creates pre-commit git hook.
  • Creates lint-staged configuration in package.json.

The setup is almost done. But let's dictate the lint-staged to run which commands on pre-commit.

Create lint-staged.config.js file in the root and paste the below.

module.exports = {
'*.{ts,tsx}': (filenames) => [
'npm run format:fix',
'npm run validate'
],
}

Notes:

  • The commands in the array run sequentially one after another.
  • We run format:fix to automatically fix the staged files to our prettier config (So format:check inside validate never fails.)
  • lint staged runs git add . in the end, so we don’t have to do it ourselves. (Remember? format:fix will update files.)

Now we have our dedicated file for lint-staged, go ahead and delete the lint-staged in package.json file.

Now, let's test our lint-staged setup by committing our code.

I made both the eslint and typescript made with the below in App.tsx.

const a = 9
a = 99

Of course, VSCode is yelling at us about the issue.

But, let's proceed to commit the code.

Good. Our validate script is working fine in the pre-commit hook.

Let's test it with a formatting issue. Mess up the App.tsx and save the file without formatting(CMD+SHIFT+P and select File: save without formating )

How cool is that!

Lint staged can also detect empty git commits and stop the commit.

I highly encourage you to play around with the validate script by creating more issues.

Commit Lint

How to encourage developers to give meaningful commit messages? Let's do it.

We use commitlint for this. Follow the link for the documentation. Or run the below commands.

# Install commitlint cli and conventional config
npm install --save-dev @commitlint/{config-conventional,cli}
# Configure commitlint to use conventional config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# Add hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Now let’s try to commit using an unformatted commit message.

The commit failed with two errors.

  • subject may not be empty.
  • type may not be empty.

You can go to the get help link provided and get clarified about the format that commitlint accepts. But I’m going to install commitizen to help us with a questionnaire that improves the quality of our commit messages.

config.commitizen key can be added to the root of package.json but I prefer to keep the config separate. Run the below command to create a .czrc file.

echo "{ \"path\": \"cz-conventional-changelog\" }" > .czrc

And to commit, run one of the below commands

  • npx cz
  • npx git-cz
  • Or you can also install commitizen globally npm install commitizen -g and run cz, git-cz or git cz.

Now, commitizen will start asking simple questions.

It asks six questions for every commit.

That is it. Commit lint is done.

Redux

React itself is very functional. Look at the below example.

const [count, setCount] = useState<number>(0)
  • count: It is read-only. We cannot mutate count directly.
  • setCount: Set the value by returning a new object/value.

What about the other parts of react?

  • Props are immutable.
  • As discussed,useState, useReducer hooks have immutable states.
  • Side effects: Side effects are neatly packed only within useEffect hook.

Then why redux? Redux is probably not needed for simple applications. For enterprise-level applications, redux provides structure.

I use redux to identify the domains the application deals with and encapsulate all logic within each domain. Redux toolkit calls this a slice.

Redux Devtool’s time travel feature has been a huge help for me too. Looking at every action getting dispatched makes finding and fixing bugs a breeze compared to otherwise.

Install the below dependencies.

npm i @reduxjs/toolkit react-redux

Create store/index.ts file and paste the below.

The reducer property we pass to configureStore is important. Each slice we create, we hook it to this reducer property. Right now I have only one example slice called counter.

Kindly go to the below link and copy those files.

Next, we have to wrap the entire application with the Provider by react-redux passing the store we export in store/index.ts.

// Add these imports at the top.
import { Provider } from 'react-redux'
import { store } from './store'
// And wrap the <App />.
<Provider store={store}>
<App />
</Provider>

I modified the appearance of App.tsx and the result looks like below.

Github actions

We use Github actions to defend our remote repository.

Workflow

Let's create the workflow file in .github/workflows/node.js.yml with the below content.

Protect the main branch

Settings -> Branches -> Branch protection rules -> Click Add rule

Enter the branch name and select the below rules.

protection rules for the main branch.

The rules are self-explanatory and they protect the main branch from developers pushing code directly into it. It needs someone to review the PR.

So, if we try to push directly into the main branch, we will get this error.

So, let's create adevelop branch.

git checkout -b developgit push --set-upstream origin develop

We should see the Github action running inside the Actions tab in the Github repository page.

Tangent: Custom render function

My workflow failed with the below error.

FAIL src/App.test.tsxcould not find react-redux context value; please ensure the component is wrapped in a <Provider>

My test does only one thing.

import { render } from '@testing-library/react'
import App from './App'
test('renders', () => {
render(<App />)
})

Remember we wrapped our <App/> with the <Provider store={store}>...</Provider> from react-redux? We need to wrap the App here too.

Let's create a reusable custom render function that wraps the necessary providers.

Update the test file like below.

import { renderWithProviders } from 'src/utils/testUtils'
import App from './App'
test('renders', () => {
renderWithProviders(<App />)
})

Now even if we replace redux providers with something else like an apollo client-provider or react-query provider, our tests don't know.

Warnings as errors

Now our npm run build step failed with the below error.

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.
Failed to compile. src/App.tsxLine 18:10: 'text' is assigned a value but never used no-unused-vars
Line 18:16: 'textSetter' is assigned a value but never used no-unused-vars

In the linting section, I said that setting no-unused-vars to error will make the development hard. So, we have to fix the warnings before we push.

Collaborator

Add a collaborator to the project.

Settings -> Manage Access -> Add People

And one PR’s the added ones will get to review.

That’s it. Submit a review and do the merge request.

npm run analyze

"preanalyze": "npm run build","analyze": "npx source-map-explorer 'build/static/js/*.js'",

Dependency Cruise

Initiate depcruise

npx depcruise --init

Answer a few questions before the CLI ends with ✔ Successfully created ‘.dependency-cruiser.js’.

Have the below scripts.

"depcheck": "npx depcruise --config .dependency-cruiser.js src","depcheck:graph": "npx depcruise --include-only '^src' --output-type dot src | dot -T svg > dependencygraph.svg",

You can instantly run depcheck to find the dependency violations.

Although depcheck:graph needs dot to be installed. You can use Graphviz. brew install graphviz

complete dependency graph of our project

I find this diagram, not just a dependency graph but it gives a clear mental model of how various parts of our application interact with each other.

Cloc

"cloc": "npx cloc src"

Chromatic

How to keep track of all visual changes that happen on each code change? It could be purely visual with some messedup CSS or it could be functional too.

Once after pushing the application to production I found the arrows of a reused carrousel missing!

The developers can only keep track of a few things manually. Errors happen. Edge cases happen. Arrows go missing. We need some one to keep track of UI changes to the pixel level.

Chromatic is one such tool which works well with storybook.

Just go to the below URL and you should be able to follow the instructions easily.

  • Login/Create account in chromatic.com.
  • Add project.
  • Choose from github or create a project.
  • npm install -D chromatic
  • npx chromatic — project-token='id’ Replace id.
  • Then chromatic will ask you to create some UI change and run the above command again.

I introduced some extra padding and changed the border color.

// from
<div className='p-4 border'>Some component!!</div>
// to
<div className='p-5 border border-red-900'>Some component!!</div>

And chromatic showed me this!

How cool is this?

Chromatic in CI/CD

We want this automated UI checking to happen on each code push. Lets implement that in our ci/ci pipeline.

Refer this link for how to implement chromatic in our github actions.

Add the below as a step in our node.js.yml file.

- name: Publish to Chromatic
uses: chromaui/action@v1
# Chromatic GitHub Action options
with:
token: ${{ secrets.GITHUB_TOKEN }}
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN e7d1b1895737 }}

And add fetch-depth for chromatic to retrieve the git history under actions/checkout@v2.

- uses: actions/checkout@v2
with:
fetch-depth: 0 # 👈 Required to retrieve git history

Add the chromatic id given as a secret in github settings -> Secrets -> New repository secret.

Now our pull request is showing a pending check in UI tests.

You can click the details and approve the changes in chromatic. The merge PR will automatically turn green!

Happy merging PRs confidently!

Firebase

Login to firebase and create a new project.

We use firebase for the below services.

  • Auth
  • Firestore
  • Storage
  • Hosting

In this boilerplate we will setup auth and hosting.

Auth

Enable email password authentication in the console (Authentication -> Signin method)

Please find the user slice with auth actions and listeners from the below link.

Hosting

# Install firebase globally
npm i -g firebase
# Install firebase in the project
npm i firebase
# Login
firebase login
# Initializing hosting
firebase init hosting
# Answer a bunch of questions and voila. Firebase hosting will be setup for you including the ci/cd workflows.

Commit your code and push to the repository to start your firebase workflows.

Sentry

Go to sentry.io and create a new project.

  • Choose platform
  • Set alert settings

You will be clearly guided on what to install and how to initiate sentry in our react project.

Once setup, we have to break the application to see if sentry is working. It is actually hard to write a bug with our listing and type checking in place. But we can @ts-ignore our way to crash our application.

# Have these states.
const obj = { msg: 'Objects are not valid as a React child' }
const [text, setText] = useState<string>(JSON.stringify(obj))
# Then render text in jsx....
{text}
{/** @ts-ignore */}
<button onClick={() => setText(obj)}>Break the world</button>
...

On clicking Break the world button, our application crashes and we get alerted in sentry dashboard like below.

Application crash reported in sentry dashboard

Sentry error boundary

Ok, sentry is tracking our application crashes, but lets make the application crash gracefully using error boundary.

Currently (in production build) the application just turns blank with no explanations.

This is after implementing error boundary.

crash gracefully with sentry error boundary.

We can make this error message look much cooler that suits the application’s overall tone.

And with showDialog option in <Sentry.ErrorBoundary> we can get the below dialog box like below.

But, if don’t want to expose this to our customers, this can be useful in early stages of our application where the clients, testers and developers can record how the application broke.

Thats it for sentry.

Cypress

We will use react-testing-library for unit and integration testing. Cypress is great for testing the critical user flows interacting with our backend exactly like our user.

# Install dependenciesnpm i -D cypress @testing-library/cypress eslint-plugin-cypress
  • cypress: That command will install cypress as dev dependency.
  • @testing-library/cypress: The documentation says ‘The more your tests resemble the way your software is used, the more confidence they can give you.’
  • eslint-plugin-cypress: To satisfy eslint.

Generate sample tests

# Simply run this in the terminal.
./node_modules/.bin/cypress open
# or have a script in package.json
"cypress:open" : "cypress open"
# and run this
npm run cypress:open

Executing cypress open for the first time will generate a whole bunch of example cypress spec files. But there are a lot of lint errors. Lets fix them.

Update eslint

  • Add ‘plugin:cypress/recommended’, to extends array in eslintrc.js.
  • Add ‘cypress/no-unnecessary-waiting’: ‘warn’ in rules.
  • To fix a unused-expression problem by chai, add the below in the overrides.
overrides: [{
files: ['*.test.js', '*.spec.js'],
rules: {
'no-unused-expressions': 'off',
},
}]

Update tsconfig

  • Add “types”: [“cypress”, “@testing-library/cypress”] in compilerOptions.

Configure testing library

Cypress Testing Library extends Cypress's cy commands. Add the below line to your project’s cypress/support/commands.js.

import '@testing-library/cypress/add-commands';

Why testing library?

I absolutely love how testing library wants us to mimic actual user while interacting with the ui.

Look at the below two pictures the first without testing library and notice how the second (cy with testing library) provides really useful helpers that lets us access elements in an accessible way.

Without dom testing library:

With testing library:

We can delete the sample test files generated. But I choose to rename the 1-getting-started and 2-advanced-examples to .1-getting-started and .2-advanced-examples to keep them in the repository. Having the dot in the folder name gets these tests skipped while running.

Thank you.

--

--

Fullstack engineer @ atem.green | React, Typescript, Redux, JavaScript, UI, Storybook, CSS, UX, Cypress, CI/CD.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Karthick Ragavendran

Fullstack engineer @ atem.green | React, Typescript, Redux, JavaScript, UI, Storybook, CSS, UX, Cypress, CI/CD.