| name | react-conventions |
| description | React and React Native coding conventions with TypeScript. Use this skill when creating components, writing hooks, or reviewing React/React Native code. Enforces naming conventions, component structure, typing patterns, accessibility, and performance best practices. |
React Conventions
Coding conventions for React and React Native with TypeScript. It defines how to write good components, props, hooks and styles with tailwind (uniwind).
By keeping in mind accessibility, performances (with and without react-compiler) and testability.
Exports & Naming
Named exports only
export const Button = ({ label }: ButtonProps) => { ... };
export default function Button({ label }) { ... }
Component typing
interface ButtonProps {
label: string;
onPress: VoidFunction;
}
export const Button = ({ label, onPress }: ButtonProps) => {
return <Pressable onPress={onPress}><Text>{label}</Text></Pressable>;
};
export const Button: FC<ButtonProps> = ({ label, onPress }) => { ... };
Naming conventions
| Element | Convention | Example |
|---|
| Component | PascalCase | UserProfile.tsx |
| Props interface | [Component]Props | UserProfileProps |
| Hook | camelCase with use prefix | useAuth.ts |
| Hook return type | [Hook]Return | UseAuthReturn |
Component Structure
Order within a component file:
import { useState } from "react";
import { View, Text, Pressable } from "react-native";
type UserCardProps = Readonly<{
name: string;
avatarUrl?: string;
onPress: VoidFunction;
}>;
export const UserCard = ({ name, avatarUrl, onPress }: UserCardProps) => {
const [isPressed, setIsPressed] = useState(false);
const displayName = name.toUpperCase();
const handlePress = () => {
setIsPressed(true);
onPress();
};
return (
<Pressable onPress={handlePress} className="p-4 bg-white rounded-lg">
<Text className="text-lg font-bold">{displayName}</Text>
</Pressable>
);
};
JSX readability
Extract complex logic from JSX:
const showError = hasError && !isLoading;
const statusText = isOnline ? "Connected" : "Offline";
return (
<View>
{showError && <ErrorMessage />}
<Text>{statusText}</Text>
</View>
);
return (
<View>
{hasError && !isLoading && <ErrorMessage />}
<Text>{isOnline ? "Connected" : "Offline"}</Text>
</View>
);
Props
Readonly by default
Use the Readonly utility type:
type CardProps = Readonly<{
title: string;
subtitle?: string;
}>;
interface CardProps {
readonly title: string;
readonly subtitle?: string;
}
Children with PropsWithChildren
type ContainerProps = PropsWithChildren<
Readonly<{
padding?: number;
}>
>;
type WrapperProps = PropsWithChildren;
type ContainerProps = Readonly<{
children: ReactNode;
padding?: number;
}>;
Default values via destructuring
export const Button = ({
variant = "primary",
size = "medium",
disabled = false,
}: ButtonProps) => { ... };
Button.defaultProps = {
variant: "primary",
};
Optional vs required
Be intentional:
type FormFieldProps = Readonly<{
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
errorMessage?: string;
}>;
Hooks
Naming
const useAuth = () => { ... };
const useUserProfile = (userId: string) => { ... };
const authHook = () => { ... };
const getUserProfile = () => { ... };
One hook per file
hooks/
├── useAuth.ts
├── useToggle.ts
└── useDebounce.ts
Explicit return type
interface UseToggleReturn {
isOn: boolean;
toggle: VoidFunction;
setOn: VoidFunction;
setOff: VoidFunction;
}
export const useToggle = (initial = false): UseToggleReturn => {
const [isOn, setIsOn] = useState(initial);
return {
isOn,
toggle: () => setIsOn((v) => !v),
setOn: () => setIsOn(true),
setOff: () => setIsOn(false),
};
};
Rules of hooks
export const Component = ({ userId }: Props) => {
const [state, setState] = useState(null);
const user = useUser(userId);
};
export const Component = ({ userId }: Props) => {
if (userId) {
const user = useUser(userId);
}
};
items.map((item) => {
const data = useData(item.id);
});
Performance
React Compiler
React 19+ with React Compiler handles most memoization automatically. Avoid premature optimization:
export const List = ({ items, onItemPress }: ListProps) => {
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
return (
<FlatList
data={sortedItems}
renderItem={({ item }) => (
<ListItem item={item} onPress={() => onItemPress(item.id)} />
)}
/>
);
};
When to manually optimize
Only optimize when you measure a performance problem:
export const ExpensiveComponent = memo(({ data }: Props) => {
});
const handlePress = useCallback(() => {
onPress(id);
}, [onPress, id]);
const processedData = useMemo(() => {
return heavyComputation(rawData);
}, [rawData]);
FlatList optimization
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
/>;
const renderItem = useCallback(
({ item }: { item: Item }) => <ItemCard item={item} />,
[]
);
Avoid inline arrays in props
const emptyArray: Item[] = [];
<FlatList data={items ?? emptyArray} />
<FlatList data={items ?? []} />
Accessibility
Required attributes
<Pressable
onPress={handlePress}
accessibilityRole="button"
accessibilityLabel="Submit form"
accessibilityHint="Submits your information and proceeds to the next step"
accessibilityState={{ disabled: isLoading }}
>
<Text>Submit</Text>
</Pressable>
<Pressable onPress={handlePress}>
<Text>Submit</Text>
</Pressable>
Accessibility roles
Use appropriate roles:
| Element | Role |
|---|
| Clickable action | button |
| Navigation link | link |
| Text input | none (handled by TextInput) |
| Image | image |
| Header text | header |
| Checkbox | checkbox |
| Switch | switch |
| Tab | tab |
Accessibility state
<Pressable
accessibilityRole="checkbox"
accessibilityState={{
checked: isChecked,
disabled: isDisabled,
}}
>
<Text>Accept terms</Text>
</Pressable>
Screen reader considerations
<View accessibilityRole="summary" accessibilityLabel={`${title}. ${subtitle}`}>
<Text>{title}</Text>
<Text>{subtitle}</Text>
</View>
<Image
source={decorativeIcon}
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
/>
Testability
testID from constants only
Never hardcode testIDs. Always use the centralized constants:
export const TestIDs = {
Login: {
emailInput: "login-email-input",
passwordInput: "login-password-input",
submitButton: "login-submit-button",
errorMessage: "login-error-message",
},
Profile: {
avatar: "profile-avatar",
nameText: "profile-name-text",
editButton: "profile-edit-button",
},
} as const;
Usage
import { TestIDs } from "@/constants/testIDs";
<Pressable testID={TestIDs.Login.submitButton} onPress={handleSubmit}>
<Text>Submit</Text>
</Pressable>
<TextInput
testID={TestIDs.Login.emailInput}
value={email}
onChangeText={setEmail}
/>
<Pressable testID="submit-button" onPress={handleSubmit}>
<Text>Submit</Text>
</Pressable>
Naming convention
Pattern: [screen]-[element]-[action?]
Format: kebab-case
Examples:
- login-email-input
- profile-save-button
- home-user-list
- settings-notifications-toggle