Most topics in this guide go beyond what can be statically analyzed ESLint. For that, we also have an ESLint-configuration.
- Components
- Props
- File Structure
- State Management
- Styling
- Utilities
- Other
- Naming conventions
- Code formatting
- About Clean Code
Write components as function components using hooks. Only if the desired functionality is not available to function components (e.g. Error Boundaries), use class components.
// good
const Button = () => {
return /* jsx... */;
};
// avoid
class Button extends React.Component {
// ...
}
Functions are easier to understand than classes and the
this
-keyword. Hooks allow for extraction of stateful logic. Read more about why we use function components and hooks in the official React documentation.
Write function components with this structure:
const Button = ({ children, onClick }) => {
// ...
};
Button.propTypes = {
// ...
};
Button.defaultProps = {
// ...
};
Button.displayName = 'Button';
export default Button;
Keep your components as small as possible. When components grow, split them into multiple components.
Aim to have fewer than 200 lines of code in a component file.
Use Error Boundaries around different parts of your application.
To determine where to use Error Boundaries, think about if the surrounding components would still function and make sense, if your component would unmount. Some examples of this:
-
Two Column Layout
Must have an Error Boundary around each of the columns. If one of the columns crashes the other one could still be used normally.
-
Message in Chat Application
Must not have an Error Boundary around each message component. A user would be confused if one message was missing from the chat. In this case it is better to unmount the whole message list.
When React encounters an error in one of your components, it will unmount the tree up to the next Error Boundary or the root. That means without error boundaries, your whole screen will go blank when you throw an Error in one of your components.
Export components with export default
, but always assign them to a variable
with a name. Never export an anonymous function
// good
export default Button;
// bad
export { Button };
// worst
export default () => {
// component code...
};
Code splitting with
React.lazy
only works with components exported asdefault
.
Always provide the value
-prop to <input>
, <textarea>
or any other
component that can be controlled.
// good
const MyForm = () => {
const [inputValue, setInputValue] = useState('');
return (
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
);
};
// bad
const MyForm = () => {
const [inputValue, setInputValue] = useState('');
return (
<input
// value prop not set
onChange={(e) => setInputValue(e.target.value)}
/>
);
};
Specify the displayName
property on your components. Assign it to the exact
string which is also your variable and file name.
// good
const Button = () => {
// ...
};
// ...
Button.displayName = 'Button';
export default Button;
The
displayName
property is used by the React Devtools and error messages within React. For more information refer to this article.
Use the logical AND operator (&&
) in JSX for conditional rendering.
// good
<div>
{todos.length > 0 && <TodoList todos={todos} />}
</div>
// bad
<div>
{todos.length > 0 ? <TodoList todos={todos} /> : null}
</div>
While this syntax provides great readability, be cautious with non-boolean values on the left hand of the
&&
operator. React will skip rendering fornull
,undefined
, the empty string orfalse
, but it will render0
andNaN
. Read more about this here.
Destructure props inside the parameter parentheses for function components.
// good
const TodoItem = ({ text, checked }) => (
<div className="todo">
<Checkbox className="todo__checkbox" checked={checked} />
<div>{text}</div>
</div>
);
// bad
const TodoItem = (props) => (
<div className="todo">
<Checkbox className="todo__checkbox" checked={props.checked} />
<div>{props.text}</div>
</div>
);
Pass single props rather than grouping props as an object.
// good
const TodoItem = ({ id, title, text }) => (
<div className="todo">
<TodoTitle id={id} title={title} />
<div>{text}</div>
</div>
);
// bad
const TodoItem = ({ todo }) => (
<div className="todo">
<TodoTitle id={todo.id} title={todo.title} />
<div>{todo.text}</div>
</div>
);
Check objects passed to your component with PropTypes.shape(...)
. Never use
PropTypes.object
or PropTypes.any
.
Similarily, use PropTypes.arrayOf(...)
instead of PropTypes.array
.
If a prop is passed as an object, it has to be checked with PropTypes.shape
,
not PropTypes.object
or PropTypes.any
.
// good
Todo.propTypes = {
todo: PropTypes.shape({
creationTime: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
checked: PropTypes.bool,
}).isRequired,
toggleTodoChecked: PropTypes.func.isRequired,
removeTodo: PropTypes.func.isRequired,
};
// bad
Todo.propTypes = {
todo: PropTypes.object.isRequired,
toggleTodoChecked: PropTypes.func.isRequired,
removeTodo: PropTypes.func.isRequired,
};
A short explanation of
PropTypes.shape
can be found here.
Extract complex shapes used in multiple occasions into the
src/constants/shapes.js
-file for increased reusability.
shapes.js
:
const TODO_SHAPE = {
creationTime: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
checked: PropTypes.bool,
};
Todo.jsx
:
Todo.propTypes = {
todo: PropTypes.shape(TODO_SHAPE).isRequired,
toggleTodoChecked: PropTypes.func.isRequired,
removeTodo: PropTypes.func.isRequired,
};
Strive to represent the DOM tree with your file structure. That means that nested components should live in nested folders.
Put shared components in a shared
-folder in the components
directory.
If there are multiple different view for an app (e.g. user-view and admin-view), split their components into different top level folders:
components
├───admin
├───shared
└───user
A components
directory structure should look like this:
src
└───components
│ App.jsx
├───add-todo
│ addTodo.scss
│ AddTodo.jsx
├───headline
│ Headline.jsx
├───intro
│ Intro.jsx
└───todos
│ Todos.jsx
├───todo
│ Todo.jsx
│ todo.scss
└───todos-headline
TodosHeadline.jsx
And a project directory structure should look like this:
src
├───api
│ └───todos
│ get.js
│ post.js
├───components
│ │ App.jsx
│ ├───headline
│ │ Headline.jsx
│ └───todos
│ │ Todos.jsx
│ └───todo
│ Todo.jsx
│ todo.scss
├───constants
│ config.js
│ shapes.js
│ types.js
├───redux-modules
│ └───todos
│ actions.js
│ selectors.js
│ slice.js
│ transforms.js
└───utils
date.js
Constant values of a project should be declared in files separated by topic within the
constants
folder. These can then be easily identified as constants within the project due to the naming conventions.
The
redux-modules
directory should only exist ifredux toolkit
is used in the project. Check out the "Use Redux Toolkit for Complex State" section for more information.
Manage simple state without external libraries. Use the useState
or
useReducer
hooks and pass props down.
When prop drilling becomes tedious, use React Context and the useContext
hook.
A guide on how to manage application state without external libraries can be found in this article by Kent C. Dodds.
Use the @reduxjs/toolkit
to manage complex
state.
Separate global state into slices. A slice should look like this:
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++;
},
decrement(state) {
state.value--;
},
incrementByAmount(state, { payload }) {
state.value += payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const counterReducer = counterSlice.reducer;
Redux Toolkit is the officially recommended way of using Redux.
Styling your components with (S)CSS-files. Name your classes to follow the BEM methodology.
// good
.todo-item {
&__header {
// ...
}
&__checkbox {
// ...
&--checked {
// ...
}
}
}
// bad
.todoItem {
// ...
.todoItemHeader {
// ...
}
.todoItemCheckbox {
// ...
}
}
Avoid inline-styles. If style values must be calculated in the context of a component, it is fine to use inline-styles.
// good
const TodoItem = ({ id, title, text }) => (
<div className="todo" style={{ height: calculateHeight() }}>
<TodoTitle id={id} title={title} />
<div className="todo__text">{text}</div>
</div>
);
// bad
const TodoItem = ({ id, title, text }) => (
<div
style={{
margin: '8px 0',
backgroundColor: '#a1a1a1',
}}
>
<TodoTitle id={id} title={title} />
<div style={{ fontSize: 12 }}>{text}</div>
</div>;
);
Use the clsx package for constructing
className
strings.
import clsx from 'clsx';
// ...
const locationMapClasses = clsx(
'locations-map',
!isMapsScriptLoaded && 'locations-map--hidden'
);
The
clsx
-package provides the same API as theclassnames
-package but is smaller in size and faster at runtime.
Never export utility functions from a component file. Create a separate file for utility functions if they are used in multiple locations.
Extract big utility functions, even if only used in one component.
Big component files are intimidating and difficult to scan. Aim to have component files of 200 or fewer lines of code.
Use named exports instead of export default
for utilities.
// good
export function getDate() {
// ...
}
// good
export const getDate = () => {
// ...
};
// bad
function getDate() {
// ...
}
export default getDate;
Name utility files according to the topic of the contained functions so multiple functions can live next to each other without the need to create a new file per function. This makes utilities easier to find.
src
└───utils
├───logger.js // good
├───initializeLogger.js // bad
│ ...
├───viewport.js // good
└───getWindowHeight.js // bad
A viewport.js
file could look like this:
// As a function declaration
export function getWindowHeight() {
// ...
}
export function getScrollPosition() {
// ...
}
// Or as an arrow function
export const getWindowHeight = () => {
// ...
};
export const getScrollPosition = () => {
// ...
};
chayns-components
have to be tree-shaken. If your tooling is not automatically
configured for this, refer to the
tree-shaking guide.
Follow these naming conventions:
-
Constants (
UPPER_CASE
)const FRONTEND_VERSION = 'development';
-
Functions (
camelCase
)Name functions as self-explanatory as possible. Names of functions that handle user interaction start with
handle
function handleShowCategory() { // ... }
-
Folders (
kebab-case
)├───add-todo │ addTodo.scss └───todos todos.scss
-
Files (
camelCase
)├───add-todo │ addTodo.scss └───todos todos.scss
-
Component Files (
PascalCase
)Name component files with
PascalCase
, just like components themselves.└───components │ App.jsx ├───add-todo │ AddTodo.jsx ├───headline │ Headline.jsx
-
Boolean Values
Prefix boolean values with
is
,has
orshould
(e.g.isLoading
,hasTitle
orshouldShowImage
).
Format the code in your project with Prettier. Use the
following configuration in your package.json
-file:
{
"...": "",
"prettier": {
"proseWrap": "always",
"singleQuote": true,
"tabWidth": 4
},
"...": ""
}
To format files right in your editor, check out the Prettier editor integrations page or the Webstorm guide, if you are using that.
Using the same code formatter important for clean Git diffs. Read this for more information on why we use Prettier.
Strive to write clean code that is readable and maintainable. If you want to read more about clean code, check out "Clean Code JavaScript" by Ryan McDermott.