React
General
- Prefer Function Components over Class Components.
- Create one file per "logical component." Multiple Function Components per file are allowed for structuring, styling, etc.
- Always use JSX (use React.createElement only for app initialization).
- Use
React.PropsWithChildren
instead of defining children manually. - Inject dependencies to services/APIs via Context.
- How to structure folders? "Move files around until it feels right" Recommendation: Separate by modules instead of by type (File Structure – React).
Naming
- Use PascalCase for React components and camelCase for instances
import carCard from "./CarCard";
const CarItem = <CarCard />;
import CarCard from "./CarCard";
const carItem = <CarCard />;
- Use filename as component name
import Footer from "./Nav";
import Footer from "./Footer";
- Speaking component filename
Include the component name in the filename.
clients / Table.tsx;
clients / Table.sc.ts;
clients / Table.gql.ts;
clients / ClientsTable.tsx;
clients / ClientsTable.sc.ts;
clients / ClientsTable.gql.ts;
Do not use DOM props as component props unless they are intended for that purpose.
<MyComponent className="fancy" />
<MyComponent variant="fancy" />
- Always use camelCase for prop names.
<Foo UserName="hello" phone_number={12345678} />
<Foo userName="hello" phoneNumber={12345678} />
- Always name React.useState like this: const [variable, setVariable] = React.useState()
Conventions
- If the property is true, then do not pass the value
<Foo hidden={true} />
<Foo hidden />
- Boolean props should always be optional
type Props = {
hidden: boolean;
};
<Foo hidden={false} />;
type Props = {
visible?: boolean;
};
<Foo />;
Use parameter?: type
instead of parameter: type | undefined
, otherwise parameter = undefined
must be explicitly set.
Recommendations
- If a component becomes too complex:
- Move GraphQL and styled components into separate files with
.gql.ts
and.sc.ts
extensions.cautionThese files should be treated as private and must not be imported from other files.
- Split the component into smaller sub-components.
- Move GraphQL and styled components into separate files with
Working with SVGs
If possible, use SVGs inline with <use>
.
It is important that the SVG file contains an id
, which is then referenced via the hash parameter in the path.
You can use it like this:
<svg>
<use xlinkHref="/icon.svg#custom-id"></use>
</svg>
SVG File:
<svg viewBox="0 0 24 24" id="custom-id" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>icon</title>
<polygon
fill="currentColor"
points="12.661 11.954 17.904 6.707 17.198 6 11.954 11.247 6.707 6.003 6 6.71 11.247 11.954 6.003 17.2 6.71 17.907 11.954 12.661 17.2 17.904 17.907 17.198"
></polygon>
</svg>
import Icon from "../assets/icon.svg"
...
<Icon />
<img src="/icon.svg" />
Background: SVGs imported as modules end up in the JS bundle, which increases download and compile time. Additionally, when used as <img>
tags, they cannot be manipulated (e.g., changing path color).
Common Bugs
These are common pitfalls, not strict rules that must always be followed.
- Do not use array index as
key
(Explanation: Index as a key is considered an anti-pattern)
{
todos.map((todo, index) => <Todo {...todo} key={index} />);
}
{
todos.map((todo) => <Todo {...todo} key={todo.id} />);
}
- Use
&&
with0
or""
export default function PostList({ posts }) {
return (
<div>
<ul>{posts.length && posts.map((post) => <PostItem key={post.id} post={post} />)}</ul>
</div>
);
}
- Renders
0
instead ofnull
for an empty list!
export default function PostList({ posts }) {
return (
<div>
<ul>
{posts.length ? posts.map((post) => <PostItem key={post.id} post={post} />) : null}
</ul>
</div>
);
}
Common Anti-Patterns
Constant components or functions that have no dependencies are created on every render.
const Foo: React:FC = (jobs) => {
const sortJobs = (a: Job, b: Job) => {
return a.createdAt - b.createdAt;
}
const Wrapper = styled.div`...`;
return <Wrapper>...</Wrapper>;
}
Explanation: Created on every render and can lead to performance issues. (See also Hooks API Reference – React)
const sortJobs = (a: Job, b: Job) => {
return a.createdAt - b.createdAt;
}
const Wrapper = styled.div`...`;
const Foo: React:FC = (jobs) => {
return <Wrapper>...</Wrapper>;
}
GraphQL
Use fragment to define required data of a component (see Colocating Fragments)
// DisplayName.tsx
function DisplayName({
user: { firstName, lastName }: GQLUserDetailQuery
}): JSX.Element {
return <>{firstName} {lastName}</>
}
// UserDetail.tsx
const userDetailQuery = gql`
query UserDetail($id: ID!) {
user(id: $id) {
firstName
lastName
}
}
`
function UserDetail({ id }: { id: string}): JSX.Element {
const user = useQuery(userDetailQuery);
...
return (
<>
<DisplayName user={user} />
...
</>
)
}
// DisplayName.tsx
export const displayNameFragment = gql`
fragment DisplayName on User {
firstName
lastName
}
`;
function DisplayName({
user: { firstName, lastName }: { user: GQLDisplayNameFragment }
}): JSX.Element {
return <>{firstName} {lastName}</>
}
// UserDetail.tsx
const userDetailQuery = gql`
query UserDetail($id: ID!) {
user(id: $id) {
...DisplayName
}
}
${displayNameFragment}
`
function UserDetail({ id }: { id: string}): JSX.Element {
const user = useQuery(userDetailQuery);
...
return (
<>
<DisplayName user={user} />
...
</>
)
}
Explanation: The child component should define for itself which fields of a GraphQL object it needs (and not rely on the parent component’s query!). This way, the child component is not directly affected by changes in the parent query, for example if a field is no longer queried.