Setup React Native with Typescript, Expo Router, Tailwind, & Testing Library in 2023image
All Stories

Setup React Native with Typescript, Expo Router, Tailwind, & Testing Library in 2023

Recently, I started working on a new React Native project. I haven't worked with React Native in over a year, and the ecosystem has changed a lot in that time. There are tons of new recommended tools and ways of starting a project.

So, in this post I'm going to walk you through how I setup a modern React Native boilerplate project using the following technologies:

If this stack is interesting to you and you'd just like to use it, the blank starter template is available on my Github.

Let's get started by setting up...

⚒️ Expo + Typescript + Expo Router

To create our new Expo project, we're going to use the Expo CLI and run

npx create-expo-app --template

This will bring up a dialog where we can select from a variety of pre-defined Expo projects. Select the one called Navigation (Typescript)

    Blank
    Blank (TypeScript)
❯   Navigation (TypeScript)
    several example screens and tabs using react-navigation and TypeScript
    Blank (Bare)

This will create a new React Native / Expo project with a lot of our boilerplate already preinstalled and configured. This could take a little while.

Once it's done installing, open up the project in your editor and head to the package.json or cd into your new project directory and run cat package.json. It should look something like this:

❯ cat package.json
{
  "name": "blog-post-app",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "test": "jest --watchAll"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "@expo/vector-icons": "^13.0.0",
    "@react-navigation/native": "^6.0.2",
    "expo": "~48.0.9",
    "expo-font": "~11.1.1",
    "expo-linking": "~4.0.1",
    "expo-splash-screen": "~0.18.1",
    "expo-status-bar": "~1.4.4",
    "expo-system-ui": "~2.2.1",
    "expo-web-browser": "~12.1.1",
    "expo-router": "^1.0.0-rc5",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.71.4",
    "react-native-safe-area-context": "4.5.0",
    "react-native-screens": "~3.20.0",
    "react-native-web": "~0.18.10"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/react": "~18.0.14",
    "jest": "^29.2.1",
    "jest-expo": "~48.0.0",
    "react-test-renderer": "18.2.0",
    "typescript": "^4.9.4"
  },
  "private": true
}

Notice in our dependencies and devDependencies that this template already comes with expo-router pre-configured so we get that aspect of our stack for free!

jest, jest-expo and react-test-renderer are also already installed and configured. So, let's extend this testing setup by installing react-native-testing-library.

✅ React Native Testing Library

Before we setup testing library, let's make it so that we can write our jest tests in Typescript. To do that, we just need to install a few packages:

npm run --save-dev ts-jest @types/jest @types/react-test-renderer

Easy! Now, let's set up support for testing library and its custom native matchers by installing a few more packages:

npm install --save-dev @testing-library/react-native @testing-library/jest-native

Next, remove the "jest" configuration key from our package.json and move it into a jest.config.js file at the root of our project. It should look something like this:

// jest.config.js
/** @type {import('jest').Config} */

const config = {
	preset: "jest-expo",
};

module.exports = config;

Add the following option to the jest.config.js so that we can use the additional matchers from testing library:

// jest.config.js
/** @type {import('jest').Config} */

const config = {
	preset: "jest-expo",
	setupFilesAfterEnv: ["@testing-library/jest-native/extend-expect"]
};

module.exports = config;

Now, go ahead and rename the example test in the project from StyledText-test.js to StyledText.test.tsx and paste in the following content:

import { MonoText } from "../StyledText";
import { render, screen } from "@testing-library/react-native";

it(`renders correctly`, () => {
	render(<MonoText>Hello</MonoText>);
	expect(screen.getByText("Hello")).toBeTruthy();
});

Now, run npm run test and you should see our test suite run correctly and be passing. Yay!

💨 Tailwind / NativeWind

The last time I worked with React Native, I used Tailwind React Native Classnames to style components using Tailwind's framework. However, there's a new kid on the block that takes the cake when trying to work with TailwindCSS and React Native: NativeWind.

Instead of running a function inside our components to generate style properties from Tailwind's utility classes, NativeWind takes a different approach. It uses babel to convert the className prop on our components to the native style prop during transpilation and compilation. It's really slick and extremely performant.

So, let's set it up! Run the following to install NativeWind and Tailwind.

npm install nativewind
npm install --save-dev tailwindcss

Next, run Tailwind's init command like you would normally.

npx tailwindcss init

And update the content parameter in your tailwind.config.js to have Tailwind and NativeWind scan for styles in the right directories for your project. For me, I'll be styling components in the expo-router app directory and my components folder.

