| name | react-component-writing |
| description | React component patterns and style guide for the Commons monorepo. Use when creating React components, working with GraphQL in components, or implementing internationalization with MessageFormat. |
React Components
Component Guidelines
- Named exports only:
export const MyComponent = ... (no default exports)
- Plain arrow functions: Don't type with
React.FC
- No spread props in GraphQL wrappers: List all props individually for type safety
- Boolean naming: booleans must start with
is/are/should/has/have/can/did/will/does/was/were (enforced by @typescript-eslint/naming-convention). e.g. isReasonAllowed, not allowsReason
interface MyComponentProps {
title: MessageFormat;
onSubmit: () => void;
}
export const MyComponent = ({ title, onSubmit }: MyComponentProps) => {
};
UI Components (no GraphQL)
Simple structure—see example: commons-packages/frontend/shared/example-ui-component/
interface ExampleUiComponentProps {
description?: MessageFormat;
onSubmitClick: () => void;
title: MessageFormat;
}
export const ExampleUiComponent = ({ description, onSubmitClick, title }: ExampleUiComponentProps) => {
};
GraphQL Components
Separate into two components:
- UI sub-component (
MyComponentUI): Renders UI, receives data as props
- GraphQL wrapper (
MyComponent): Fetches data, passes to UI
See example: commons-packages/frontend/shared/example-graphql-component/
interface MyComponentUIProps extends MyComponentBaseProps {
error?: ApolloError;
isLoading?: boolean;
data?: GetDataQuery['data'];
}
export const MyComponentUI = ({ error, isLoading, data }: MyComponentUIProps) => {
if (isLoading) return <Spinner />;
if (error) return <ErrorComponent error={error} />;
return ();
};
export const MyComponent = ({ id }: MyComponentProps) => {
const { data, loading, error } = useGetDataQuery({ variables: { id } });
return (
<MyComponentUI
isLoading={loading}
error={error}
data={data?.data}
/>
);
};
Custom Hooks
- Prefix with
use: useMyHook
- Return typed objects or tuples
- Place in
hooks/ directory or co-locate with component
interface UseMyHookResult {
value: string;
setValue: (v: string) => void;
isLoading: boolean;
}
export const useMyHook = (initialValue: string): UseMyHookResult => {
};
Internationalization
Use MessageFormat for text. Render with useFormatMessage from Commonplace (not useIntl from react-intl):
import { Text, useFormatMessage } from '@commons/frontend/shared/commonplace';
const Component = ({ pageNumber }: { pageNumber: number }) => {
const formatMessage = useFormatMessage();
const nextText = formatMessage({ id: 'shared.next' });
return <Text text={`${nextText}: ${pageNumber}`} />;
};
Dynamic message ids
Template-literal message ids are not in the typed MessageKey union, so they fail
tsc unless cast. Cast them with as MessageFormat:
formatMessage({ id: `opportunities.supplementalReason.${reason}` } as MessageFormat)
optionLabelContent: { id: `${prefix}.${value}` } as MessageFormat,
Testing
Import from test-utils
import { render } from '@commons/frontend/rtlTests/test-utils';
This wraps components with necessary providers (IntlProvider, MockedProvider, AppStateProvider, Router).
Two Approaches for GraphQL Components
-
Test UI sub-component directly — pass props without mocks:
render(<MyComponentUI data={mockData} isLoading={false} />);
-
Use GraphQL mocks — test full wrapper:
render(<MyComponent id="123" />, { customMocks: [myMock] });
See examples: commons-packages/frontend/shared/example-graphql-component/__tests__/
File Structure
Co-locate related files:
my-component/
├── my-component.tsx
├── my-component.graphql
├── my-component.css (or .module.css)
└── __tests__/
└── my-component.spec.tsx