Have you ever done any development in a team environment - anywhere it's not just one person committing code? You've probably seen inconsistent code formating, different structure, etc. You've probably even been in a few "tabs vs. spaces" debates - perhaps more than a few times on the same team 🤦‍♂️.

Several teams I've worked with have solved these problems by either using EditorConfig or Prettier in their IDEs. However, this has alway lead to someone's IDE or config file being slightly different and not always being consistent.

Another pain point in group development is with code that has cruft. Say for example, someone adds a variable or imports a library in a file and never actually uses them. Other teams might have a "rule" of "No for loops!", but they have no way to enforce it other than code reviews. We know many of our team's rules are going to slip through the cracks.

The best way to solve all these problems, improve productivity, and prevent constant bike shedding is to have automated tools enforce code quality. Don't worry, this doesn't mean you can't choose your own rules, it's just that you won't have to repeatedly insist on them during code reviews. After implementing the suggestions in this post, your code will be "perfect" - but not necessarily bug free (that's on you).

The open source tools I'll present here will save you & your team/company countless hours of bickering or developing custom tooling.

Please consider contributing to them to ensure the developers have the funding and motivation necessary to keep making these great tools.

You can find links to several of these tools on my own contribution record on Open Collective or their respective GitHub Sponsorship pages.

In this article, I'm going to walk through starting a brand new Expo tabs project (this whole article can be used for ANY React or React Native project) from scratch and do the following:

  • Configure Prettier
  • Configure Linting with TypeScript
  • Solve all the existing linting & formatting issues
  • Setup auto-commit hooks that enforce all the new configuration rules
  • Teach you how to speed up your auto-commit hooks
  • Explain the neeed for CI/CD build tools to prevent anyone from bypassing the team's rules before PRs are merged

TL;DR: You can see the final configuration on this Git repo: https://github.com/calendee/expo-lint-demo

Start an Expo Project

Our base project is going to be the default Expo "tabs" project with TypeScript.

expo init expo-lint

The Expo CLI will prompt us to choose a template and then install it for us.

NOTE: The configuration ideas from this point forward are from several of my own older blog posts and code I liberally copied from Khalil Stemmler's great series on doing similar things in TypeScript projects.

Configure ESLint for TypeScript

First, we need to get ESLint configured to support TypeScript:

yarn add --dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Now, let's add an .eslintrc file in our project's root directory so that it looks like this:

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ]
}

Next, we'll need to add an .eslintignore file to ensure our linter doesn't go to crazy and start linting every directory in our project:

node_modules
dist
.expo
.expo-shared
web-build

Now, we'll need to add a lint script to our package.json:

{
...,
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.json",
"lint-and-fix": "eslint . --ext .ts,.tsx,.js,.jsx,.json --fix",
...
}

The two script above will let you run yarn lint or yarn lint-and-fix to see/fix all the linting errors in your project.

If you run yarn lint right now, you'll see an explosion of errors/warnings 🤯!

Let's just ignore those for now and move along.

Installing and Configuring Prettier

To install Prettier, run this command

yarn add --dev prettier

Now, we need to add the magic sauce to keep all your files formatted just the way your team likes (after arguing for a few days).

Add a .prettierrc file to look something like this:

{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": false,
  "printWidth": 80
}

Now, everytime you save a file, your IDE will hopefully respect this file and format it just the way you've requested. You may need to adjust its settings to get this to work properly.

For example, Prettier will automatically convert const a = '1' to const a = "1"; every time you save the file.

However, the real magic will happen later on. Even if someone codes in VI with no IDE assistance, we'll make sure those files are formatted properly.

We also need to keep Prettier and ESLint from whacking each other over the head. Modify your .eslintrc file to look like this:

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "@typescript-eslint", "prettier"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ]
}

Finally, add a script to package.json to run prettier on all your existing files:

{
...,
"prettier-format": "prettier --config .prettierrc '**/*.{json,js,jsx,ts,tsx,css,scss,md}' --write",
...
}

This script will automatically cleanup all the files we care about. If other file types are needed, include them inside the brackets with a comma - be sure to NOT have any spaces!

Now, run yarn prettier-format and watch it automagically format many of the files in our project. Just like that, we've updated the formatting for every file in the project. Now, if a team member modifies one of those files days or weeks later, their PR won't be polluted with tons of formatting changes that hide their real changes.

Configuring React & Jest Linting Support

When we ran yarn lint earlier, there was something pretty strange in the output. It frequently pointed out things like:

  • warning 'React' is defined but never used @typescript-eslint/no-unused-vars
  • warning 'View' is defined but never used @typescript-eslint/no-unused-vars
  • warning 'TouchableOpacity' is defined but never used @typescript-eslint/no-unused-vars
  • warning 'it' is not defined
  • warning 'expect' is not defined

WTH??? The problem is that ESLint doesn't understand React and JSX. So, we need to introduce it to them.

yarn add --dev @types/react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-config-react eslint-plugin-prettier eslint-plugin-jest eslint-plugin-react

