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 };