- Introduction
- Code Structure
- Naming Conventions
- TypeScript Usage
- React Component Patterns
- Hooks Conventions
- Error Handling
- State Management
- Styling Conventions
- Memory Management
- Atomic Design Specific Rules
This document outlines the coding conventions and best practices for the React Native Atomic Design project. Following these conventions ensures code consistency, maintainability, and scalability across the codebase.
Purpose:
- Establish consistent coding standards
- Improve code readability and maintainability
- Facilitate team collaboration
- Ensure type safety and error prevention
- Guide component creation and organization
All code in this project should adhere to these conventions. When in doubt, refer to this document or discuss with the team.
The project follows a strict Atomic Design folder structure:
src/
├── views/
│ ├── atoms/ # Basic building blocks
│ ├── components/ # Composed components (molecules)
│ └── containers/ # Screen containers (organisms)
├── hooks/ # Custom React hooks
├── services/ # Business logic services
├── entities/ # Domain entities and DTOs
├── managers/ # Manager classes
├── types/ # TypeScript type definitions
└── config/ # Configuration files
Each atom or component should be in its own file:
atoms/
└── Card.tsx # Single component per file
Each container follows a consistent multi-file structure:
containers/
└── home/
├── home.screen.tsx # Logic and state management
├── home.view.tsx # Presentation component (with styled-components)
├── home.props.ts # Props interfaces
└── home.types.ts # TypeScript types
File Naming:
- Use kebab-case for container folders:
home/,profile/,settings/ - Use dot notation for container files:
home.screen.tsx,home.view.tsx - Use PascalCase for component files:
Card.tsx,RepositoryCard.tsx
While not currently implemented, barrel exports can be used to simplify imports:
// atoms/index.ts
export {Card} from './Card';
export {LoadingSpinner} from './LoadingSpinner';
export {ErrorMessage} from './ErrorMessage';
// Usage
import {Card, LoadingSpinner} from '../atoms';Guidelines:
- Use barrel exports for frequently imported components
- Keep barrel exports simple and focused
- Don't create deep barrel export chains
Atoms:
- Render a single UI element
- Accept props for customization
- No business logic
- No state management (except local UI state)
Components (Molecules):
- Combine multiple atoms
- Simple presentation logic only
- No API calls or data fetching
- No complex state management
Containers (Organisms):
- Manage business logic and state
- Coordinate data fetching via hooks
- Separate logic (screen.tsx) from presentation (view.tsx)
- Handle user interactions and side effects
Hooks:
- Encapsulate data fetching logic
- Manage loading and error states
- Provide clean API for components
Services:
- Handle API communication
- Transform DTOs to domain models
- Isolate external dependencies
Use camelCase for variables and functions:
// Good
const repositoryList = [];
const fetchRepositories = async () => {};
// Bad
const RepositoryList = [];
const FetchRepositories = async () => {};Use PascalCase for React components:
// Good
export const Card: React.FC<CardProps> = ({children}) => {};
export const RepositoryCard: React.FC<RepositoryCardProps> = ({repository}) => {};
// Bad
export const card: React.FC<CardProps> = ({children}) => {};
export const repositoryCard: React.FC<RepositoryCardProps> = ({repository}) => {};Use UPPER_SNAKE_CASE for constants:
// Good
const API_BASE_URL = 'https://api.github.com';
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_PAGE_SIZE = 10;
// Bad
const apiBaseUrl = 'https://api.github.com';
const maxRetryAttempts = 3;Use PascalCase for interfaces and types:
// Good
interface CardProps {
children: React.ReactNode;
}
type RepositoryStatus = 'loading' | 'success' | 'error';
// Bad
interface cardProps {
children: React.ReactNode;
}- Components: PascalCase -
Card.tsx,RepositoryCard.tsx - Containers: kebab-case folders, dot notation files -
home/home.screen.tsx - Hooks: camelCase with
useprefix -useRepositories.ts - Services: camelCase with
.service.tssuffix -api.service.ts - Types: camelCase with
.types.tssuffix -repository.types.ts - Utils: camelCase -
formatDate.ts,validateEmail.ts
Atoms:
- Simple, descriptive names:
Card,Button,Input,Label - No feature-specific prefixes
Components (Molecules):
- Descriptive, feature-specific:
RepositoryCard,SearchBar,FormField - Indicates what it displays or does
Containers (Organisms):
- Screen or feature name:
HomeScreen,ProfileScreen,SettingsScreen - Followed by
.screen.tsxand.view.tsxfiles
Always define types for component props, function parameters, and return values:
// Good
interface CardProps {
children: React.ReactNode;
style?: ViewStyle;
}
export const Card: React.FC<CardProps> = ({children, style}) => {
// Implementation
};
// Bad
export const Card = ({children, style}) => {
// No type safety
};Use Interfaces for:
- Component props
- Object shapes that may be extended
- Public APIs
interface CardProps {
children: React.ReactNode;
style?: ViewStyle;
}
interface RepositoryCardProps extends CardProps {
repository: GithubRepo;
}Use Types for:
- Unions and intersections
- Primitives and computed types
- Type aliases
type Status = 'loading' | 'success' | 'error';
type Nullable<T> = T | null;
type RepositoryWithStatus = GithubRepo & {status: Status};Always define props interfaces for components:
// Good - Separate props file for containers
// home.props.ts
export interface HomeViewProps {
repositories: GithubRepo[];
loading: boolean;
error: Error | null;
}
// home.view.tsx
import {HomeViewProps} from './home.props';
export const HomeView: React.FC<HomeViewProps> = ({
repositories,
loading,
error,
}) => {
// Implementation
};
// Good - Inline for simple components
interface CardProps {
children: React.ReactNode;
style?: ViewStyle;
}Use generics for reusable components and utilities:
// Good
interface BaseResponse<T> {
items: T;
total: number;
}
interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
// Usage
type RepositoryResponse = BaseResponse<GithubRepo>;Avoid type assertions when possible. Use type guards instead:
// Good - Type guard
function isError(value: unknown): value is Error {
return value instanceof Error;
}
if (isError(error)) {
console.error(error.message);
}
// Avoid - Type assertion
const error = someValue as Error;Always use functional components with hooks:
// Good
export const Card: React.FC<CardProps> = ({children, style}) => {
return <View style={[styles.card, style]}>{children}</View>;
};
// Bad - Class components are deprecated
export class Card extends React.Component<CardProps> {
render() {
return <View>{this.props.children}</View>;
}
}Build complex components by composing simpler ones:
// Good - Composition
export const RepositoryCard: React.FC<RepositoryCardProps> = ({repository}) => {
return (
<Card style={styles.container}>
<Text style={styles.name}>{repository.name}</Text>
<Text style={styles.description}>{repository.description}</Text>
</Card>
);
};
// Bad - Monolithic component
export const RepositoryCard: React.FC<RepositoryCardProps> = ({repository}) => {
return (
<View style={[styles.card, styles.container]}>
{/* Duplicated card styling */}
</View>
);
};Always destructure props in the function signature:
// Good
export const Card: React.FC<CardProps> = ({children, style}) => {
return <View style={[styles.card, style]}>{children}</View>;
};
// Acceptable for many props
export const RepositoryCard: React.FC<RepositoryCardProps> = (props) => {
const {repository} = props;
// Implementation
};
// Bad
export const Card: React.FC<CardProps> = (props) => {
return <View style={[styles.card, props.style]}>{props.children}</View>;
};Provide default values for optional props:
// Good - Default parameter
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
message = 'Loading...',
}) => {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.text}>{message}</Text>
</View>
);
};
// Or use defaultProps (less common with functional components)
LoadingSpinner.defaultProps = {
message: 'Loading...',
};Add JSDoc comments for complex components:
/**
* Card Atom
* Basic card container component with shadow and border radius
*
* @param children - Content to display inside the card
* @param style - Optional style overrides
*/
export const Card: React.FC<CardProps> = ({children, style}) => {
return <View style={[styles.card, style]}>{children}</View>;
};Use descriptive state variable names and initialize with proper types:
// Good
const [repositories, setRepositories] = useState<GithubRepo[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
// Bad
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);Always include cleanup in useEffect when needed:
// Good - With cleanup
useEffect(() => {
const subscription = someObservable.subscribe(handleUpdate);
return () => {
subscription.unsubscribe();
};
}, []);
// Good - Async operations
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
const data = await api.fetchData();
if (!cancelled) {
setData(data);
}
};
fetchData();
return () => {
cancelled = true;
};
}, []);
// Bad - No cleanup
useEffect(() => {
someObservable.subscribe(handleUpdate);
// Memory leak!
}, []);Create custom hooks for reusable logic:
// Good - Custom hook
export const useRepositories = (): UseRepositoriesResult => {
const [repositories, setRepositories] = useState<GithubRepo[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const data = await fetchTopTypeScriptRepositories();
setRepositories(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return {
repositories,
loading,
error,
refetch: fetchData,
};
};
// Usage
const {repositories, loading, error} = useRepositories();Custom Hook Naming:
- Always start with
useprefix - Use camelCase:
useRepositories,useAuth,useNavigation
Always include all dependencies in dependency arrays:
// Good
useEffect(() => {
fetchData(id);
}, [id]); // Include id in dependencies
// Bad - Missing dependencies
useEffect(() => {
fetchData(id);
}, []); // Missing id dependency
// Good - Empty array for mount-only effects
useEffect(() => {
// Initialize something once
}, []); // Intentionally emptyESLint Rule:
Enable react-hooks/exhaustive-deps to catch missing dependencies.
Always handle errors in async operations:
// Good
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const data = await fetchTopTypeScriptRepositories();
setRepositories(data);
} catch (err) {
setError(err as Error);
console.error('Error fetching repositories:', err);
} finally {
setLoading(false);
}
};
// Bad - No error handling
const fetchData = async () => {
const data = await fetchTopTypeScriptRepositories();
setRepositories(data);
};Use error boundaries for component-level error handling (when implemented):
// Error Boundary Component (class component required)
class ErrorBoundary extends React.Component<
{children: React.ReactNode},
{hasError: boolean}
> {
constructor(props: {children: React.ReactNode}) {
super(props);
this.state = {hasError: false};
}
static getDerivedStateFromError() {
return {hasError: true};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorMessage message="Something went wrong" />;
}
return this.props.children;
}
}Handle API errors at the service level:
// Good - Service level error handling
export const fetchTopTypeScriptRepositories = async (): Promise<GithubRepo[]> => {
try {
const response = await axios.get<SearchRepositoriesResponse>(
`${API_BASE_URL}/search/repositories`,
{params: {/* ... */}},
);
return response.data.items.map(mapRepoFromDTO);
} catch (error) {
console.error('Error fetching repositories:', error);
throw error; // Re-throw for hook to handle
}
};
// Good - Hook level error handling
export const useRepositories = (): UseRepositoriesResult => {
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
try {
// ...
} catch (err) {
setError(err as Error);
}
};
// ...
};Provide user-friendly error messages:
// Good
if (error) {
return (
<ErrorMessage
message={error.message || 'Failed to load repositories. Please try again.'}
/>
);
}
// Bad
if (error) {
return <Text>{error.toString()}</Text>; // Technical error message
}Use Local State (useState) for:
- Component-specific UI state
- Form inputs
- Toggle states
- Temporary data
// Good - Local state
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState<string>('');Use Global State (Redux) for:
- User authentication
- App-wide settings
- Shared data across multiple screens
- API data that needs to be accessed from multiple components
- Complex state that requires predictable updates
If using Context API (not currently implemented):
// Good - Context pattern
interface AppContextType {
user: User | null;
setUser: (user: User | null) => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
export const useAppContext = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within AppProvider');
}
return context;
};The project uses Redux Toolkit for state management. Follow these patterns:
Organize Redux store by feature:
store/
├── index.ts # Store configuration
├── hooks.ts # Typed hooks
└── repositories/
├── repositories.slice.ts # Slice with reducers
├── repositories.thunks.ts # Async thunks
└── repositories.types.ts # Type definitions
Always use typed Redux hooks:
// Good - Typed hooks
import {useAppDispatch, useAppSelector} from '../store/hooks';
const dispatch = useAppDispatch();
const repositories = useAppSelector(state => state.repositories.repositories);
// Bad - Untyped hooks
import {useDispatch, useSelector} from 'react-redux';
const dispatch = useDispatch(); // No type safetyUse Redux Toolkit slices for reducers:
// Good - Redux Toolkit slice
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
const repositoriesSlice = createSlice({
name: 'repositories',
initialState,
reducers: {
setRepositories: (state, action: PayloadAction<GithubRepo[]>) => {
state.repositories = action.payload;
},
},
});Use Redux Thunk for async operations:
// Good - Async thunk
import {createAsyncThunk} from '@reduxjs/toolkit';
export const fetchRepositories = createAsyncThunk(
'repositories/fetchRepositories',
async (_, {dispatch, rejectWithValue}) => {
try {
const data = await fetchTopTypeScriptRepositories();
return data;
} catch (error) {
return rejectWithValue(error);
}
},
);Keep state normalized and flat:
// Good - Normalized state
interface RepositoriesState {
repositories: GithubRepo[];
loading: boolean;
error: Error | null;
}
// Avoid - Nested state
interface BadState {
data: {
repositories: {
items: GithubRepo[];
};
};
}Use selectors for accessing state:
// Good - Selector in hook
const repositories = useAppSelector(state => state.repositories.repositories);
const loading = useAppSelector(state => state.repositories.loading);
// Good - Memoized selector (for complex calculations)
const selectTopRepositories = (state: RootState) =>
state.repositories.repositories.slice(0, 5);Dispatch actions through thunks or direct actions:
// Good - Dispatch thunk
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchRepositories());
}, [dispatch]);
// Good - Dispatch action
dispatch(setLoading(true));Always use styled-components for styling. Import from styled-components/native:
// Good - Using styled-components
import styled from 'styled-components/native';
const Container = styled.View`
flex: 1;
background-color: ${({theme}) => theme.colors.background};
padding: ${({theme}) => theme.spacing.md}px;
`;
const Card = styled.View`
background-color: ${({theme}) => theme.colors.surface};
border-radius: ${({theme}) => theme.borderRadius.md}px;
padding: ${({theme}) => theme.spacing.md}px;
`;
// Bad - Don't use StyleSheet
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
});Always use theme values from the centralized theme:
// Good - Using theme
const Title = styled.Text`
font-size: ${({theme}) => theme.typography.sizes.xl}px;
font-weight: ${({theme}) => theme.typography.weights.bold};
color: ${({theme}) => theme.colors.text.primary};
`;
// Bad - Hardcoded values
const Title = styled.Text`
font-size: 20px;
font-weight: bold;
color: #1a1a1a;
`;Define styled components at the top of the file, before the main component:
// Good - Styled components defined first
import styled from 'styled-components/native';
const Container = styled.View`
flex: 1;
background-color: ${({theme}) => theme.colors.background};
`;
const Header = styled.View`
background-color: ${({theme}) => theme.colors.primary};
padding: ${({theme}) => theme.spacing.md}px;
`;
const HeaderTitle = styled.Text`
font-size: ${({theme}) => theme.typography.sizes.xl}px;
font-weight: ${({theme}) => theme.typography.weights.bold};
color: ${({theme}) => theme.colors.text.inverse};
`;
// Main component
export const HomeView: React.FC<HomeViewProps> = ({repositories}) => {
return (
<Container>
<Header>
<HeaderTitle>Top TypeScript Repositories</HeaderTitle>
</Header>
</Container>
);
};Extend existing styled components when creating variations:
// Good - Extending base component
const BaseCard = styled.View`
background-color: ${({theme}) => theme.colors.surface};
border-radius: ${({theme}) => theme.borderRadius.md}px;
padding: ${({theme}) => theme.spacing.md}px;
`;
const ContainerCard = styled(BaseCard)`
margin-horizontal: ${({theme}) => theme.spacing.md}px;
margin-vertical: ${({theme}) => theme.spacing.sm}px;
`;
// Or extend from other styled components
const StyledCard = styled(Card)`
margin-horizontal: ${({theme}) => theme.spacing.md}px;
`;Use props for conditional styling:
// Good - Conditional styling with props
interface ButtonProps {
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const Button = styled.TouchableOpacity<ButtonProps>`
padding: ${({theme}) => theme.spacing.md}px;
background-color: ${({theme, variant}) =>
variant === 'primary' ? theme.colors.primary : theme.colors.background};
opacity: ${({disabled}) => (disabled ? 0.5 : 1)};
border-radius: ${({theme}) => theme.borderRadius.md}px;
`;Wrap the app with ThemeProvider in App.tsx:
// App.tsx
import {ThemeProvider} from 'styled-components/native';
import {theme} from './src/theme';
function App() {
return (
<ThemeProvider theme={theme}>
{/* App content */}
</ThemeProvider>
);
}The theme is centralized in src/theme/:
// theme/theme.ts
export const theme = {
colors: {
primary: '#007AFF',
background: '#f5f5f5',
surface: '#ffffff',
text: {
primary: '#1a1a1a',
secondary: '#666666',
tertiary: '#888888',
inverse: '#ffffff',
},
error: '#d32f2f',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
borderRadius: {
sm: 8,
md: 12,
lg: 16,
},
typography: {
sizes: {
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
},
weights: {
normal: '400',
semibold: '600',
bold: '700',
},
},
};Use theme spacing and relative units:
// Good - Using theme spacing
const Container = styled.View`
flex: 1;
padding: ${({theme}) => theme.spacing.md}px;
`;
const Row = styled.View`
flex-direction: row;
align-items: center;
gap: ${({theme}) => theme.spacing.sm}px;
`;
const Card = styled.View`
width: 100%;
max-width: 600px;
align-self: center;
`;
// Avoid - Fixed pixel values
const Container = styled.View`
padding: 16px; // Bad - Use theme.spacing.md instead
`;Pass style props when needed for dynamic styling:
// Good - Accepting style prop for overrides
interface CardProps {
children: React.ReactNode;
style?: object;
}
const StyledCard = styled.View`
background-color: ${({theme}) => theme.colors.surface};
border-radius: ${({theme}) => theme.borderRadius.md}px;
`;
export const Card: React.FC<CardProps> = ({children, style}) => {
return <StyledCard style={style}>{children}</StyledCard>;
};Always clean up subscriptions, timers, and event listeners:
// Good - Cleanup subscriptions
useEffect(() => {
const subscription = eventEmitter.subscribe(handleEvent);
return () => subscription.unsubscribe();
}, []);
// Good - Cleanup timers
useEffect(() => {
const timer = setInterval(() => {
// Do something
}, 1000);
return () => clearInterval(timer);
}, []);Common Memory Leak Sources:
- Unclosed subscriptions:
// Bad
useEffect(() => {
someObservable.subscribe(handleUpdate);
// Missing cleanup
}, []);
// Good
useEffect(() => {
const subscription = someObservable.subscribe(handleUpdate);
return () => subscription.unsubscribe();
}, []);- Uncancelled async operations:
// Bad
useEffect(() => {
fetchData(); // May update state after unmount
}, []);
// Good
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
const data = await api.fetchData();
if (!cancelled) {
setData(data);
}
};
fetchData();
return () => {
cancelled = true;
};
}, []);- Event listeners:
// Bad
useEffect(() => {
window.addEventListener('resize', handleResize);
// Missing cleanup
}, []);
// Good
useEffect(() => {
const handleResize = () => {
// Handle resize
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);Always return cleanup functions from useEffect:
// Good - Multiple cleanups
useEffect(() => {
const subscription1 = source1.subscribe(handleUpdate1);
const subscription2 = source2.subscribe(handleUpdate2);
const timer = setInterval(handleTimer, 1000);
return () => {
subscription1.unsubscribe();
subscription2.unsubscribe();
clearInterval(timer);
};
}, []);Create a new atom when:
- You need a basic UI element that doesn't exist
- The element will be reused in multiple places
- It represents a single, indivisible UI component
- It has no dependencies on other custom components
Examples:
Button,Input,Label,Icon,Card,Spacer
Don't create an atom for:
- One-time use components (use inline JSX)
- Complex components (use molecules/organisms)
- Components that depend on other custom components
Create molecules when:
- You need to combine 2+ atoms into a functional unit
- The combination serves a specific, reusable purpose
- It doesn't require business logic or state management
- It represents a common UI pattern
Examples:
RepositoryCard(Card + Text elements)SearchBar(Input + Button + Icon)FormField(Label + Input + ErrorMessage)
Guidelines:
- Keep molecules simple and focused
- Don't add business logic to molecules
- Make molecules reusable across different contexts
- Use props for customization
Create organisms (containers) when:
- You need to combine multiple molecules and atoms
- The component requires business logic or state management
- It represents a complete feature or screen
- It's screen-specific and not meant for reuse
Guidelines:
- Separate logic (screen.tsx) from presentation (view.tsx)
- Keep screen files focused on state and side effects
- Keep view files focused on rendering
- Use custom hooks for data fetching
- Don't put API calls directly in containers
Templates (not currently used, but good to know):
- Page-level layouts without real content
- Define structure and placeholder content
- Used for design and prototyping
Pages (our containers):
- Specific instances with real content
- Connected to data and state
- What users actually see and interact with
In this project, we use containers directly as pages, skipping the template level for simplicity.
- Atoms compose into Molecules:
// RepositoryCard (molecule) uses Card (atom)
export const RepositoryCard = ({repository}) => (
<Card>
<Text>{repository.name}</Text>
</Card>
);- Molecules compose into Organisms:
// HomeView (organism) uses RepositoryCard (molecule)
export const HomeView = ({repositories}) => (
<FlatList
data={repositories}
renderItem={({item}) => <RepositoryCard repository={item} />}
/>
);- Keep dependencies unidirectional:
- Atoms don't depend on molecules or organisms
- Molecules don't depend on organisms
- Organisms can depend on molecules and atoms
- One component per file for atoms and molecules
- Multi-file structure for containers (screen, view, props, styles, types)
- Group related files in the same folder
- Use consistent naming across the project
- Atoms: Highly reusable, no context-specific logic
- Molecules: Reusable within similar contexts
- Organisms: Screen-specific, not reusable
When in doubt about where a component belongs:
- Can it be used in multiple places? → Atom or Molecule
- Does it need business logic? → Organism (Container)
- Is it screen-specific? → Organism (Container)
Remember: These conventions are guidelines. Use your judgment, but maintain consistency. When introducing new patterns, update this document.