Creating An Ultimate CRA Boilerplate
Let's create a robust boilerplate that helps us focus on application building.
Repository
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.
// 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
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
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 withno-unused-vars
set to error is very hard. Hence I downgraded it towarn
.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 likesrc/components/atoms
from the root which makes the imports very clean. - We set
noImplicityAny
to true. Becauseany
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
andlint-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 (Soformat:check
insidevalidate
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 runcz
,git-cz
orgit 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.
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
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.
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.
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.