Frontend Best Practices and Conventions
useRef vs useState
Rule: If it doesn't need to cause a re-render in the component, always use useRef.
useState: Use when the value needs to reflect in the UIuseRef: Use for values that change but don't affect rendering (timers, internal counters, DOM references)
Example:
// ❌ Causes unnecessary re-render
const [renderCount, setRenderCount] = useState(0);
// ✅ Doesn't cause re-render
const renderCount = useRef(0);
useEffect: A Warning Sign
In most cases, using useEffect indicates a problem in the application's logic flow.
Consider alternatives:
- Calculate values directly during render
- Use event handlers instead of synchronization
- Move logic to where the event actually happens
When useEffect is really necessary:
- Synchronization with external systems (APIs, subscriptions)
- Side effects that cannot be avoided
❌ NEVER use useEffect for data fetching
// ❌ Old and problematic pattern
useEffect(() => {
fetch('/api/users').then(setUsers);
}, []);
✅ Use TanStack Query instead
// ✅ With TanStack Query (React Query)
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
// ✅ Or with @codeleap/query (QueryManager)
const usersQuery = APIClient.User.usersManager.useList();
Why TanStack Query?
- Automatic caching and smart invalidation
- Managed loading/error/success states
- Automatic retry and background refetch
- Eliminates 60-80% of state management code
Logic Separation
Components with many functions and states should be refactored into custom hooks.
Why?
- Easier maintenance
- Improved readability
- Enables reusability
- Simplifies testing
Example:
// ❌ Everything in the component
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
// ... 50 lines of logic
}
// ✅ Extracted logic
function UserProfile() {
const { user, loading, updateUser } = useUserData();
// Component focused only on UI
}
Components Are UI
Fundamental principle: Components should be responsible only for the interface.
- Business rules → Custom hooks or services
- Components → Presentation and interaction
Benefits:
- Reusable components
- Easy to test logic separately
- Cleaner and more organized code
useCallback and useMemo
Use these tools for optimization and to avoid unnecessary recalculations on re-renders.
useCallback: Memoizes functions
const handleSubmit = useCallback(() => {
// function that doesn't need to be recreated on every render
}, [dependencies]);
useMemo: Memoizes calculated values
const expensiveValue = useMemo(() => {
return calculateSomethingComplex(data);
}, [data]);
⚠️ Warning: Don't optimize prematurely. Use only when:
- Passing callbacks to optimized child components (React.memo)
- Really expensive calculations
- Dependencies of other hooks
Naming Conventions
Clear names avoid confusion and facilitate maintenance. Follow consistent patterns throughout the project.
Components
// ✅ PascalCase for components
function UserProfile() {}
function ProductCard() {}
function NavigationMenu() {}
// ❌ Avoid
function userProfile() {}
function product_card() {}
Custom Hooks
// ✅ Always start with "use"
function useUserData() {}
function useFormValidation() {}
function useDebounce() {}
// ❌ Doesn't work without the "use" prefix
function getUserData() {}
function formValidation() {}
Event Handlers
// ✅ "handle" prefix for internal functions
function handleClick() {}
function handleSubmit() {}
function handleUserDelete() {}
// ✅ "on" prefix for callback props
<Button onClick={handleClick} />
<Form onSubmit={handleSubmit} />
<UserCard onDelete={handleUserDelete} />
Boolean Variables
// ✅ Prefixes that indicate boolean
const isLoading = true;
const hasError = false;
const canEdit = true;
const shouldRender = false;
// ❌ Ambiguous
const loading = true;
const error = false;
Files
// ✅ Components: PascalCase
UserProfile.tsx
ProductCard.tsx
// ✅ Hooks: camelCase with "use" prefix
useUserData.ts
useDebounce.ts
// ✅ Utils/Services: camelCase
apiClient.ts
formatDate.ts
validators.ts
Props and Componentization
Good componentization and proper use of props make code cleaner and more reusable.
Avoid Props Drilling
// ❌ Passing props through several levels
function App() {
const [user, setUser] = useState(null);
return <Parent user={user} setUser={setUser} />;
}
function Parent({ user, setUser }) {
return <Child user={user} setUser={setUser} />;
}
function Child({ user, setUser }) {
return <GrandChild user={user} setUser={setUser} />;
}
// ✅ Use Context for global data
const UserContext = createContext();
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Parent />
</UserContext.Provider>
);
}
function GrandChild() {
const { user, setUser } = useContext(UserContext);
// Direct access to data
}
Props Destructuring
// ✅ Destructure props in the parameter
function UserCard({ name, email, avatar, isActive }) {
return (
<div>
<img src={avatar} />
<h3>{name}</h3>
<p>{email}</p>
</div>
);
}
// ❌ Using props.something makes code more verbose
function UserCard(props) {
return (
<div>
<img src={props.avatar} />
<h3>{props.name}</h3>
<p>{props.email}</p>
</div>
);
}
Default Values
// ✅ Destructuring with default values
function Button({
variant = 'primary',
size = 'medium',
children
}) {
return <button className={`btn-${variant} btn-${size}`}>{children}</button>;
}
// ✅ Or use defaultProps (class/function components)
Button.defaultProps = {
variant: 'primary',
size: 'medium'
};
TypeScript
TypeScript prevents bugs and improves the development experience. Use it correctly.
Using Types
Always use type instead of interface for consistency.
// ✅ Use type for everything - objects, unions, primitives, tuples
type User = {
id: string;
name: string;
email: string;
}
type UserCardProps = {
user: User;
onEdit: (id: string) => void;
}
// ✅ Types for unions and literals
type Status = 'idle' | 'loading' | 'success' | 'error';
type Coordinates = [number, number];
type ID = string | number;
// ✅ Extend types with intersection
type Admin = User & {
role: 'admin';
permissions: string[];
}
// ❌ Avoid interfaces
interface User {
name: string;
}
Typing Component Props
// ✅ Explicit and clear typing
type ButtonProps = {
children: React.ReactNode;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
function Button({ children, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
);
}
// ✅ With generics when necessary
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <div>{items.map(renderItem)}</div>;
}
Avoid Any
// ❌ Any eliminates TypeScript benefits
function processData(data: any) {
return data.value; // No type safety
}
// ✅ Use specific types
type Data = {
value: string;
count: number;
}
function processData(data: Data) {
return data.value; // Type-safe
}
// ✅ Use unknown when you really don't know the type
function handleApiResponse(response: unknown) {
if (typeof response === 'object' && response !== null) {
// Type narrowing
}
}
Utility Types
// ✅ Use built-in utility types
type User = {
id: string;
name: string;
email: string;
password: string;
}
// Partial - makes all properties optional
type UserUpdate = Partial<User>;
// Pick - selects specific properties
type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
// Omit - removes properties
type UserWithoutPassword = Omit<User, 'password'>;
// Record - creates object with typed keys and values
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// ReturnType - extracts return type
function getUser() {
return { id: '1', name: 'John' };
}
type UserData = ReturnType<typeof getUser>;
Event Handlers
// ✅ Type events correctly
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
console.log(event.target.value);
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
}
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log(event.currentTarget);
}
Hooks with TypeScript
// ✅ useState with explicit type when necessary
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
// ✅ Typed useRef
const inputRef = useRef<HTMLInputElement>(null);
const timeoutRef = useRef<number | null>(null);
// ✅ Typed custom hooks
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
return [value, setValue] as const; // Tuple type
}
Component Organization
File Structure
Organize components by feature, not by type. Keep related files close together:
src/
├── components/
│ ├── auth/ # Authentication-specific components
│ │ ├── LoginForm.tsx
│ │ ├── SignupForm.tsx
│ │ └── AuthProvider.tsx
│ ├── chat/ # Chat functionality components
│ │ ├── MessageList.tsx
│ │ ├── MessageInput.tsx
│ │ ├── ChatProvider.tsx
│ │ └── context.ts
│ ├── layout/ # Layout components
│ │ ├── Header.tsx
│ │ ├── TabBar.tsx
│ │ └── Page.tsx
│ └── shared/ # Truly shared components
│ ├── Button.tsx
│ ├── Input.tsx
│ └── Avatar.tsx
├── scenes/ # Screen/Page components
│ ├── Home/
│ │ ├── index.tsx
│ │ ├── HomeHeader.tsx
│ │ └── RecentPosts.tsx
│ ├── Profile/
│ │ ├── index.tsx
│ │ ├── ProfileAbout.tsx
│ │ └── ProfileEdit.tsx
│ └── Navigation.tsx
├── hooks/ # Custom hooks
│ ├── useDebounce.ts
│ ├── useCamera.ts
│ └── useKeyboard.ts
├── services/ # External integrations
│ ├── api/
│ │ ├── posts.ts
│ │ ├── users.ts
│ │ └── comments.ts
│ ├── analytics/
│ └── notifications/
├── stores/ # Global state
│ ├── auth.ts
│ └── appStatus.ts
├── types/ # Type definitions
│ ├── models.ts
│ ├── api.ts
│ └── navigation.ts
└── utils/ # Utility functions
├── date.ts
├── format.ts
└── validation.ts
State Management
Global State (@codeleap/store)
Use @codeleap/store for all application global state. It's built on nanostores with a simpler API.
// stores/auth.ts
import { globalState } from '@codeleap/store'
type AuthState = {
user: User | null
isAuthenticated: boolean
isLoading: boolean
}
export const state = globalState<AuthState>({
user: null,
isAuthenticated: false,
isLoading: true,
})
// Actions as named functions
export function setUser(user: User) {
state.set({
user,
isAuthenticated: true,
isLoading: false,
})
}
// Custom hooks for selectors
export function useAuth() {
return state.use()
}
export function useUser() {
return state.use(s => s.user)
}
Server State (@tanstack/react-query)
Use createQueryManager for CRUD operations and createQueryOperations for custom mutations.
QueryManager - For complete CRUD
Use when you have a resource with standard Create, Read, Update, Delete operations:
// services/api/posts.ts
import { createQueryManager } from '@codeleap/query'
type Post = {
id: string
title: string
content: string
author: User
createdAt: string
}
type PostFilters = {
authorId?: string
search?: string
}
export const postsManager = createQueryManager<Post, PostFilters>({
name: 'posts',
listFn: async (limit, offset, filters) => {
const response = await api.get<PaginatedResponse<Post>>('/posts', {
params: { limit, offset, ...filters }
})
return response.data
},
retrieveFn: async (id) => {
const response = await api.get<Post>(`/posts/${id}`)
return response.data
},
createFn: async (data) => {
const response = await api.post<Post>('/posts', data)
return response.data
},
updateFn: async (id, data) => {
const response = await api.patch<Post>(`/posts/${id}`, data)
return response.data
},
deleteFn: async (id) => {
await api.delete(`/posts/${id}`)
},
})
QueryOperations - For mutations or custom queries
Use for actions that are not standard CRUD (e.g., like, share, etc):
// services/api/posts.ts
import { createQueryOperations } from '@codeleap/query'
export const postsOperations = createQueryOperations()
.mutation('toggleLike', async (postId: string) => {
const response = await api.post(`/posts/${postId}/like`)
return response.data
})
.mutation('share', async (postId: string) => {
const response = await api.post(`/posts/${postId}/share`)
return response.data
})
State Colocation
Most underused principle: Keep state as close as possible to where it's used.
// ❌ State too high in the tree
function App() {
const [modalOpen, setModalOpen] = useState(false);
// All children re-render when modal opens
return (
<>
<Header />
<Sidebar />
<Content modalOpen={modalOpen} />
<Footer />
</>
);
}
// ✅ State colocated where it's used
function Content() {
const [modalOpen, setModalOpen] = useState(false);
// Only Content re-renders
return (
<div>
<Modal open={modalOpen} />
</div>
);
}
Proper state colocation can drastically improve performance without any memoization.
Form State (@codeleap/form)
Use @codeleap/form for all forms. Prefer field-level validation with Zod.
// app/forms/auth.ts
import { form, fields, zodValidator } from '@codeleap/form'
import { z } from 'zod'
export const login = form('login', {
email: fields.text({
label: 'Email',
placeholder: 'Enter your email',
validate: zodValidator(
z.string()
.min(1, 'Email is required')
.email('Invalid email address')
),
keyboardType: 'email-address',
autoCapitalize: 'none',
}),
password: fields.text({
label: 'Password',
placeholder: 'Enter your password',
secure: true,
validate: zodValidator(
z.string()
.min(8, 'Password must be at least 8 characters')
),
autoCapitalize: 'none',
}),
})
Styling System
Naming Convention
Use wrapper for containers as the styles name
// Common component structure
<View style={styles.wrapper}>
<View style={styles.innerWrapper}>
<Text style={styles.text}>Content</Text>
</View>
</View>
Variants - For Reusable Styles
Use variants for styles that will be shared across multiple places in the app.
Variants go in /app/stylesheets/ and are registered globally:
// app/stylesheets/Button.ts
export const ButtonStyles = {
default: createButtonVariant((theme) => ({
wrapper: {
backgroundColor: theme.colors.primary,
padding: theme.spacing.md,
borderRadius: theme.radius.md,
},
text: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.md,
},
})),
secondary: createButtonVariant((theme) => ({
wrapper: {
backgroundColor: theme.colors.surface,
borderWidth: 1,
borderColor: theme.colors.border,
},
text: {
color: theme.colors.primary,
},
})),
}
<Button style="default" text="Save" />
<Button style="secondary" text="Cancel" />
createStyles - For Specific Styles
Use createStyles for component or page-specific styles.
// scenes/Profile/Edit.tsx
import { createStyles } from '@codeleap/styles'
const styles = createStyles((theme) => ({
wrapper: {
flex: 1,
backgroundColor: theme.colors.background,
},
innerWrapper: {
padding: theme.spacing.lg,
},
avatarContainer: {
alignItems: 'center',
marginBottom: theme.spacing.xl,
},
form: {
gap: theme.spacing.md,
},
}))
// Usage in component
export function ProfileEdit() {
return (
<View style={styles.wrapper}>
<View style={styles.innerWrapper}>
<View style={styles.avatarContainer}>
<Avatar />
</View>
<View style={styles.form}>
<TextInput />
<TextInput />
</View>
</View>
</View>
)
}
Summary:
- Variants (
/app/stylesheets/) = Reusable across places - createStyles (in the file itself) = Component/page-specific
- Convention:
wrapperprefix for structure
Performance Optimization
List Optimization
Use useCallback to avoid unnecessary re-renders in lists.
function OptimizedList() {
const renderItem = useCallback(({ item }: { item: Post }) => (
<PostCard post={item} />
), [])
const keyExtractor = useCallback((item: Post) => item.id, [])
return (
<FlatList
data={posts}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
)
}
Lists with Pagination, Loading, and States
Use useQueryList to manage lists with pagination, loading, pull-to-refresh, and states automatically.
The useQueryList hook works together with QueryManager.useList() and returns props ready to spread on FlatList/List:
- Automatic pagination (infinite scroll)
- Loading states (initial, pagination, refresh)
- Pull-to-refresh
- Pagination indicator (footer)
- Automatic cache and invalidation
import { useQueryList } from '@/hooks'
import { APIClient } from '@/services'
function PostsList() {
// 1. Get the list from QueryManager
const postsList = APIClient.Post.postsManager.useList({
filters: {
authorId: userId,
search: searchTerm
},
})
// 2. Pass to useQueryList
const listProps = useQueryList(postsList, {
noMoreItemsText: 'No more posts', // Text when there are no more items
forceLoading: false, // Force loading state (optional)
forceRefreshing: false, // Force refreshing state (optional)
forceHasNextPage: false, // Force hasNextPage (optional)
})
const renderItem = useCallback(({ item }: { item: Post }) => (
<PostCard post={item} />
), [])
// 3. Spread the props on List/FlatList
return (
<Page
component={List}
{...listProps}
renderItem={renderItem}
ItemSeparatorComponent={() => <View style={['separator']} />}
/>
)
}
The hook returns the following props ready for use:
data- Array of itemsonEndReached- Handler for paginationonEndReachedThreshold- Threshold for pagination (0.2)onRefresh- Handler for pull-to-refreshrefreshing- Refreshing stateloading- Initial loading statehasMore- Whether there are more pagesListFooterComponent- Footer component with pagination indicatorremoveClippedSubviews- Optimization (true)
Internationalization (i18n)
Lingui Configuration
Use Lingui macros for all user-facing text.
// Usage in component
import { useLingui } from '@lingui/react/macro'
function LoginScreen() {
const { t } = useLingui()
return (
<Page>
<Text text={t`Welcome back!`} />
<TextInput placeholder={t`Email`} />
<TextInput placeholder={t`Password`} />
<Button text={t`Sign in`} />
</Page>
)
}
// Usage with variable interpolation
function ProfileAbout() {
const { t } = useLingui()
const dateString = dayjs(timestamp).format('MMM Do YYYY')
const currentYear = dayjs().format('YYYY')
return (
<View>
<Text text={t`Copyright © ${currentYear} ${Settings.CompanyName}`} />
<Text text={t`This page was updated on ${dateString}`} />
</View>
)
}
Translating Static Messages
For service files (not components), use @lingui/core/macro:
// services/api/errors.ts
import { t } from '@lingui/core/macro'
const errors = {
'auth/wrong-password': t`Incorrect email or password`,
'auth/email-in-use': t`This email address is already in use`,
'auth/user-not-found': t`Incorrect email or password`,
'auth/too-many-requests': t`Access temporarily blocked due to too many attempts`,
}
Code Organization Best Practices
Avoid Default Exports
Prefer named exports instead of default exports for better refactoring and IDE support.
// ❌ Bad
export default function Button() { }
// ✅ Good
export function Button() { }
Path Aliases
IMPORTANT: Always stop at the folder level, never go to the file. React Native may crash if you import directly from the file with path alias.
// ❌ BAD - relative imports
import { Button } from '../../../components/Button'
// ❌ BAD - goes to the file (may cause CRASH)
import { Button } from '@/components/Button'
import { useAuth } from '@/stores/auth'
// ✅ GOOD - stops at folder level
import { Button } from '@/components'
import { useAuth } from '@/stores'
import { formatDate } from '@/utils'
Code Comments
Use comments only when another developer needs to know something important that isn't obvious in the code - like the "why" of a decision, business context, or non-obvious side effects.
If you're explaining what a function does, your function is not well written. Refactor to descriptive names and smaller functions.
// ❌ Bad - explaining the obvious
// Sets the user to null
setUser(null)
// ❌ Bad - explaining what the function does
// Validates the email and returns true if valid
function validateEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
// ✅ Good - explaining the WHY of something non-obvious
// We need to invalidate the cache before clearing the user because React Query
// keeps stale data that can reappear in navigation transitions
queryClient.clear()
setUser(null)
// ✅ Good - non-obvious side effect
// WARNING: This function triggers a webhook to the notification system
// Do not call in loops or batch operations
function sendNotification(userId: string, message: string) {
// ...
}
// ✅ Even better - self-explanatory code without comments
function isUserAllowedToPost(user: User): boolean {
const isAdult = user.age >= 18
const hasApproval = user.hasParentalApproval
return isAdult || hasApproval
}