Setup React Native with Typescript, Expo Router, Tailwind, & Testing Library in 2023
Let’s setup a modern React Native project using the best tools out there 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:
- React Native w/ Expo
- Typescript
- Expo Router
- Jest
- React Native Testing Library
- TailwindCSS (via NativeWind)
- ESLint
- Prettier
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.