My Overly Opinionated React App Setup
I have been developing React.js applications for about 3 years now, and in that time I have learned 2 things:
1. I am very anal about consistency in my code.
2. When working on a team, if you don’t have tools to enforce consistency, people will inevitably break the rules.
Now I won’t claim to have this down pat, but over the years I have developed an organization style I’m happy with, and found a useful set of tools to handle it for me. This will be an overview of how I set up a new React application from scratch and the tools/rules I use to keep it consistent.
Should you listen to me? That’s up to you. This is mostly aimed at people who like consistency but don’t already have some rules to keep their code organized.
You can check out the repository with the complete code for this post here: https://github.com/csandman/next-app-template
The first tool I use is Next.js, a React framework for building web applications that offers automatic SSR (Server Side Rendering) and SSG (Static Site Generation). This allows you to generate your pages before returning them to the client which can have an impact on SEO, and it generally makes the process of page routing simpler. I use the next-seo package to make per-page SEO a much simpler process.
Whether or not Next.js helps with SEO is a debated topic as Google claims that they render dynamic content before indexing pages however from what I’ve read and seen in practice is that it is much more consistent than something like create-react-app as well as being much simpler to implement. At this point, even the react team themselves are using Next.js for their new version of the React documentation.
To get started I use the create-next-app cli tool:
npx create-next-app my-web-app
Personally I use npm as my package manager over yarn (which is the default in create-next-app) but if you prefer yarn, use this command:
yarn create next-app my-web-app
Also if you prefer typescript add the
--typescript flag, but if that’s the case, a few parts of this process would need to be altered a bit.
By default the create-next-app package does not use a
src/ directory for the core app code, but I’ve always found it easier to keep my app code organized by grouping it together. That being said, the next step for me is creating a
src/ directory and moving the
styles/ folders inside of it. Another benefit of this is searching specifically in that folder from your IDE, something that VSCode and I’m sure most other IDE’s offer.
After that I set up absolute imports to the files inside my
src/ directory. To do so, you’ll need to add a
jsconfig.json file in your root directory with the following:
If you’re using typescript, you’ll need to add this to the
tsconfig.json that is automatically generated. In a later step we’ll also need to add an ESLint setting so it recognizes these imports.
To implement this change in the boilerplate code, remove the
../ from your style imports in the
import styles from 'styles/Home.module.css';
Later in this guide I’m going to set up the Airbnb style guide, which has it’s own rule about naming files:
23.6 A base filename should exactly match the name of its default export.
While I follow the airbnb style guide pretty closely, this is the one part where I differ. In my opinion, all of your files should stick with a kebab-case naming convention. This is because it aligns with pretty much every import from an NPM package I’ve ever used, and seems to be a lot more standard in my opinion. And if you ever decide to split a part of your web app into a public NPM package, it will be easier to do so if you’re already following that standard. Besides, this also aligns with Google’s JS style guide:
File names must be all lowercase and may include underscores (
_) or dashes (
-), but no additional punctuation. Follow the convention that your project uses. Filenames’ extension must be
The only change to the create-next-app boilerplate you’ll need to make to implement this is rename
styles/home.module.css and change the import in your
pages/index.js file to reflect it.
For each of my React projects I generally have the same top level directories inside my source directories:
components/This is the obvious one, I have a components directory with all of my React components, usually grouped into folders based on their place within the application.
hooks/I keep a top level hooks directory with reusable hooks that can be used throughout the application. Each hook gets its own file with the hook as the default export. For example
use-local-storage.jsexports a default
utils/I keep a utils folder with common utility functions that can be used throughout my app. I tend to keep a single file for each type of utility in an app, such as
utils/date.jswhich exports a few non-default date helper functions. You could also use a
date/sub folder which exports one function per file, but I find that often times some of my utils need to reference each other and keeping them in the same file can be simpler. Side note: I’ve spent more time than I’d like to admit trying to decide whether to use
util/because I’ve seen both used. I ended up using the plural because it matches my other top level folders.
styles/I usually keep a top level styles directory which stores the styles global to my app. In the past I’ve used
.scssfiles for this but since switching to Chakra UI (I’ll mention that next), I mostly use this directory for their global styles, and their text/layer styles. I’ve also kept a component specific sass file in each of my component folders in the past, but again, with Chakra UI I mostly use their style props system of styling so I don’t really need them.
data/I keep a top level data file for any json-like data I need in my application. Instead of a
.jsonfile however, I usually use
.jsfiles with a default export of the object I need. Even if I only need it in one specific location, I find it keeps my component files cleaner to keep this data out of my component files as it tends to stay relatively static so I don’t want to scroll through it to see my component code.
contexts/In my current project I’ve been using zustand for state management, which uses stores however for most simple applications I just use basic React contexts. So if you’re using a state management tool like zustand or redux, you can use a
stores/folder, if you use contexts, just use a
The next package I like to install is Chakra UI, an accessible component library for React with an elegant styling system. Whether or not you use a component library is completely up to you, but over time I’ve found that I prefer spending my time on the functionality of my apps rather than a core design system. This is an interesting reddit post discussing whether to use a UI framework or not.
I won’t go into detail about setting up Chakra as they have a detailed guide in their docs.
To use it we’ll just install it as a
npm i -D prettier
To get it set up to your liking, you’ll want to look over the available options and figure out if there are any you’d like to change. Next, create a
.prettierrc file in the root of your project where you’ll define your options. Personally, I think you should define all of the available options, as a lot of people have the prettier plugin installed in their editor, and if they have changed the defaults there, it will override any defaults in your project that aren’t specifically defined. Prettier has also changed their defaults for certain options in major package versions and this ensures your formatting won’t change suddenly without you noticing. This is an example of what I’d use for mine:
The only non-default options I use are
"singleQuote": true and overriding SVG files to use the html formatter, but these are just the way I personally like it and you can change it as you please.
You’ll also want to add a
.prettierignore file in the root of your project with the following to prevent it from running on your built files:
There is also a script I like to add for formatting your entire package:
"format": "prettier --write ."
This is a plugin for prettier I only recently discovered which sorts your imports based on a list of regex expressions that you define. Keeping your imports well organized can help with quickly scanning for the imports you’re looking for as your brain learns where they’re supposed to be.
To set this up you’ll need to install it as a
npm i -D @trivago/prettier-plugin-sort-imports
And then you’ll need to add some new rules to your
I have a lot of regex rules here, so I’ll explain my basic rules
- All external dependencies come before all internal dependencies, including absolutely imported ones
- The top level order of external dependencies it react -> next -> chakra as those are the dependencies I’ll be using the most and want at the top consistently. Additionally, they sort of follow an order of the main application layers.
<THIRD_PARTY_MODULES>which includes everything not matched (in this case all other external imports) I want to have each of my absolute local imports in an order that makes sense to me. To do this, I added a regex for each of the top level folders in my
src/directory. This order can be up to you. Alternatively, if you want them sorted alphabetically by folder name, combine them into one line:
- I have all relative imports at the bottom. This follows some standard ESLint rules I’ve seen and I don’t use them very often. For the most part I only use relative imports if the file I’m importing is in the same directory as the file thats importing it.
- I have
truein order to sort the destructured imports I have alphabetically, this is just visually appealing.
- I have
importOrderCaseInsensitiveset to false (the default) so that the destructured imports get sorted by components first (PascalCase variables) followed by functions (camelCase variables). This is nice because GitHub applies different syntax highlighting to each of them, and it looks more organized this way
The last two options,
"importOrderParserPlugins" I include with their default options just to keep track of the options that exist.
While this package isn’t technically a prettier plugin, it is another nice formatter which keeps your project consistent. It simply organizes your
package.json file with sensible defaults. For example, it groups your
peerDependecies so you can easily find them all.
To install it run:
npm i -D prettier-package-json
And to use it you can run
npx prettier-package-json --write or add a shortcut
"script" to your
"format:package": "prettier-package-json --write"
And tweak the options as you like! I’m pretty happy with the defaults on this one.
My personal favorite way to use prettier is by having my editor VSCode do the formatting for me on save. To do this, go to the extensions tab in VSCode and search for “prettier”, it will be the one with the most downloads.
Once it’s installed, go to the settings in VSCode and search for “Default Formatter”, and from the dropdown select “Prettier — Code Formatter”.
Next search for “Format On Save” and check the box. Now all of your files should be formatted according to your specifications each time you save a file!
ESLint has become a standard in most peoples’ code formatting toolboxes. It defines a specific set of rules for how your code should be written, that you can enable or disable as you see fit.
To set it up I normally use the script
npx eslint --init and follow the prompts to get a basic config, however ESLint comes with create-next-app by default now so it won’t work in the same way.
The eslint config for next.js already comes with a lot of good options enabled, including the recommended configs from
eslint-plugin-next. However, there are a couple more packages you should add:
npm i -D eslint-config-airbnb eslint-config-prettier
This will set you up with lint rules for the Airbnb JS Style Guide that I mentioned earlier. If you’d prefer, feel free to use the Google JS Style Guide or the Standard Style instead of Airbnb’s, but I think it’s a good idea to use a specific style guide to enforce more consistency.
This also adds the config for prettier to prevent it from conflicting with other rules. Alternatively you could use the package eslint-plugin-prettier however they specifically recommend against it, so I’d stick with the config.
Next change your
.eslintrc.json file to the following:
"prettier" as your last
"extends" option keeps it from conflicting with the next.js or airbnb rules, and adding the
"import/resolver" to your
"settings" informs ESLint about your absolute path setup so you don’t get any errors there.
I also tweak a couple rules:
"react/jsx-props-no-spreading"For the most part I agree with this rule, the point of it is to specify which props a component can receive to prevent unexpected behaviors. However, when working with Chakra UI, most of the custom components you’ll make are styled with React props, so defining all of the props a component could accept would be very cumbersome.
"react/jsx-filename-extension"By default, this rule expects you to only use jsx in components with an extension of
.jsxhowever, I prefer to use plain
.jsextensions for my components. If you’d prefer to use
.jsxthen you can leave this modification out, or if you use typescript, you have to use
.tsx. Regardless of what you choose, you should probably stick to one file extension to keep things consistent.
You’ll probably find that you don’t agree with some of the rules added, but removing them is as simple as adding
"rule-name": "off" to the
"rules" section, so decide which ones you don’t like as you go!
When you want to lint your entire project, you can use the built in lint script that comes with next:
npm run lint
I like to add an additional script though for linting and fixing any errors that might pop up:
"lint:fix": "next lint --fix"
This plugin is essential for using ESLint effectively, as VSCode will give you interactive errors when your code is breaking lint rules. To install it, go to the extensions tab and search for “ESLint”, it’ll be the plugin with over 17 million downloads.
I also like setting up VSCode to automatically fix the lint rules that it can when you save a file. To do this, open Preferences and search for “Code Actions On Save”.
It’ll give you an option to open
settings.json so click that. Then all you need to do is add the following:
This will not fix every lint error, only the ones that are simple to fix automatically, but it is a nice feature. It’s especially useful when you’re pasting in some code from a different source, as they can potentially break a lot of rules.
Husky + Lint Staged
The final piece of this puzzle is adding the packages husky, paired with lint-staged. Husky is a package for adding pre-commit hooks, and lint-staged is a package for linting and formatting code that has been staged for a git commit. I use these along with prettier, eslint, and prettier-package-json to force anyone committing to a repo to ensure their code is formatted correctly, and follows all of the rules before pushing it up to GitHub.
You may wonder what the purpose of this is, considering that you set up your IDE to format and lint fix all code on save, and there are 2 reasons. The first is preventing code from being pushed that breaks any lint rules that can’t be fixed automatically. You can always disable rules on a per-file basis but you at least have to consider the rules you’re breaking before you push. The other reason is that if you’re working on a team, it’s likely that not everyone has the same setup. This ensures that code pushed by anyone is consistent with the rules of the project.
The easiest way to set this up is to use the mrm cli tool:
npx mrm lint-staged
This is a convenient codemod tool for setting up certain boilerplate that will install both of the required dependencies and set up the
.husky/ directory. Because we are using next.js, you’ll have to make a change to make it work with
First remove the
"lint-staged" key that was added by mrm from your
package.json and add a
.lintstagedrc.js file in the root of your project. We need a JS config file to run a function on the staged files for
In this file, paste the following:
This will run all of the scripts we added above to your project on each of the files you commit. And this is it, your project should be automatically formatted and stay consistent!
Bonus: Insert Final Newline
One thing that prettier does with all of the files it formats is add a new line character at the end of every file. This provides nice consistency but it also has a practical purpose. However, this only happens for files that prettier is compatible with, so to keep all of my files consistent with this pattern, I use a specific VSCode setting.
In the preferences for VSCode, search for “Insert Final Newline” and check the box, and now all of your files will include this extra formatting!