Now, once again, edit the .eslintrc file as follows:

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "@typescript-eslint", "prettier", "jest"],
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier",
    "plugin:jest/recommended"
  ],
  "rules": {
    "no-console": 1,
    "prettier/prettier": 2,
    "jest/no-disabled-tests": "warn",
    "jest/no-focused-tests": "error",
    "jest/no-identical-title": "error",
    "jest/prefer-to-have-length": "warn",
    "jest/valid-expect": "error"
  },
  "settings": {
    "react": {
      "pragma": "React",
      "version": "detect"
    }
  },
  "env": {
    "jest/globals": true
  }
}

If we run yarn lint again, there are still a ton of problems, but React ain't one of em.

Adding pre-commit Hooks with Husky

We've finally got our project configured to lint and prettify everything! Now, let's configure Husky to make sure our code can't be committed with linting issues and that all code is automatically prettified.

yarn add --dev husky

Now, modify the scripts section of package.json again:

{
...
"husky": {
  "hooks": {
    "pre-commit": "yarn prettier-format && yarn lint"
  }
},
...
}

Now, every time a developer runs git commit -m"here's my awesome code!" Husky will try to run prettier and linting to make sure that code really is "awesome".

Running that right now, results in a big, "NOT!"

Husky blocks the commit because there are a ton of errors in the code. Woohoo! Mission accomplished!

Fixing All the Linting Errors

Our new yarn lint output frequently shows issues like this:

/Users/jn/Documents/Apps/expo-lint/App.tsx
  10:16  warning  Missing return type on function  @typescript-eslint/explicit-module-boundary-types

Missing Return Type

In our IDE, the function App() is underlined to show that ESLint thinks something is wrong with this code.

Moving the mouse over the underlined code will show a hint about what is wrong:

The issue is that TypeScript really likes to have the return value of every function explicitly identified. This helps avoid bugs by ensuring any calling functions are expecting and properly using the return value of the function. It also forces a developer to seriously think about the consequences of changing the return signature of the function. If they just let type inference happen, they might not go investigate what impacts their change might have.

With React, there are several ways to solve this problem. In the sample above, ESLint helpfully displays the inferred return value of the function.  So, one possible fix to this problem is:

export default function App(): JSX.Element | null {...}

This code explicitly tells TypeScript what to expect this function to return.

Another possible refactor depending on style preferences is:

export const App: React.FunctionComponent = () => { ... }
export default App;

So, let's go through finding all these Missing return type issues and solve them like

export default function ???(): JSX.Element | null {...}

We're not done with missing return types yet 😢.

/Users/jn/Documents/Apps/expo-lint/components/Themed.tsx
7:8 warning Missing return type on function @typescript-eslint/explicit-module-boundary-types

This one's a bit different as this is just a function. We need to explicitly type the return as a string like:

export function useThemeColor(
  props: { light?: string; dark?: string },
  colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
): string {...}

And another similar one:

/Users/jn/Documents/Apps/expo-lint/hooks/useColorScheme.web.ts
  3:16  warning  Missing return type on function  @typescript-eslint/explicit-module-boundary-types

Again, this one is a bit different. Let's look at this code:

// useColorScheme from react-native does not support web currently. You can replace
// this with react-native-appearance if you would like theme support on web.
export default function useColorScheme() {
  return "light";
}

We could fix it just like this:

export default function useColorScheme(): string {
  return "light";
}

However, since this function literally can't return anything other than light, we can fix this linting issue more precisely with a "string literal".

export default function useColorScheme(): "light" {
  return "light";
}

Component Definition is Missing Display Name

Our next big issue is about the navigation/BottomTabNavigator.tsx missing a display name.

The react/display-name warning hints that the code in question is going to make debugging more difficult. When using the React debugging tools, it's very helpful for every component to have a name. This makes it easier to "see" what code is generated and search for a component in the debugger.

Normally, a developer doesn't have to provide a "display name" to their components. React automatically derives it based on the variable assigned to it.

However, in this file, the bottom tabs icon is generated in an anonymous, inline function. React can't make up a display name for it - hence the warning.

To solve this, we need to get rid of those inline functions and replace them with a component as follows:

const BottomTabTabBarIcon = ({ color }: { color: string }) => (
  <TabBarIcon name="ios-code" color={color} />
);
export default function BottomTabNavigator(): JSX.Element {
  const colorScheme = useColorScheme();

  return (
    <BottomTab.Navigator
      initialRouteName="TabOne"
      tabBarOptions={{ activeTintColor: Colors[colorScheme].tint }}
    >
      <BottomTab.Screen
        name="TabOne"
        component={TabOneNavigator}
        options={{
          tabBarIcon: BottomTabTabBarIcon,
        }}
      />
      <BottomTab.Screen
        name="TabTwo"
        component={TabTwoNavigator}
        options={{
          tabBarIcon: BottomTabTabBarIcon,
        }}
      />
    </BottomTab.Navigator>
  );
}

