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's
createElementonly for app initialization). - Use React's
PropsWithChildreninstead 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
Casing
Use PascalCase for React components and camelCase for instances.
import carCard from "./CarCard";
const CarItem = <CarCard />;
import CarCard from "./CarCard";
const carItem = <CarCard />;
Naming components and their file
A component and its file should have the same name.
The exception would be the rare case where a file exports multiple components.
export const Footer = () => {
// ...
};
export const Footer = () => {
// ...
};
File names
Prefer meaningful file names over short, generic ones, even when the files are already in a descriptive folder.
clients/Table.tsx
clients/Table.sc.ts
clients/Table.gql.ts
clients/ClientsTable.tsx
clients/ClientsTable.sc.ts
clients/ClientsTable.gql.ts
Prop naming
- Name props in camelCase.
- Do not name props after DOM attributes.
type FooProps = {
phone_number: number;
UserName: string;
};
type BarProps = {
className?: "default" | "fancy";
};
type FooProps = {
phoneNumber: number;
userName: string;
};
type BarProps = {
variant?: "default" | "fancy";
};
Defining boolean props
Boolean props should generally be optional and not have a default value, as they are falsy by default.
type FooProps = {
hidden: boolean;
};
const Bar = ({ hidden = false }: BarProps) => {
if (hidden) {
// ...
}
// ...
};
type BazProps = {
hidden?: boolean;
};
const Baz = ({ hidden }: BazProps) => {
if (hidden) {
// ...
}
// ...
};
Using boolean props
To set a prop to true, simply add the prop, without a value.
<Foo hidden={true} />
<Foo hidden />
React states
Name the state value in camelCase, and the setter function as the value name prefixed with "set", also in camelCase.
const [UserName, setName] = useState();
const [userName, setUserName] = useState();
Recommendations
- If a component becomes too complex:
- Move GraphQL and styled components into separate files with
.gql.tsand.sc.tsextensions.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
- Use
parameter?: typeinstead ofparameter: type | undefined, otherwiseparameter = undefinedmust be explicitly set.
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.
Avoid array index as key
Use a unique identifier as the key, such as the item's id.
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} />);
}
Conditional rendering
Don't use the && syntax when rendering, depending on a number. The value 0 will be rendered by React.
Instead, compare the value to 0 or use the ternary operator (? :).
See React docs: Conditional Rendering
export const PostDetails = ({ post }) => (
<>
<PostContent />
{post.comments.length && <PostComments comments={post.comments} />}
</>
);
export const PostDetails = ({ post }) => (
<>
<PostContent />
{post.comments.length > 0 && <PostComments comments={post.comments} />}
</>
);
export const PostDetails = ({ post }) => (
<>
<PostContent />
{post.comments.length ? <PostComments comments={post.comments} /> : null}
</>
);
Common Anti-Patterns
Components or functions declared as constants inside a component are recreated on every render.
const Foo = (jobs) => {
const sortJobs = (a: Job, b: Job) => {
return a.createdAt - b.createdAt;
};
const Wrapper = styled.div`...`;
return <Wrapper>...</Wrapper>;
};
Explanation: They are recreated 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 = (jobs) => {
return <Wrapper>...</Wrapper>;
};
GraphQL
Use a fragment to define the required data of a component (see Colocating Fragments)
function DisplayName({
user: { firstName, lastName }: GQLUserDetailQuery,
}) {
return <>{firstName} {lastName}</>
}
const userDetailQuery = gql`
query UserDetail($id: ID!) {
user(id: $id) {
firstName
lastName
}
}
`;
function UserDetail({ id }: { id: string }) {
const user = useQuery(userDetailQuery);
// ...
return (
<>
<DisplayName user={user} />
{/* ... */}
</>
);
}
export const displayNameFragment = gql`
fragment DisplayName on User {
firstName
lastName
}
`;
function DisplayName({
user: { firstName, lastName }: { user: GQLDisplayNameFragment },
}): JSX.Element {
return <>{firstName} {lastName}</>;
}
const userDetailQuery = gql`
query UserDetail($id: ID!) {
user(id: $id) {
...DisplayName
}
}
${displayNameFragment}
`;
function UserDetail({ id }: { id: string }) {
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.