eslint-config-uta
v3.12.2
Published
Modular ESLint configuration for UTA (Universal Template Architecture) projects - architecture, imports, React best practices for web and mobile
Maintainers
Readme
eslint-config-uta
Enforce clean architecture in Next.js, React Native, and Expo projects - Stop architecture violations, circular dependencies, and framework anti-patterns before they ship.
Supports: Next.js 13+ (App Router) • React Native • Expo • Remix
What Does It Do?
Catches these problems in your code:
// ❌ Architecture violation - feature importing another feature
import { UserCard } from '@/features/profile' // in features/dashboard
// ❌ Circular dependency
import { validateUser } from '@/features/user' // which imports from here
// ❌ Security risk - server code in client component
'use client'
import { db } from 'server-only' // Leaks to client bundle!
// ❌ Performance issue - inline styles in React Native
<View style={{ padding: 16 }} /> // New object every render
// ❌ Bug - hooks in async Server Component
export default async function Page() {
const [state, setState] = useState(0) // Runtime error!
}
// ❌ Crash risk - Zod .parse() throws
const user = UserSchema.parse(data) // Unhandled error crashes appAll auto-detected and reported by ESLint.
Quick Start
Install
pnpm add -D eslint-config-utaInstall Optional Plugins (Based on Your Stack)
# Next.js projects (recommended)
pnpm add -D @tanstack/eslint-plugin-query eslint-plugin-zod
# React Native projects
pnpm add -D eslint-plugin-react-nativeNote: Most peer dependencies (TypeScript, React, Import plugins) are auto-installed by pnpm/npm 7+. Only optional ones need manual installation.
Configure
Next.js:
// eslint.config.mjs
import { dirname } from "path"
import { fileURLToPath } from "url"
import { FlatCompat } from "@eslint/eslintrc"
import { createNextJSRecommendedPreset } from "eslint-config-uta/presets/nextjs-recommended"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({ baseDirectory: __dirname })
export default [
...compat.extends("next/core-web-vitals", "next/typescript"),
...createNextJSRecommendedPreset({
extendsNextJSConfig: true, // Required when using FlatCompat
includeTanStackQuery: true,
includeZod: true,
includeRSC: true,
}),
]React Native:
// eslint.config.mjs
import { createReactNativeRecommendedPreset } from 'eslint-config-uta/presets/react-native-recommended'
export default createReactNativeRecommendedPreset()Run
pnpm lint
pnpm lint --fix # Auto-fix importsCore Features
1. Architecture Boundaries
Enforces the three-layer UI architecture:
app/ → Can import: features, ui, core
features/ → Can import: ui, core (NOT other features)
ui/business/ → Can import: ui/patterns, ui/foundation, core
ui/patterns/ → Can import: ui/foundation, core
ui/foundation → Can import: core only
core/domains/ → Can import: core/shared only
core/shared/ → No internal importsCatches:
- Upward imports (foundation importing from business)
- Feature-to-feature coupling
- Core importing from UI
- Circular dependencies
Rules:
uta/enforce-layer-boundariesuta/no-cross-feature-importsuta/enforce-barrel-exportsuta/no-deep-relative-imports
2. Next.js Server Components
Prevents React Server Component bugs:
// ❌ Server imports in Client Component (security)
'use client'
import { db } from 'server-only'
// ✅ Fixed
// Don't import server code in client components
// ❌ Hooks in async components (runtime error)
export default async function Page() {
const [state, setState] = useState(0)
}
// ✅ Fixed - add 'use client'
'use client'
export default function Page() {
const [state, setState] = useState(0)
}
// ❌ Missing 'use client' directive
export default function Counter() {
const [count, setCount] = useState(0) // Hooks need 'use client'
}
// ✅ Fixed
'use client'
export default function Counter() {
const [count, setCount] = useState(0)
}Rules:
uta/nextjs-no-server-only-in-client(auto-fixable)uta/nextjs-no-hooks-in-server-component(auto-fixable)uta/nextjs-enforce-use-client(auto-fixable)@next/next/no-async-client-component@next/next/no-html-link-for-pages@next/next/no-img-element
3. TanStack Query Best Practices
Prevents stale data and performance issues:
// ❌ Missing cache invalidation
const mutation = useMutation({
mutationFn: updateUser,
// Cache never updates!
})
// ✅ Invalidate queries on success
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
// ❌ Missing query key dependencies
const { data } = useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(userId) // userId not in key!
})
// ✅ Exhaustive dependencies
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})
// ❌ Unstable QueryClient
function App() {
const queryClient = new QueryClient() // New every render!
return <QueryClientProvider client={queryClient}>...</>
}
// ✅ Stable reference
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</>
}Rules:
uta/tanstack-require-mutation-invalidation@tanstack/query/exhaustive-deps@tanstack/query/stable-query-client
4. Zod Validation Safety
Prevents runtime crashes:
// ❌ .parse() throws on invalid data
const user = UserSchema.parse(data) // Crashes app!
// ✅ .safeParse() returns error object
const result = UserSchema.safeParse(data)
if (!result.success) {
return { error: result.error.message }
}
// ❌ Schema in wrong location
// features/user/UserForm.tsx
const UserSchema = z.object({ ... })
// ✅ Schemas in core/domains
// core/domains/user/schemas/user.schema.ts
export const UserSchema = z.object({ ... })
// ❌ Verbose union syntax
const StatusSchema = z.union([
z.literal('active'),
z.literal('inactive')
])
// ✅ Concise enum
const StatusSchema = z.enum(['active', 'inactive'])Rules:
uta/zod-prefer-safe-parseuta/zod-schema-locationzod/prefer-enumzod/require-strict(optional)
5. React Native Performance
Critical mobile performance rules:
// ❌ Inline styles (creates new object every render)
<ScrollView>
{items.map(item => (
<View style={{ padding: 16, margin: 8 }} />
))}
</ScrollView>
// ✅ StyleSheet.create (optimized)
const styles = StyleSheet.create({
item: { padding: 16, margin: 8 }
})
<ScrollView>
{items.map(item => <View style={styles.item} />)}
</ScrollView>
// ❌ .map() in ScrollView (no virtualization)
<ScrollView>
{users.map(user => <UserCard user={user} />)}
</ScrollView>
// ✅ FlatList (virtualized)
<FlatList
data={users}
renderItem={({ item }) => <UserCard user={item} />}
keyExtractor={item => item.id}
/>
// ❌ Hardcoded colors
const styles = StyleSheet.create({
text: { color: '#FF0000' }
})
// ✅ Theme colors
import { colors } from '@/styles/theme'
const styles = StyleSheet.create({
text: { color: colors.error }
})
// ❌ Raw text (runtime error)
<View>Hello</View>
// ✅ Text wrapper
<View><Text>Hello</Text></View>Rules:
uta/rn-no-inline-styles-performanceuta/rn-prefer-flatlist-for-listsreact-native/no-inline-stylesreact-native/no-color-literalsreact-native/no-raw-textreact-native/no-unused-styles
6. Import Organization
Auto-sorts imports with framework awareness:
// ❌ Messy imports
import { Button } from '@/ui/foundation/button'
import { useState } from 'react'
import './styles.css'
import type { User } from './types'
// ✅ Auto-sorted (run: eslint --fix)
import { useState } from 'react'
import { Button } from '@/ui/foundation/button'
import type { User } from './types'
import './styles.css'Rules:
simple-import-sort/imports(auto-fixable)simple-import-sort/exports(auto-fixable)import/no-cycle(circular dependency detection)@typescript-eslint/consistent-type-imports
Configuration Options
Preset Functions
All presets accept these options:
createNextJSRecommendedPreset({
// Severity level
severity: 'error', // 'error' | 'warn'
// Tech stack features (requires plugins)
includeTanStackQuery: true, // Requires @tanstack/eslint-plugin-query
includeZod: true, // Requires eslint-plugin-zod
includeRSC: true, // Next.js Server Components rules
// Plugin conflict prevention
extendsNextJSConfig: true, // Set true when using FlatCompat
})Custom Directory Structure
If your project doesn't use @/ or src/:
Option 1: Config file (recommended)
// uta.config.cjs
module.exports = {
architecture: {
pathAlias: '~',
layers: {
core: 'lib/core',
ui: 'lib/components',
},
},
}// eslint.config.mjs
import { createArchitectureConfig } from 'eslint-config-uta/plugins/uta/utils/architecture-factory.js'
export default [
...createArchitectureConfig({}, { enableConfigFile: true }),
]Option 2: Inline settings
import { createArchitectureConfig } from 'eslint-config-uta/plugins/uta/utils/architecture-factory.js'
export default [
...createArchitectureConfig({
pathAlias: '~',
layers: {
core: 'lib/core',
ui: 'lib/components',
},
}),
]Supported config files:
uta.config.cjs.utarc/.utarc.jsonpackage.json(under "uta" key)
Available Presets
Recommended (Balanced)
import { createNextJSRecommendedPreset } from 'eslint-config-uta/presets/nextjs-recommended'
import { createReactNativeRecommendedPreset } from 'eslint-config-uta/presets/react-native-recommended'Includes: Architecture + Framework rules + Tech stack (optional plugins)
Strict (More Rigorous)
import { createNextJSStrictPreset } from 'eslint-config-uta/presets/nextjs-strict'
import { createReactNativeStrictPreset } from 'eslint-config-uta/presets/react-native-strict'Adds: Explicit return types, stricter TypeScript, strict Zod mode
Minimal (Architecture Only)
import { createMinimalPreset } from 'eslint-config-uta/presets/minimal'Only architecture boundary rules. For gradual adoption.
Troubleshooting
Missing Peer Dependency
Error: Cannot find package '@tanstack/eslint-plugin-query'Solution 1: Install the plugin
pnpm add -D @tanstack/eslint-plugin-querySolution 2: Disable the feature
createNextJSRecommendedPreset({
includeTanStackQuery: false,
})Plugin Conflict Error
Error: Plugin "react" was conflictedCause: Using FlatCompat with Next.js config registers plugins twice.
Solution: Set extendsNextJSConfig: true
createNextJSRecommendedPreset({
extendsNextJSConfig: true, // Prevents duplicate plugin registration
})Peer Dependencies Not Auto-Installing
pnpm users: Add to .npmrc:
auto-install-peers=trueThen reinstall:
rm -rf node_modules pnpm-lock.yaml
pnpm installNote: Only required peers auto-install. Optional peers (TanStack Query, Zod, React Native) need manual installation.
ESLint 8 Legacy Config
Recommended: Upgrade to ESLint 9 flat config.
If you must use ESLint 8:
// .eslintrc.js
const { architectureConfig } = require('eslint-config-uta')
const utaPlugin = require('eslint-config-uta/plugin')
module.exports = {
plugins: ['uta'],
overrides: architectureConfig.map(config => ({
files: config.files,
rules: config.rules,
})),
}Gradual Adoption
Start with warnings, upgrade to errors when ready:
export default [
...createNextJSRecommendedPreset({
severity: 'warn', // Start here
}),
// Override specific rules
{
rules: {
// Enforce these immediately
'uta/nextjs-no-server-only-in-client': 'error',
// Keep as warnings for now
'uta/enforce-layer-boundaries': 'warn',
},
},
]Migration steps:
- Install with
severity: 'warn' - Run
eslint --fixto auto-fix imports - Fix remaining warnings
- Upgrade to
severity: 'error'
Performance
Optimized for large codebases:
- Single-pass AST traversal per rule
- Minimal file system operations
- Efficient regex path matching
- Framework detection: <5ms per project
Benchmark:
- ~0.5-1ms overhead per file
- Scales to 1000+ file projects
- No noticeable impact on ESLint runtime
FAQ
Which preset should I use?
Next.js App Router: nextjs-recommended
React Native/Expo: react-native-recommended
Gradual adoption: minimal (architecture only)
Start with recommended. Upgrade to strict when your team is ready for stricter TypeScript rules.
Can I disable specific rules?
Yes. Override in your config:
export default [
...createNextJSRecommendedPreset(),
{
rules: {
'uta/enforce-barrel-exports': 'off',
'simple-import-sort/imports': 'warn',
},
},
]Does this work with monorepos?
Yes. Each package can have its own eslint.config.mjs and uta.config.cjs.
What peer dependencies do I need?
Auto-installed (required):
- TypeScript ESLint plugins
- React plugins
- Import plugins
Manual install (optional, based on features you enable):
@tanstack/eslint-plugin-query(forincludeTanStackQuery: true)eslint-plugin-zod(forincludeZod: true)eslint-plugin-react-native(for React Native projects)
Why so many peer dependencies?
Design choice: Each project chooses its plugin versions. This prevents:
- Version conflicts
- Duplicate plugin installations
- Breaking changes forced on users
Modern package managers (pnpm, npm 7+) auto-install required peers. Only optional ones need manual installation.
Contributing
Contributions welcome! Please:
- Fork and create a feature branch
- Add tests in
plugins/uta/tests/ - Run
npm test - Update CHANGELOG
- Submit PR
See CONTRIBUTING.md for detailed guidelines.
Links
License
MIT © UpdateTheApp
Made with ❤️ by the UpdateTheApp team
