Skip to main content

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 UI
  • useRef: 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: wrapper prefix 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 items
  • onEndReached - Handler for pagination
  • onEndReachedThreshold - Threshold for pagination (0.2)
  • onRefresh - Handler for pull-to-refresh
  • refreshing - Refreshing state
  • loading - Initial loading state
  • hasMore - Whether there are more pages
  • ListFooterComponent - Footer component with pagination indicator
  • removeClippedSubviews - 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
}