You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document outlines our recommended structure for React components, following screaming architecture while maintaining clarity, testability, and scalability.
// ✅ Do: Use function declarationfunctionUserProfile({ name, age }: UserProfileProps){return(<div><h2>{name}</h2><span>{age}</span></div>);}// ❌ Don't: Use arrow functionconstUserProfile: FC<UserProfileProps>=({ name, age })=>{return(<div><h2>{name}</h2><span>{age}</span></div>);};
2. Test Organization
Single Test File Rule:
When a component/hook/utility has only one test file, place it next to the source:
// ✅ Do: Single test file structure/ButtonGroup
├──index.tsx├──types.ts└──ButtonGroup.test.tsx// ❌ Don't: Create __tests__ directory for single file/ButtonGroup├──index.tsx├──types.ts└──__tests__/└──ButtonGroup.test.tsx
Multiple Test Files Rule:
Create a __tests__ directory when you have multiple test files:
// ✅ Do: Multiple test files structure/DataGrid└──__tests__/├──DataGrid.unit.test.tsx// Unit tests├──DataGrid.integration.test.tsx// Integration tests├──utils.ts// Test utilities└──fixtures.ts// Test data// ❌ Don't: Mix multiple test files at root/DataGrid├──DataGrid.unit.test.tsx├──DataGrid.integration.test.tsx└──DataGrid.e2e.test.tsx
Purpose: Child components specific to this component.
// ✅ Do: Clear component structure/components└──SubComponentA/├──index.tsx├──types.ts└──SubComponentA.test.tsx// ❌ Don't: Mix concerns/components└──SubComponentA.tsx// All in one file
Example:
// ✅ Do: Clear type definitions and implementation// components/SubComponentA/types.tsexportinterfaceSubComponentAProps{data: SubComponentData;onChange?: (data: SubComponentData)=>void;}// components/SubComponentA/index.tsxfunctionSubComponentA({ data, onChange }: SubComponentAProps){return<divonClick={()=>onChange?.(data)}>{data.label}</div>;}// ❌ Don't: Mix types and implementationfunctionSubComponentA({ data, onChange }: {data: any;onChange?: Function}){return<divonClick={()=>onChange?.(data)}>{data.label}</div>;}
2. /shared
Purpose: Shared business logic, types, and utilities.
// ✅ Do: Clear type exports// shared/types.tsexportinterfaceComponentData{id: string;value: unknown;}// ❌ Don't: Mix internal and external typesinterfaceInternalType{}// Should be in a separate fileexportinterfacePublicType{}
3. /hooks
Purpose: Custom hooks for component logic.
// ✅ Do: Clear hook structure/hooks└──useFeatureA/├──index.ts├──types.ts└──useFeatureA.test.ts// ❌ Don't: Mix hook with components/hooks└──FeatureAHook.tsx// Wrong file extension and structure
Example:
// ✅ Do: Clear hook implementationfunctionuseFeatureA({ initialValue }: UseFeatureAOptions): UseFeatureAResult{const[value,setValue]=useState(initialValue);functionupdateValue(newValue: string){setValue(newValue);}return{ value, updateValue };}// ❌ Don't: Mix concerns in hooksfunctionuseFeatureA(){const[value,setValue]=useState('');// Don't include JSX in hooksreturn<div>{value}</div>;}
Complete Example
Here's a complex component following all rules:
Here's a complete example showing both rules in practice:
// Simple component with single test file/ButtonGroup
├──index.tsx├──types.ts└──ButtonGroup.test.tsx// Complex component with multiple test files/DataGrid├──components/│├──GridHeader/││├──index.tsx││├──types.ts││└──__tests__/││├──GridHeader.test.tsx││├──GridHeader.integration.tsx││└──utils.ts│└──GridCell/│├──index.tsx│├──types.ts│└──GridCell.test.tsx// Single test file├──shared/│├──utils.ts│├──types.ts│└──utils.test.ts// Single test file└──hooks/│└──useGridSort/│├──index.ts│├──types.ts│└──__tests__/│├──useGridSort.test.ts│├──useGridSort.integration.ts│└──utils.ts├──index.tsx└──styles.ts[Optional]
Component Implementation Examples:
Component Structure Additional Rules
Test Organization Rules
Single Test File Rule
When a component/hook/utility has only one test file, place it directly next to the source file with a .test.tsx extension:
/ComponentName
├── components/
│ └── SubComponentA/
│ ├── index.tsx
│ ├── types.ts
│ └── SubComponentA.test.tsx // Single test file
Multiple Test Files Rule
Create a __tests__ directory when you have:
Multiple types of tests (unit, integration, e2e)
Test utilities or fixtures
Multiple test files for different aspects of the component
Always destructure props directly in the function arguments:
// ✅ Do: Destructure props in function argumentsfunctionUserProfile({ name, age, email, isAdmin }: UserProfileProps){return({name}{age}{email}{isAdmin&&});}// ❌ Don't: Destructure props inside the functionfunctionUserProfile(props: UserProfileProps){const{ name, age, email, isAdmin }=props;// Avoid thisreturn({name}{age}{email}{isAdmin&&});}
2. Benefits of Arguments Destructuring
Immediate Prop Visibility:
// ✅ Do: Props are immediately visible in function signaturefunctionProductCard({
title,
price,
onAdd,
isInStock
}: ProductCardProps){// Props usage is clear from the start}// ❌ Don't: Hide props in function bodyfunctionProductCard(props: ProductCardProps){const{ title, price, onAdd, isInStock }=props;// Props are hidden}
TypeScript Type Inference:
// ✅ Do: Better type inference for destructured propsfunctionUserStatus({
status,
lastActive
}: UserStatusProps){// TypeScript knows 'status' and 'lastActive' types immediatelyconststatusColor=getStatusColor(status);// Better type inference}// ❌ Don't: Less precise type inferencefunctionUserStatus(props: UserStatusProps){const{ status, lastActive }=props;// Extra step for TypeScript to infer types}
Default Values:
// ✅ Do: Clean default value assignmentfunctionPagination({
page =1,
pageSize =10,
total
}: PaginationProps){// Default values are clearly visible in the signature}// ❌ Don't: Separate default valuesfunctionPagination(props: PaginationProps){const{ page, pageSize, total }=props;constcurrentPage=page??1;// Less clearconstcurrentPageSize=pageSize??10;// Clutters the function body}
Optional Props:
// ✅ Do: Clear optional props handlingfunctionAlert({
message,
type ='info',onClose?: ()=>void}: AlertProps){// Optional onClose is clearly marked in signature}// ❌ Don't: Hide optional propsfunctionAlert(props: AlertProps){const{ message, type ='info', onClose }=props;// Optional nature of onClose is hidden}
3. Complex Props Cases
For components with many props, group them logically:
// ✅ Do: Group related propsfunctionDataTable({// Data props
data,
columns,// UI props
isLoading,
error,// Event handlers
onSort,
onFilter,
onRowClick,// Pagination props
page =1,
pageSize =10,
total
}: DataTableProps){// Clean implementation}// ❌ Don't: Mix props without logical groupingfunctionDataTable(props: DataTableProps){const{
data, isLoading, onSort, page,
columns, error, onFilter, pageSize,
onRowClick, total
}=props;// Harder to understand prop relationships}
Remember:
Always destructure props in function arguments
Group related props together in signature
Use default values in the destructuring
Keep optional props clear with ? syntax
Benefits include:
Better readability
Immediate prop visibility
Better TypeScript type inference
Cleaner default value handling
Clear optional props marking
Logical prop grouping
Types Organization
1. Types Location
Public types go in shared/types.ts, while internal types should be co-located with their components:
// ✅ Do: Keep public types in shared/types.ts// shared/types.tsexportinterfaceTableProps{data: TableData[];onSort?: (column: string)=>void;}// ✅ Do: Keep internal types with their components// components/TableHeader/types.tsinterfaceInternalHeaderState{isResizing: boolean;startWidth: number;}// ❌ Don't: Put internal types in shared/types.ts// shared/types.tsinterfaceInternalHeaderState{ ... }// Should be in component's types.ts
2. Enum-like Constants
Use object-like enum notation with as const in shared/constants.ts, using PascalCase for enum values and derive types using the suffix Type:
// ✅ Do: Use PascalCase for enum values// shared/constants.tsexportconstSORT_DIRECTION={Ascending: 'ascending',Descending: 'descending',None: 'none',}asconst;exportconstVIEW_MODES={Grid: 'grid',List: 'list',Compact: 'compact',}asconst;// shared/types.tsexporttypeSortDirectionType=typeofSORT_DIRECTION[keyoftypeofSORT_DIRECTION];// ❌ Don't: Use incorrect casing for enum valuesexportconstSORT_DIRECTION={ASCENDING: 'ascending',// Should be PascalCaseascending: 'ascending',// Should be PascalCase}asconst;
3. Constants Organization
All constants should be defined in shared/constants.ts using UPPER_SNAKE_CASE for the constant name:
// ✅ Do: Define all constants in shared/constants.ts with proper casing// shared/constants.tsexportconstTABLE_DEFAULT_PAGE_SIZE=10;exportconstMINIMUM_COLUMN_WIDTH=100;exportconstCOLUMN_SIZES={Small: 'sm',Medium: 'md',Large: 'lg',}asconst;// ❌ Don't: Define constants in component files or use incorrect casing// components/Table/index.tsxconstdefaultPageSize=10;// Should be in constants.tsconstCOLUMN_SIZES={SMALL: 'sm',// Should use PascalCase}asconst;
4. Type Composition Exception
For type composition, all related types should stay in shared/types.ts:
// ✅ Do: Keep composed types together in shared/types.ts// shared/constants.tsexportconstCOLUMN_SIZES={Small: 'sm',Medium: 'md',Large: 'lg',}asconst;// shared/types.tsexporttypeColumnSizeType=typeofCOLUMN_SIZES[keyoftypeofCOLUMN_SIZES];exportinterfaceColumnProps{size: ColumnSizeType;title: string;}// ❌ Don't: Split composed types// components/Column/types.tstypeColumnSizeType='sm'|'md'|'lg';// Should be in shared/types.ts with other composed types
5. Naming Conventions
Follow consistent naming for types and constants:
// ✅ Do: Use consistent naming// shared/constants.tsexportconstLAYOUT_MODES={Horizontal: 'horizontal',Vertical: 'vertical',Grid: 'grid',}asconst;// shared/types.tsexporttypeLayoutModeType=typeofLAYOUT_MODES[keyoftypeofLAYOUT_MODES];// ❌ Don't: Use inconsistent namingexportconstlayoutModes={ ... }// Should be UPPER_SNAKE_CASEtypeTLayoutMode= ... // Should be LayoutModeType
This organization ensures:
Clear separation between public and internal types
Consistent naming conventions
PascalCase for enum values
UPPER_SNAKE_CASE for constant names
Type safety through derived types
Proper co-location of internal types
Remember:
Public types go in shared/types.ts
Internal types stay with their components
All constants and enum-like objects go in shared/constants.ts
Use as const with object-like enums
Use PascalCase for enum values
Use UPPER_SNAKE_CASE for constant names
Derive types from constants using Type suffix
Exception: Keep composed types together in shared/types.ts
State Management Organization (Context)
1. Simple Context Case
For simple state (few values, simple updates):
// ✅ Do: Keep it simple for basic state/ComponentName├──context/│├──ThemeContext.tsx│└──ThemeContext.test.tsx// context/ThemeContext.tsxinterfaceThemeContextValue{isDark: boolean;toggleTheme: ()=>void;}exportconstThemeContext=createContext(undefined);exportfunctionThemeProvider({ children }: PropsWithChildren){const[isDark,setIsDark]=useState(false);consttoggleTheme=useCallback(()=>setIsDark(prev=>!prev),[]);return({children});}// ❌ Don't: Over-engineer simple state/ComponentName├──context/│├──ThemeContext/││├──reducer.ts// Unnecessary for simple state││├──actions.ts││└──selectors.ts
2. Complex Context Case
For complex state (multiple values, complex updates, derived state):
// ✅ Do: Keep it simple for basic API calls/ComponentName├──api/│├──userApi.ts│└──userApi.test.ts// api/userApi.tsimportaxiosfrom'axios';exportconstuserApi={getUser: async(id: string)=>{const{ data }=awaitaxios.get(`/api/users/${id}`);returndata;},updateUser: async(id: string,userData: UserData)=>{const{ data }=awaitaxios.put(`/api/users/${id}`,userData);returndata;},};// ❌ Don't: Mix API calls in componentsfunctionUserProfile({ id }: {id: string}){constfetchUser=async()=>{// Should be in api fileconstresponse=awaitaxios.get(`/api/users/${id}`);returnresponse.data;};}
API Organization Rules
1. Global API Directory
Use /src/api for shared API calls that are used across multiple components:
// ✅ Do: Place shared API calls in global directory/src├──api/│├──users/││├──types.ts││├──usersApi.ts││└──__tests__/││└──usersApi.test.ts│├──studies/││└── ...
│└──config/│└── ...
// api/users/types.tsexportinterfaceUser{id: string;name: string;email: string;}exportinterfaceUserParams{role?: string;status?: 'active'|'inactive';}// api/users/usersApi.tsimportaxiosfrom'axios';import{User,UserParams}from'./types';exportconstusersApi={getUsers: async(params?: UserParams)=>{const{ data }=awaitaxios.get<User[]>('/api/users',{ params });returndata;},updateUser: async(id: string,userData: Partial<User>)=>{const{ data }=awaitaxios.put<User>(`/api/users/${id}`,userData);returndata;},};// ❌ Don't: Duplicate shared API calls in components/src/components/UserList/api/usersApi.ts// Don't duplicate/src/components/UserProfile/api/usersApi.ts// Don't duplicate
2. Component-Level API Directory
Use component-level API calls only for highly specific, component-bound functionality:
// ✅ Do: Place component-specific API calls in the component/src/components/DataGrid├──api/│├──types.ts│├──gridApi.ts│└──__tests__/│└──gridApi.test.ts└──index.tsx// components/DataGrid/api/types.tsexportinterfaceGridLayout{columns: Array<{id: string;width: number}>;sortOrder: 'asc'|'desc';pageSize: number;}// components/DataGrid/api/gridApi.tsimportaxiosfrom'axios';import{GridLayout}from'./types';exportconstgridApi={saveLayout: async(layout: GridLayout)=>{const{ data }=awaitaxios.post<{success: boolean}>('/api/user-preferences/grid-layout',layout);returndata;},getLayout: async()=>{const{ data }=awaitaxios.get<GridLayout>('/api/user-preferences/grid-layout');returndata;},};// ❌ Don't: Place generic API calls in components/src/components/UserProfile/api/usersApi.ts// Should be in global api
3. Usage Examples
Simple Global API Usage:
// ✅ Do: Simple API hooks with loading and error states// src/api/users/useUsers.tsexportfunctionuseUsers(initialParams?: UserParams){const[data,setData]=useState<User[]|null>(null);const[loading,setLoading]=useState(false);const[error,setError]=useState<Error|null>(null);constfetchUsers=async(params?: UserParams)=>{try{setLoading(true);constusers=awaitusersApi.getUsers(params);setData(users);}catch(err){setError(errinstanceofError ? err : newError('Unknown error'));}finally{setLoading(false);}};useEffect(()=>{fetchUsers(initialParams);},[/* dependencies */]);return{ data, loading, error,refetch: fetchUsers};}// Usage in componentfunctionUserList(){const{data: users, loading, error }=useUsers({role: 'admin'});if(loading)return<div>Loading...</div>;
if(error)return<div>Error: {error.message}</div>;return(<ul>{users?.map(user=>(<likey={user.id}>{user.name}</li>
))}</ul>
);}
Complex Component API Usage:
// ✅ Do: Complex API hooks with state management// src/components/DataGrid/api/useGridLayout.tsexportfunctionuseGridLayout(){const[layout,setLayout]=useState<GridLayout|null>(null);const[saving,setSaving]=useState(false);const[error,setError]=useState<Error|null>(null);constsaveLayout=async(newLayout: GridLayout)=>{try{setSaving(true);awaitgridApi.saveLayout(newLayout);setLayout(newLayout);}catch(err){setError(errinstanceofError ? err : newError('Failed to save layout'));throwerr;// Allow parent component to handle error}finally{setSaving(false);}};constloadLayout=async()=>{try{constsavedLayout=awaitgridApi.getLayout();setLayout(savedLayout);}catch(err){setError(errinstanceofError ? err : newError('Failed to load layout'));// Fall back to default layoutsetLayout(defaultLayout);}};// Load initial layoutuseEffect(()=>{loadLayout();},[]);return{
layout,
saving,
error,
saveLayout,
loadLayout,};}// Usage in componentfunctionDataGrid(){const{
layout,
saving,
error,
saveLayout
}=useGridLayout();consthandleLayoutChange=async(newLayout: GridLayout)=>{try{awaitsaveLayout(newLayout);toast.success('Layout saved successfully');}catch(err){toast.error('Failed to save layout');}};return(<div>{/* Grid implementation */}</div>);}
Remember:
Use global API directory for shared functionality
Keep component-specific API calls in the component
Implement proper loading and error states
Handle errors consistently
Use TypeScript for better type safety
Create custom hooks for complex API state management
Keep API calls separate from UI components
Use proper error boundaries
Future optimisations: Data Fetching & Validation
1. Basic Setup
First, set up your schema and types:
// ✅ Do: Define schemas in a dedicated types file// types.tsimport{z}from'zod';exportconstUserSchema=z.object({id: z.string(),name: z.string(),email: z.string().email(),role: z.enum(['user','admin']),createdAt: z.string().datetime(),});exporttypeUser=z.infer<typeofUserSchema>;// ❌ Don't: Mix schemas with API or component logicfunctionUserProfile(){constuserSchema=z.object({ ... });// Don't define schemas here}
2. Simple Query Example
Basic query with validation:
// ✅ Do: Create reusable query hooks with validation// api/queries/useUser.tsexportfunctionuseUser(id: string){returnuseQuery({queryKey: ['user',id],queryFn: async()=>{constresponse=awaitaxios.get(`/api/users/${id}`);returnUserSchema.parse(response.data);},});}// UsagefunctionUserProfile({ id }: {id: string}){const{data: user, isLoading, error }=useUser(id);if(isLoading)return<div>Loading...</div>;
if(error)return<div>Error: {error.message}</div>;return<div>{user.name}</div>;
}// ❌ Don't: Validate data in componentsfunctionUserProfile({ id }: {id: string}){const{ data }=useQuery({queryKey: ['user',id],queryFn: async()=>{constresponse=awaitaxios.get(`/api/users/${id}`);try{// Don't validate herereturnUserSchema.parse(response.data);}catch(error){// Error handling...}},});}
3. Complex Query with Relations
Handle related data with proper typing and validation:
// ✅ Do: Create comprehensive schemas for related data// types.tsconstPostSchema=z.object({id: z.string(),title: z.string(),content: z.string(),authorId: z.string(),});constCommentSchema=z.object({id: z.string(),content: z.string(),postId: z.string(),userId: z.string(),});constPostWithDetailsSchema=PostSchema.extend({author: UserSchema,comments: z.array(CommentSchema),});typePostWithDetails=z.infer<typeofPostWithDetailsSchema>;// api/queries/usePost.tsexportfunctionusePost(id: string){returnuseQuery({queryKey: ['post',id],queryFn: async()=>{constresponse=awaitaxios.get(`/api/posts/${id}?include=author,comments`);returnPostWithDetailsSchema.parse(response.data);},});}// ❌ Don't: Make multiple separate queriesfunctionPostDetails({ id }: {id: string}){// Don't do separate queries for related dataconst{data: post}=useQuery(['post',id], ...);const{data: author}=useQuery(['user',post?.authorId], ...);const{data: comments}=useQuery(['comments',id], ...);}
4. Mutations with Optimistic Updates
// ✅ Do: Implement type-safe optimistic updates// types.tsconstUpdateUserSchema=UserSchema.partial().omit({id: true});typeUpdateUserInput=z.infer<typeofUpdateUserSchema>;// api/mutations/useUpdateUser.tsexportfunctionuseUpdateUser(){constqueryClient=useQueryClient();returnuseMutation({mutationFn: async({ id, data }: {id: string;data: UpdateUserInput})=>{constresponse=awaitaxios.patch(`/api/users/${id}`,data);returnUserSchema.parse(response.data);},onMutate: async({ id, data })=>{awaitqueryClient.cancelQueries({queryKey: ['user',id]});constpreviousUser=queryClient.getQueryData<User>(['user',id]);queryClient.setQueryData<User>(['user',id],old=>({
...old!,
...data,}));return{ previousUser };},onError: (err,{ id },context)=>{queryClient.setQueryData(['user',id],context?.previousUser);},onSettled: (_,__,{ id })=>{queryClient.invalidateQueries({queryKey: ['user',id]});},});}// ❌ Don't: Skip validation in mutationsconstuseUpdateUser=()=>useMutation({mutationFn: ({ id, data })=>axios.patch(`/api/users/${id}`,data),// Missing validation});
5. Cached Selectors
// ✅ Do: Create reusable selectors with type safety// api/selectors/userSelectors.tsexportconstselectUserPermissions=(user: User)=>{constrolePermissions={admin: ['read','write','delete'],user: ['read'],};returnrolePermissions[user.role]??[];};functionAdminPanel(){const{data: user}=useUser(id);constpermissions=user ? selectUserPermissions(user) : [];returnpermissions.includes('delete') ? <DeleteButton/> : null;
}// ❌ Don't: Recalculate derived datafunctionAdminPanel(){const{data: user}=useUser(id);// Don't recalculate on every renderconstpermissions=user?.role==='admin' ? ['read','write','delete'] : ['read'];}
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
This document outlines our recommended structure for React components, following screaming architecture while maintaining clarity, testability, and scalability.
Base Structure
Key Rules & Best Practices
1. Component Definition
Always use function syntax for components:
2. Test Organization
Single Test File Rule:
When a component/hook/utility has only one test file, place it next to the source:
Multiple Test Files Rule:
Create a
__tests__
directory when you have multiple test files:Naming
Simple small component:
Bigger component with more than unit tests:
Directory Details
1.
/components
Purpose: Child components specific to this component.
Example:
2.
/shared
Purpose: Shared business logic, types, and utilities.
Example:
3.
/hooks
Purpose: Custom hooks for component logic.
Example:
Complete Example
Here's a complex component following all rules:
Here's a complete example showing both rules in practice:
Component Implementation Examples:
Component Structure Additional Rules
Test Organization Rules
Single Test File Rule
When a component/hook/utility has only one test file, place it directly next to the source file with a
.test.tsx
extension:Multiple Test Files Rule
Create a
__tests__
directory when you have:Benefits
Clear Boundaries
Maintainability
Scalability
Performance
Props Destructuring
1. Function Arguments Destructuring
Always destructure props directly in the function arguments:
2. Benefits of Arguments Destructuring
3. Complex Props Cases
For components with many props, group them logically:
Remember:
?
syntaxTypes Organization
1. Types Location
Public types go in
shared/types.ts
, while internal types should be co-located with their components:2. Enum-like Constants
Use object-like enum notation with
as const
inshared/constants.ts
, using PascalCase for enum values and derive types using the suffixType
:3. Constants Organization
All constants should be defined in
shared/constants.ts
using UPPER_SNAKE_CASE for the constant name:4. Type Composition Exception
For type composition, all related types should stay in
shared/types.ts
:5. Naming Conventions
Follow consistent naming for types and constants:
This organization ensures:
Remember:
shared/types.ts
shared/constants.ts
as const
with object-like enumsType
suffixshared/types.ts
State Management Organization (Context)
1. Simple Context Case
For simple state (few values, simple updates):
2. Complex Context Case
For complex state (multiple values, complex updates, derived state):
API Services Organization
1. Simple API Case
For components with 1-2 API calls:
API Organization Rules
1. Global API Directory
Use
/src/api
for shared API calls that are used across multiple components:2. Component-Level API Directory
Use component-level API calls only for highly specific, component-bound functionality:
3. Usage Examples
Simple Global API Usage:
Complex Component API Usage:
Remember:
Future optimisations: Data Fetching & Validation
1. Basic Setup
First, set up your schema and types:
2. Simple Query Example
Basic query with validation:
3. Complex Query with Relations
Handle related data with proper typing and validation:
4. Mutations with Optimistic Updates
5. Cached Selectors
6. Query Invalidation Strategy
Remember:
Beta Was this translation helpful? Give feedback.
All reactions