Tada! Problem solved!

Module is Not Defined

We've got yet another problem with the babel.config.js file. ESLint is warning:

/Users/jn/Documents/Apps/expo-lint/babel.config.js
  1:1  error  'module' is not defined  no-undef

There are several solutions to this, but this file is not very irrelavant to our code; so, we're going to just ignore it like this:

// eslint-disable-next-line no-undef
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
  };
};

React/No-Unescaped-Entities

Another issue in our yarn lint output:

/Users/jn/Documents/Apps/expo-lint/components/EditScreenInfo.tsx
   9:16  warning  Missing return type on function                                  @typescript-eslint/explicit-module-boundary-types
  42:39  error    `'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`  react/no-unescaped-entities

Let's focus on the react/no-unescaped-entities part of this. When writing HTML, developers should avoid the use of apostrophe's, quotes, etc. "But..... this is a React Native project!", you might exclaim. That's true, but thanks to Expo for Web (via React Native Web), our React Native project can also run in the browser. So, we need to be good citizens and fix our code.

Replace:

Tap here if your app doesn't automatically update after making changes

with:

Tap here if your app doesn&apos;t automatically update after making changes

There are several such issues throughout the app; so, go tackle them. This escaping works just fine in React Native views as well.

Finally, run another yarn lint and 💥! No more linting errors!

Commit All The Thingz!

We're finally done! We fixed all the linting errors.

Before committing, let's make one intentional mistake to prove our pre-commit hook prevents the commit.  In App.tsx, add this:

const a = 1;

This should cause the pre-commit hook to fail because the variable a is defined but never used, right?

git add .
git commit -m"cleanup all the code!"

Prevent Commits with ESLint Warnings

Husky let us down 🤦‍♂️!

What the heck?? Our pre-commit hook worked, prettified all the files, but it let our warning go through and commit?

The problem is that ESLint doesn't consider "warnings" as fatal errors; so, yarn lint runs and quits with an exit code of 0.  So, Husky thinks, "Looks good to me!"

We do NOT want this. The whole goal of this process is to enforce coding standards. If a developer can commit code with tons of warnings, we haven't really solved any problems. In fact, it can lead to buggy code if you attempt to use techniques like type safe switch statements.

To fix this, we need to make ESLint consider "warnings" as fatal errors. Modify the lint script in package.json to :

"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.json --max-warnings 0",`

Now, we're explicitly telling ESLint that warnings should not be allowed. When it encounters any warnings, it will exit with a status code other than 1 and our pre-commit hook will reject the commit.

Let's test to make sure it works.

Again, in App.tsx, change const a = 1; to something like const a = "1";.

Then,

git add App.tsx
git commit -m"Don't fail me now, little doggy!"

Woohoo! Husky saw there was a problem with the code and rejected the commit!

Now, just delete that offending code, recommit and you're done.

My Commits are So Slow 😡!!!

As your project grows, you'll add more and more files. When you commit, lint and prettier are going to examine every one of those files for issues. You can imagine in a very large project, it can take several seconds for your commits to get accepted. This gets really irritating, really fast.

Let's fix this by adding lint-staged to our project. With lint-staged, only the files we've touched and staged for commit will go through the linting process and be evaluated in the pre-commit hook.  This will drastically speed up your flow.

Install and configure lint-staged as follows:

npx mrm lint-staged

By default, the installation process modified our Husky config in package.json to:

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },

and added this:

  "lint-staged": {
    "*.{ts,tsx,js,jsx,json}": "eslint --cache --fix"
  }

There are 3 issues with this:

  1. I don't like automated fixes. So, I remove the --fix. I feel it's important for the developers to be warned about their mistakes and fix them manually.
  2. It removed our --max-warnings 0 flag; so, no ESLint warnings can make their way back into our code
  3. It removed our automatic prettification 😞

So, let's modify the lint-staged section of package.json to get it back the way we like:

"lint-staged": {
  "*.{json,js,jsx,ts,tsx,css,scss,md}": "prettier --config .prettierrc --write",
  "*.{ts,tsx,js,jsx,json}": "eslint --cache --max-warnings 0"
}

Now, all commit attempts will run prettier and linting checks on any files we're attempting to commit and no one can commit bad code anymore! Huzzah!

Here's what a commit looks like now:

Danger Will Robinson!

Developers are human. Sometimes, we get in a rush, stuck on a problem, or just need to "Ship It!".  So, we sometimes try to bypass the rules to get something out the door. Sometimes, we just need to commit code even though we know it's got warnings to get it pushed up for another developer to work on.

Be aware that anyone on your team can do the following to bypass the pre-commit hooks:

git add .
git commit -m"Damn the torpedoes. Full steam ahead!" --no-verify

Now, it's important to add something into your PR process (GitHub Actions, CI/CD tests, etc) to make sure this code doesn't actually get merged until it's fixed.

I sure hope this walk through helps your team get on the path of clean, consistent code. If you have suggestions or questions, ping me on Twitter.