// tailwind.config.js
module.exports = {
  content: [
    "./app/**/*.{js,jsx,ts,tsx}",
    "./components/**/*.{js,jsx,ts,tsx}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Next, add the nativewind/babel plugin to the babel.config.js.

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      "nativewind/babel",
      require.resolve("expo-router/babel")
    ],
  };
};

To be able to use the className attribute on React Native components without Typescript yelling at us, we need to add a global.d.ts file with the following content:

/// <reference types="nativewind/types" />`

And just like that, we can style our React Native components using Tailwind! Be sure to check NativeWind's documentation to see a list of supported and unsupported utilities.

💅 Prettier + ESLint

Next, let's enforce code style with Prettier and ESLint. We'll start by installing ESLint into our project's dev dependencies.

npm install --save-dev eslint

After that, we can run the installation wizard like so.

npm init @eslint/config
? How would you like to use ESLint? …
  To check syntax only
  To check syntax and find problems
❯ To check syntax, find problems, and enforce code style
? What type of modules does your project use? …
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these
? Which framework does your project use? …
❯ React
  Vue.js
  None of these
? Does your project use TypeScript? No / › Yes

👇 Note: in the next question about where our code runs, be sure to select Node and deselect Browser. React Native does not run in a browser environment so Node is the better option here.

? Where does your code run? …  (Press <space> to select, <a> to toggle all, <i> to invert selection)
  Browser
✔ Node

When you reach the question about defining a style guide, I recommend going through the prompts with the second option and choosing your preferences. This will generate a more customizable configuration with less bloat.

? How would you like to define a style for your project? …
  Use a popular style guide
❯ Answer questions about your style

After you answer the prompts, choose Javascript as your config file format.

? What format do you want your config file to be in? …
❯ JavaScript
  YAML
  JSON

And you're good! If you want to, you can install a few more plugins to enforce some best practices like I'm doing below:

npm install --save-dev eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react-hooks

If you followed along with me, your .eslintrc.js file should look something like this.

module.exports = {
  env: {
    es2021: true,
    node: true,
    jest: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
  ],
  overrides: [],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  plugins: ["react", "react-hooks", "@typescript-eslint"],
  rules: {
    indent: ["error", 2, { SwitchCase: 1 }],
    "linebreak-style": ["error", "unix"],
    quotes: ["error", "double", { avoidEscape: true }],
    semi: ["error", "always"],
    "no-empty-function": "off",
    "@typescript-eslint/no-empty-function": "off",
    "react/display-name": "off",
    "react/prop-types": "off",
    "react/react-in-jsx-scope": "off",
    "react/no-unescaped-entities": "off",
  },
  settings: {
    react: {
      version: "detect",
    },
  },
};

Be sure to add a .eslintignore file at the root of your project as well so that ESLint doesn't attempt to lint things it can't.

// .eslintignore
node_modules
assets

To make linting easier from the CLI, add a script to your package.json.

// package.json
{
  ...
  "scripts": {
    ...
    "lint": "eslint ."
    ...
  }
  ...
}

And finally, run that command with the --fix flag to automatically conform our project to our new linting settings.

npm run lint -- --fix

Now that we have ESLint setup, let's install Prettier and some additional plugins to make working with ESLint and Tailwind easier:

npm install --save-dev prettier eslint-plugin-prettier prettier-plugin-tailwindcss

Next, add prettier to the plugins list in your .eslintrc.js, and tell ESLint to throw an error if our code isn't formatted correctly.

// .eslintrc.js
module.exports = {
  ...
  plugins: ["react", "react-hooks", "@typescript-eslint", "prettier"],
  ...
  rules: {
    ...
    "prettier/prettier": "error",
    ...
  }
}

Next, add a format script to the package.json that runs prettier across our project.

// package.json
{
  ...
  "scripts": {
    ...
    "format": "prettier ."
    ...
  }
  ...
}

Run that command with the --write flag in order to conform all of our files to our new Prettier specification.

npm run format --- --write

✨ Go forth and conquer

And that's it! You've now setup all the boilerplate you need to start working on your next React Native project in 2023. I hope this article was helpful. If it was, let me know on Twitter or LinkedIn. You can also email me if social media isn't your thing.

Also, if you want to get notified the next time I post an article like this, you can sign up for my newsletter using the form down below.

Until next time.

Best,
Drew Lyton
Drew Lyton
Friday, March 24, 2023

I'm a software engineer, ex-founder and writer who cares deeply about creating a better relationship between technology and society. I'm currently writing a book about early stage entrepreneurship.

LET'S KEEP THE CONVERSATION GOING

If this article has been valuable and you'd like to be notified the next time I post, subscribe to my newsletter!

Each week, I share my latest post along with three other works from people smarter than me on whatever topic is top of mind. Of course, you can unsubscribe at anytime.