Skip to content

Rules and Conventions

In order to enforce a consistent code style and avoid common issues in the codebase, we have a set of rules and conventions that we follow and enforce through the starter.

This starter uses TypeScript to provide type safety and avoid common bugs in the codebase. The project configuration is based on Expo config with some updates to support absolute imports.

If you are not familiar with Typescript, you can check this article to learn more about it : Typescript for React Developers

You can find more information about it here.

We follow kabab-case for naming files and folders as we think it’s the most readable and consistent way to name files and folders in large projects and it’s the most common way to name files and folders in the react native community.

Example of kabab-case naming: my-component.tsx

For naming variables, functions, classes, interfaces, and enums, we follow camelCase as it’s the most common way to name variables in the React community. It is enforced by the linter, as you cannot create a function component without using camelCase.

Using a linter is a must in any JavaScript project. For starters, we are using ESLint with @antfu/eslint-config and some custom rules to ensure that we are following the rules and conventions related to file naming, Tailwind CSS classes, TypeScript types, import order, internationalization files, and more. This config also handles code formatting, so Prettier is no longer needed.

Here is the complete ESLint configuration file:

eslint.config.mjs
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import antfu from '@antfu/eslint-config';
import betterTailwindcss from 'eslint-plugin-better-tailwindcss';
import i18nJsonPlugin from 'eslint-plugin-i18n-json';
import reactCompiler from 'eslint-plugin-react-compiler';
import testingLibrary from 'eslint-plugin-testing-library';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default antfu(
{
// Enable React and TypeScript support
react: true,
typescript: true,
// Disable JSON processing for translation files (handled by i18n-json plugin)
jsonc: false,
// Use ESLint Stylistic for formatting
stylistic: {
indent: 2,
quotes: 'single',
semi: true,
},
// Global ignores
ignores: [
'dist/*',
'node_modules',
'__tests__/',
'coverage',
'.expo',
'.expo-shared',
'android',
'ios',
'.vscode',
'docs/',
'cli/',
'expo-env.d.ts',
'migration/*',
],
},
// Custom rules
{
rules: {
'max-params': ['error', 3],
'max-lines-per-function': ['error', 110],
'react/display-name': 'off',
'react/no-inline-styles': 'off',
'react/destructuring-assignment': 'off',
'react/require-default-props': 'off',
'react-refresh/only-export-components': 'warn', // Too strict for React Native
'unicorn/filename-case': [
'error',
{
case: 'kebabCase',
ignore: [
'/android',
'/ios',
'README.md',
'README-project.md',
'ISSUE_TEMPLATE.md',
'PULL_REQUEST_TEMPLATE.md',
],
},
],
'node/prefer-global/process': 'off', // process is commonly used in React Native configs
'ts/no-require-imports': 'off', // Sometimes needed for mocks
'ts/no-use-before-define': 'off', // Allow forward references in React components
'no-console': 'off', // Console is useful for debugging
'no-cond-assign': 'off', // Allow assignment in conditions when intentional
'regexp/no-super-linear-backtracking': 'off', // Relax regex performance rules
'regexp/no-unused-capturing-group': 'off', // Allow unused capturing groups
},
},
// TypeScript-specific rules
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'ts/consistent-type-definitions': ['error', 'type'], // Prefer type over interface
'react-hooks/refs': 'off', // Allow useRef without exhaustive-deps
'ts/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
disallowTypeAnnotations: true,
},
],
},
},
// Better TailwindCSS plugin
{
files: ['**/*.{js,jsx,ts,tsx}'],
...betterTailwindcss.configs.recommended,
settings: {
'better-tailwindcss': {
entryPoint: path.resolve(__dirname, './src/global.css'),
},
},
rules: {
...betterTailwindcss.configs.recommended.rules,
'better-tailwindcss/no-unnecessary-whitespace': 'warn',
'better-tailwindcss/no-unknown-classes': 'warn',
'better-tailwindcss/enforce-consistent-line-wrapping': 'off', // Can be too strict for some cases
},
},
// React Compiler plugin
{
plugins: {
'react-compiler': reactCompiler,
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
// i18n JSON validation
{
files: ['src/translations/*.json'],
plugins: { 'i18n-json': i18nJsonPlugin },
processor: {
meta: { name: '.json' },
...i18nJsonPlugin.processors['.json'],
},
rules: {
...i18nJsonPlugin.configs.recommended.rules,
'i18n-json/valid-message-syntax': [
2,
{
syntax: path.resolve(
__dirname,
'./scripts/i18next-syntax-validation.js',
),
},
],
'i18n-json/valid-json': 2,
'i18n-json/sorted-keys': [2, { order: 'asc', indentSpaces: 2 }],
'i18n-json/identical-keys': [
2,
{ filePath: path.resolve(__dirname, './src/translations/en.json') },
],
// Disable conflicting rules for i18n JSON files
'style/semi': 'off',
'style/comma-dangle': 'off',
'style/quotes': 'off',
'unused-imports/no-unused-vars': 'off',
},
},
// Testing Library rules
{
files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
plugins: { 'testing-library': testingLibrary },
rules: {
...testingLibrary.configs.react.rules,
},
},
);

The starter comes with a set of git hooks that help us to enforce rules and conventions. Those hooks are configured using Husky. and here is the complete list of the hooks:

As the name suggest, this hook will run before each commit and it will make sure you are not committing directly on the main branch and it will run the linter and typescript checking on the staged files.

.husky/pre-commit
. "$(dirname "$0")/common.sh"
echo "===\n>> Checking branch name..."
# Check if branch protection is enabled
if [[ -z $SKIP_BRANCH_PROTECTION ]]; then
BRANCH=$(git rev-parse --abbrev-ref HEAD)
PROTECTED_BRANCHES="^(main|master)"
if [[ $BRANCH =~ $PROTECTED_BRANCHES ]]; then
echo ">> Direct commits to the $BRANCH branch are not allowed. Please choose a new branch name."
exit 1
fi
else
echo ">> Skipping branch protection."
fi
echo ">> Finish checking branch name"
echo ">> Linting your files and fixing them if needed..."
pnpm type-check
pnpm lint-staged

As the name suggest, this hook will run after each merge and it will check if there is any changed in pnpm-lock.yaml and if there is any, it will run pnpm install to make sure the dependencies are up to date.

.husky/post-merge
function changed {
git diff --name-only HEAD@{1} HEAD | grep "^$1" >/dev/null 2>&1
}
echo 'Checking for changes in pnpm-lock.yml...'
if changed 'pnpm-lock.yml'; then
echo "📦 pnpm-lock.yml changed. Run pnpm install to bring your dependencies up to date."
pnpm install
fi
echo 'You are up to date :)'
echo 'If necessary, you can run pnpm prebuild to generate native code.'
exit 0

This hook will check if the commit message is following the conventional commit format. If it’s not, the commit will be aborted and will show you what going wrong with your commit message.

.husky/commit-msg
pnpm commitlint --edit $1

We are using commitlint to check if the commit message is following the conventional commit format.

In general, your commit message should follow this format:

Terminal window
type(scope?): subject #scope is optional; multiple scopes are supported (current delimiter options: "/", "\" and ",")

Real world examples can look like this:

fix(ui): fix input width
feat(ui): add button variants
feat(api): add usePost query hook

type should be one of the following: build, chore, ci ,docs,feat,fix, perf, refactor, revert, style or test.