React Native - Expo, Nativewind, react-native-reusables starter with Eslint, Tsconfig
This blog guides you to setup a universal mobile app repository using following tech stack:
React Native
Expo (Router)
Nativewind v4 (community mobile "port" of tailwind css)
react-native-reusables (community mobile "port" of ShadcnUI, Radix UI)
With some personal take in configuring Eslint, Tsconfig, Prettier etc.
Init repo
pnpx create-expo-app@latest --template tabs@50
Fix node_modules
linking for pnpm
specifically by adding a .npmrc
at root:
# .npmrc
node-linker=hoisted
Then follow official tutorial from https://www.nativewind.dev/v4/getting-started/expo-router
Also setup Typescript https://www.nativewind.dev/v4/getting-started/typescript
tsconfig.json and absolute path imports
Modify app.json
// app.json
{
"expo": {
//...
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true
}
}
}
Modify tsconfig.json
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
},
"jsx": "react-native",
"types": ["nativewind/types"],
"target": "ES2022",
"lib": ["dom", "dom.iterable", "ES2022"],
"allowJs": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"incremental": true,
"noUncheckedIndexedAccess": true,
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"nativewind-env.d.ts",
],
}
Eslint
pnpm i -D eslint eslint-config-prettier eslint-config-universe eslint-plugin-prettier eslint-plugin-unused-imports prettier
.eslintignore
/.expo
node_modules
.eslintrc.json
{
"extends": [
"universe/native",
"prettier"
],
"plugins": ["unused-imports"],
"rules": {
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
]
}
}
Prettier
.prettierrc
{
"singleQuote": true,
"arrowParens": "always",
"trailingComma": "es5",
"tabWidth": 2,
"jsxSingleQuote": true,
"bracketSameLine": true,
"endOfLine": "auto"
}
.prettierignore
please adjust accordingly as your project grows
/.expo
node_modules
ios
android
generated
dist
.expo
.expo-shared
web-build
graphql.schema.json
Example rewrite of tabOneScreen
import { Text, View } from 'react-native';
import EditScreenInfo from '../../components/EditScreenInfo';
export default function TabOneScreen() {
return (
<View className='flex-1 items-center justify-center bg-white dark:bg-black'>
<Text className='text-xl font-bold'>Tab One</Text>
<View className='my-[30] w-4/5 border-t-[1px] border-gray-300 dark:border-gray-50' />
<EditScreenInfo path='app/(tabs)/index.tsx' />
</View>
);
}
react-native-reusables
A ShadcnUI like library for React Native. Copy necessary codes from the repo (https://github.com/mrzachnugent/react-native-reusables) and install expo dev client if necessary (https://docs.expo.dev/build/introduction/).
Theming
This is same to tailwind css theming https://tailwindcss.com/docs/theme
To create colors, I recommend https://uicolors.app/create
Button component example
This gives utilities to create customized button variants. And we can customize the button from every lower level detail.
@/components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { useColorScheme } from 'nativewind';
import * as React from 'react';
import { Platform, Pressable, Text, View } from 'react-native';
import * as Slot from '@/lib/rn-primitives/slot/slot-native';
import { cn, isTextChildren } from '@/lib/utils';
const buttonVariants = cva(
'flex-row items-center justify-center rounded-md web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2',
{
variants: {
variant: {
default: 'bg-primary',
destructive: 'bg-destructive',
outline: 'border border-input bg-background',
secondary: 'bg-secondary',
ghost: '',
link: '',
},
size: {
default: 'px-4 py-2 native:px-6 native:py-3.5',
sm: 'px-3 py-1 px-3 native:py-2',
lg: 'px-8 py-1.5 px-8 native:py-4',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const buttonTextVariants = cva('font-medium', {
variants: {
variant: {
default: 'text-primary-foreground',
destructive: 'text-destructive-foreground',
outline: 'text-foreground',
secondary: 'text-secondary-foreground',
ghost: 'text-foreground',
link: 'text-primary underline',
},
size: {
default: 'text-sm native:text-xl',
sm: 'text-xs native:text-lg',
lg: 'text-base native:text-2xl',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});
const rippleColor = (isThemeDark: boolean) => {
const secondary = isThemeDark ? 'hsl(240 4% 16%)' : 'hsl(240 5% 96%)';
return {
default: isThemeDark ? '#d4d4d8' : '#3f3f46',
destructive: isThemeDark ? '#b91c1c' : '#f87171',
outline: secondary,
secondary: isThemeDark ? '#3f3f46' : '#e4e4e7',
ghost: secondary,
link: secondary,
};
};
const Button = React.forwardRef<
React.ElementRef<typeof Pressable>,
React.ComponentPropsWithoutRef<typeof Pressable> &
VariantProps<typeof buttonVariants> & {
textClass?: string;
androidRootClass?: string;
}
>(
(
{
className,
textClass,
variant = 'default',
size,
children,
androidRootClass,
disabled,
...props
},
ref
) => {
const { colorScheme } = useColorScheme();
const Root = Platform.OS === 'android' ? View : Slot.Pressable;
return (
<Root
className={cn(
Platform.OS === 'android' && 'flex-row rounded-md overflow-hidden',
Platform.OS === 'android' && androidRootClass
)}>
<Pressable
className={cn(
buttonVariants({
variant,
size,
className: cn(
className,
disabled && 'opacity-50 web:cursor-default'
),
})
)}
ref={ref}
android_ripple={{
color: rippleColor(colorScheme === 'dark')[variant as 'default'],
borderless: false,
}}
disabled={disabled}
{...props}>
{isTextChildren(children)
? ({ pressed, hovered }) => (
<Text
className={cn(
hovered && 'opacity-90',
pressed && 'opacity-70',
buttonTextVariants({ variant, size, className: textClass }),
disabled && 'opacity-100'
)}>
{children as string | string[]}
</Text>
)
: children}
</Pressable>
</Root>
);
}
);
Button.displayName = 'Button';
export { Button, buttonTextVariants, buttonVariants };
Subscribe to my newsletter
Read articles from James Zhang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
James Zhang
James Zhang
Founding Engineer at Bazar (General Catalyst backed), Ex Meta Ads MLE, Ex Activision Blizzard Intern