Skip to content

React Component Style Guide

  • Use functional components with TypeScript (React.FC<Props>)
  • Export both the component and its props interface
  • Group related components in a single file when they share common functionality
interface MyComponentProps {
prop1: string;
prop2?: number;
}
const MyComponent: FC<MyComponentProps> = ({ prop1, prop2 }) => {
return <div>{/* ... */}</div>;
};
export { MyComponent };
export type { MyComponentProps };
  • Use Material-UI’s styled API for base styling
  • Leverage the theme for consistent styling
  • Use sx prop for component-specific overrides
  • Define hover states and transitions in the sx prop
const StyledComponent = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(2),
borderRadius: '8px',
}))
  • Define clear, descriptive interfaces for props
  • Use JSDoc comments for complex props
  • Make props optional when they have sensible defaults
  • Use discriminated unions for components with multiple states
interface ComponentProps {
/** Image URL for the avatar */
avatar?: string
name: string
onAction?: () => void
}
  1. Create stories first using Storybook
  2. Use stories in tests via composeStory
  3. Define a default export with component metadata
  4. Include multiple stories for different states
const meta: Meta<typeof MyComponent> = {
title: "Category/Component",
component: MyComponent,
parameters: {
layout: "centered"
},
tags: ["autodocs"],
render: (args) => (
<Box sx={{ width: "256px" }}>
<MyComponent {...args} />
</Box>
)
};
  • Test component rendering
  • Test user interactions
  • Use renderWithProviders for components requiring store/router context
  • Test both success and error paths
  • Use meaningful test descriptions
test("Component handles user interactions", async () => {
const user = userEvent.setup();
renderWithProviders(<Component />);
// Test initial render
const element = screen.getByTestId("component");
expect(element).toBeInTheDocument();
// Test interactions
await user.click(element);
// Assert expected outcomes
});
  • Use descriptive handler names (handleClick, handleSubmit)
  • Prevent event bubbling when needed with stopPropagation()
  • Separate complex logic into helper functions
const handleClick = (event: React.MouseEvent) => {
event.stopPropagation()
// Handle click logic
}
  • Use Redux for global state
  • Use local state for UI-specific concerns
  • Leverage selectors for derived state
  • Handle loading and error states explicitly
  • Use semantic HTML elements
  • Include data-testid for testing
  • Add ARIA labels where necessary
  • Ensure keyboard navigation support
  • Implement hover states
  • Show selected states clearly
  • Use consistent focus indicators
  • Provide loading states
  • Avoid unnecessary re-renders
  • Use proper dependency arrays in hooks
  • Implement virtualization for long lists
  • Lazy load components when appropriate
  • Keep components focused and single-purpose
  • Extract reusable logic into custom hooks
  • Use TypeScript for type safety
  • Follow consistent naming conventions
  • Use snackbars for notifications
  • Handle loading states gracefully
  • Provide meaningful error messages
  • Include recovery actions where possible
  • Implement error boundaries for critical components
  • Log errors appropriately
  • Provide fallback UI for error states
  • Group related components in a single file when they share common base styling or functionality
  • Export individual components and their types separately
  • Use a base component (like Card) that other variants extend
// Example structure
const BaseCard = styled(Paper)(/* ... */);
const StaffCard: FC<StaffCardProps> = /* ... */;
const GridCard: FC<GridCardProps> = /* ... */;
export { BaseCard, StaffCard, GridCard };
export type { StaffCardProps, GridCardProps };
  • Add JSDoc comments for component usage examples
  • Document complex state management requirements
  • Include prop interface documentation inline
/**
* A campaign card component that displays status and metrics
*
* @usage
* <CampaignCard
* name="test campaign"
* budget={100}
* live={live}
* onChangeStatus={changeStatus}
* />
*/
  • Handle event bubbling explicitly
  • Document event propagation behavior
  • Use separate handlers for nested interactive elements
const handleMenuButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation() // Stop card click event
handleClickMenu(event) // Handle menu open
}
  • Group related state declarations together
  • Initialize all state at the top of the component
  • Use descriptive state variable names
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null)
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false)
const openMenu = Boolean(menuAnchor) // Derived state
  • Use typed hooks for Redux operations
  • Handle side effects in event handlers
  • Keep Redux operations isolated from UI logic
const selectedGridId = useAppSelector(selectors.selectedGridId)
const [dispatch, actions] = useAppDispatch()
  • Use theme values for colors, spacing, and typography
  • Handle dark/light mode variations through theme
  • Define interactive states using theme values
sx={{
color: (theme) => theme.palette.text.primary,
background: (theme) => theme.palette.action.hover,
border: isSelected
? (theme) => `2px solid ${theme.palette.primary.main}`
: "2px solid transparent",
}}
  • Use Grid components for layout
  • Handle text overflow appropriately
  • Define mobile-first responsive styles
<Grid item xs zeroMinWidth>
<Typography
noWrap
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{content}
</Typography>
</Grid>
  • Show loading states during async operations
  • Provide visual feedback for selections
  • Include hover and active states
sx={{
transition: "border-color 0.3s",
"&:hover": {
background: (theme) => theme.palette.action.hover,
cursor: "pointer",
}
}}
  • Use dialogs for destructive actions
  • Provide clear feedback messages
  • Include undo/cancel options
<DeleteConfirmationDialog
isOpen={deleteConfirmationOpen}
title={`${name}`}
message={`Are you sure you want to remove ${name}?`}
onRemove={handleRemove}
onCancel={() => setDeleteConfirmationOpen(false)}
/>