Skip to content

TobitSoftware/react-project-guideline

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tobit.Labs™ React Project Guideline

Defines a consistent structure for React projects.


Most topics in this guide go beyond what can be statically analyzed ESLint. For that, we also have an ESLint-configuration.

Overview

Components

Use Function Components

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.

Enforce Consistent Component Structure

Write function components with this structure:

const Button = ({ children, onClick }) => {
    // ...
};

Button.propTypes = {
    // ...
};

Button.defaultProps = {
    // ...
};

Button.displayName = 'Button';

export default Button;

Keep Components Small

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

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:

  1. 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.

  2. 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 as default

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 as default.

Avoid Uncontrolled Inputs

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)}
        />
    );
};

Set the displayName Property

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 && for Conditional Rendering

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 for null, undefined, the empty string or false, but it will render 0 and NaN. Read more about this here.

Props

Destructure Props

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>
);

Avoid Objects in Props

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>
);

Use Precise PropTypes

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 Reusable Shapes

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,
};

File Structure

Represent the DOM Structure

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 if redux toolkit is used in the project. Check out the "Use Redux Toolkit for Complex State" section for more information.

State Management

Use Local State or Context for Simple State

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 Redux Toolkit for Complex State

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

Stick to the BEM Methodology

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

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 clsx for Composing Classes

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 the classnames-package but is smaller in size and faster at runtime.

Utilities

Extract Commonly Used Utilities

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.

Do Not Export Utilities as default

Use named exports instead of export default for utilities.

// good
export function getDate() {
    // ...
}

// good
export const getDate = () => {
    // ...
};

// bad
function getDate() {
    // ...
}

export default getDate;

Use Keywords for File Names

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 = () => {
    // ...
};

Other

Use Tree-Shaking for chayns-components

chayns-components have to be tree-shaken. If your tooling is not automatically configured for this, refer to the tree-shaking guide.

Naming conventions

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 or should (e.g. isLoading, hasTitle or shouldShowImage).

Code formatting

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.

About Clean Code

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.