DEV Community

Serif COLAKEL
Serif COLAKEL

Posted on

Best Practices for Writing Clean React Code with Examples

 Introduction

React is a JavaScript library for building user interfaces. It’s a very powerful library that can help you build amazing applications. However, it’s important to follow best practices to get the most out of React. This article will teach you some of the best practices for writing clean React code.

 1. Component Structure and Organization

The structure of your components is very important. It can make or break your application. It’s important to follow best practices when writing React code. This article will teach you some of the best practices for writing clean React code.

1.1. Separation of Concerns

Divide your components into three categories:

  • Containers: They are responsible for fetching data and dispatching actions to the store. They are also called smart components.

  • Components: They are responsible for rendering the UI. They are also called dumb components.

  • Pages: They are responsible for connecting the containers and components to the router. They are also called views.

  • HOC: They are responsible for sharing common functionality between components.

  • Hooks: They are responsible for sharing common functionality between components.

  • Services: They are responsible for fetching data from the server.

  • Utils: They are responsible for sharing common functionality between components.

1.2. File Structure

src/
  components/
    Button/
      index.js
      Button.js
      Button.test.js
      Button.module.css
  containers/
    App/
      index.js
      App.js
      App.test.js
      App.module.css
  pages/
    Home/
      index.js
      Home.js
      Home.test.js
      Home.module.css
  store/
    index.js
    rootReducer.js
    rootSaga.js
    configureStore.js
  utils/
    api.js
    constants.js
    helpers.js
    index.js
    validators.js
  index.js
  index.css
  serviceWorker.js
  setupTests.js
Enter fullscreen mode Exit fullscreen mode

1.3. Naming Conventions

  • Components: Use PascalCase for naming the component file and the component itself. For example, Button.js and Button respectively.

  • Containers: Use PascalCase for naming the container file and the container itself. For example, App.js and App respectively.

  • Pages: Use PascalCase for naming the page file and the page itself. For example, Home.js and Home respectively.

  • CSS Modules: Use PascalCase for naming the CSS Module file. For example, Button.module.css.

  • Constants: Use SCREAMING_SNAKE_CASE for naming the constant file. For example, constants.js.

  • Helpers: Use camelCase for naming the helper file. For example, helpers.js.

  • Utils: Use camelCase for naming the util file. For example, utils.js.

  • Tests: Use PascalCase for naming the test file. For example, Button.test.js.

 2. Component Patterns

2.1. Container and Presentational Components

Define your components as either containers or presentational components. Containers are responsible for fetching data and dispatching actions to the store. Presentational components are responsible for rendering the UI.

2.2. Higher-Order Components (HOC)

React’s advanced methodology enables the reuse of component functionality within the render technique. A component can be changed to a high order by using an advanced level of the component.Consideration of higher-order components as one of the best practices for Reactjs developers. Higher-Order Components serve as an advanced technique in React, allowing reuse of the component logic inside the render method. When considering the advanced level, transform a component into a higher order of the component. For example, show some components when the user stays logged in and complement the similar code with every component.Read More:- Reasons Why Use React.js For Web Development

import React, { useState, useEffect } from 'react';

const withData = (WrappedComponent) => {
  const WithData = (props) => {
    const [data, setData] = useState([]);

    useEffect(() => {
      fetch('https://api.example.com/data')
        .then((response) => response.json())
        .then((data) => setData(data));
    }, []);

    return <WrappedComponent data={data} {...props} />;
  };

  return WithData;
};
Enter fullscreen mode Exit fullscreen mode

2.3. Render Props

Use render props to share common functionality between components. For example, you can use a render prop to share the logic for fetching data. You can also use a render prop to share the logic for handling form state.

import React, { useState, useEffect } from 'react';

const WithData = ({ render }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  return render(data);
};
Enter fullscreen mode Exit fullscreen mode

2.4. Hooks

Use hooks to share common functionality between components. For example, you can use a hook to share the logic for fetching data. You can also use a hook to share the logic for handling form state.

import React, { useState, useEffect } from 'react';

const useFetch = (input, { auto, ...init }) => {
  const [result, setResult] = useState([null, null, true]);
  const fetcher = useCallback(
    (query, config) =>
      fetch(query, config)
        .then((res) => res.json())
        .then((data) => setUsers([null, data, false]))
        .catch((err) => setResult([err, null, false])),
    [input, init]
  );
  useEffect(() => {
    if (auto) {
      fetcher(input, init);
    }
  }, []); // if you want to fetch data only once, do this.
  return [...result, fetcher];
  //fetcher(refetch) function or can be used for post api call
};

// USAGE
function App() {
  const [err, users, loading] = useFetch(`/api/users`, { auto: true });

  return (
    <div>
      {users.map((user) => (
        <User key={user.id} user={user} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

ℹ️ It’s similar to react-query/useSWR, both libraries have much more to offer. you can use these libraries, but if you have restrictions on your project you can go ahead with this approach to avoid some extra code.

3. Use an object instead of a switch inside the reducer

This is not a good idea if you have a lot of cases to handle. You can use an object literal as an alternative to switch statements. The object literal is more readable and easier to maintain.

// BAD

const initialState = {
  status: 'idle',
  error: null,
  data: null,
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_DATA_REQUEST':
      return {
        ...state,
        status: 'loading',
      };
    case 'FETCH_DATA_SUCCESS':
      return {
        ...state,
        status: 'success',
        data: action.payload,
      };
    case 'FETCH_DATA_FAILURE':
      return {
        ...state,
        status: 'error',
        error: action.payload,
      };
    default:
      return state;
  }
};

// GOOD

const initialState = {
  status: 'idle',
  error: null,
  data: null,
};

const reducer = (state = initialState, action) => {
  const { type, payload } = action;

  const handlers = {
    FETCH_DATA_REQUEST: {
      ...state,
      status: 'loading',
    },
    FETCH_DATA_SUCCESS: {
      ...state,
      status: 'success',
      data: payload,
    },
    FETCH_DATA_FAILURE: {
      ...state,
      status: 'error',
      error: payload,
    },
  };

  return handlers[type] || state;
};
Enter fullscreen mode Exit fullscreen mode

The map variable must be declared outside the dispatch context otherwise it will always be re-evaluated.

A switch can be implemented using a tree which makes it O(log n) . Searching on the map is O(1)

4. Code Splitting

use React.lazy, It is a very powerful tool that allows you to load components only when they are needed. The React.lazy function lets you render a dynamic import as a regular component.

A good place to start is with routes. When you go with the traditional approach, you have to load both components before rendering them, but this is not a good approach, because it will take extra time to load all components. Even though we are not showing the component.

We can use react.lazy to load the components asynchronously. So when you are at the first(Home) page, you can load the first component and when you are at the second(About) page, you can load the second component. This way we can avoid the unnecessary loading of components.

 4.1. Code Splitting with React.lazy

import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = React.lazy(() => import('./routes/Home'));

const About = React.lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </Router>
);
Enter fullscreen mode Exit fullscreen mode

ℹ️ This is a straightforward use case but what if we have hundreds of routes and components? You will see a massive difference in the performance.

 4.2. CSS in JS

When it comes to large projects, one of the most fundamental of the React best practices is styling and theming. However, it turns out to be a challenging task like maintaining those big CSS files. So, this is where the CSS-in-JS solutions came into the picture.Similar to managing those enormous CSS files, designing and theming can be a complex effort in a larger project. Thus, the idea of CSS-in-JS solutions (i.e., embedding CSS in JavaScript) emerged. Diverse libraries are based on this idea.Among numerous libraries, you can use the needed one based on the requirement, like some for the complicated themes.

 4.3. Lazy Loading Images

Lazy loading is a technique that defers loading of non-critical resources at page load time. Instead, these non-critical resources are loaded at the moment of need. This can help you reduce initial load time and save bandwidth.

import React, { useState, useEffect } from 'react';

const LazyImage = ({ src, alt, ...delegated }) => {
  const [source, setSource] = useState(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setSource(src);
          observer.unobserve(image);
        }
      },
      {
        rootMargin: '200px',
      }
    );

    observer.observe(image);

    return () => {
      observer.unobserve(image);
    };
  }, [src]);

  return <img {...delegated} ref={setImage} src={source} alt={alt} />;
};
Enter fullscreen mode Exit fullscreen mode

5. Keeping State Business Logic Away from Components

Separate the state management logic from the UI logic that can help you in multiple ways. Components that do both are not impactful as they make them less reusable, more difficult to test, and difficult to refactor, especially when you’re looking for state management. Rather than writing the logic for placement of the state inside a component, the best move is to extract it into a hook of its own.and another child component that contains the user interface. Then, call the child inside while the adult hands over all the necessary props.

// BAD

const App = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('idle');

  useEffect(() => {
    setStatus('loading');

    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setStatus('success');
      })
      .catch((error) => {
        setError(error);
        setStatus('error');
      });
  }, []);

  if (status === 'idle' || status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'error') {
    return <div>{error}</div>;
  }

  if (status === 'success') {
    return <div>{data}</div>;
  }
};

// GOOD

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('idle');

  useEffect(() => {
    setStatus('loading');

    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setStatus('success');
      })
      .catch((error) => {
        setError(error);
        setStatus('error');
      });
  }, []);

  return { data, error, status };
};

const App = () => {
  const { data, error, status } = useFetch('https://api.example.com/data');

  if (status === 'idle' || status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'error') {
    return <div>{error}</div>;
  }

  if (status === 'success') {
    return <div>{data}</div>;
  }
};
Enter fullscreen mode Exit fullscreen mode

6. Use TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It offers classes, modules, and interfaces to help you build robust components. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It offers classes, modules, and interfaces to help you build robust components.

import React from 'react';

interface Props {
  name: string;
}

const Hello: React.FC<Props> = ({ name }) => <div>Hello {name}</div>;
Enter fullscreen mode Exit fullscreen mode

7. Use PropTypes

PropTypes is a library that allows you to type-check your React components. It’s a good idea to use PropTypes to document the intended usage of your components.

import React from 'react';

interface Props {
  name: string;
}

const Hello: React.FC<Props> = ({ name }) => <div>Hello {name}</div>;

Hello.propTypes = {
  name: PropTypes.string.isRequired,
};
Enter fullscreen mode Exit fullscreen mode

8. Use ESLint

ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. It’s a good idea to use ESLint to enforce coding standards.

{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "plugins": [
    "react",
    "@typescript-eslint"
  ],
  "rules": {
    "react/prop-types": "off",
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-explicit-any": "off"
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Use Prettier

Prettier is an opinionated code formatter. It’s a good idea to use Prettier to enforce a consistent code style.

{
  "singleQuote": true,
  "trailingComma": "all",
  "arrowParens": "always",
  "printWidth": 80,
  "tabWidth": 2
}
Enter fullscreen mode Exit fullscreen mode

10. Use Husky

Husky is a tool that makes it easy to use Git hooks as part of your development workflow. It’s a good idea to use Husky to enforce a consistent commit message format.

{
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Writing clean and maintainable React code is essential for building robust and scalable applications. In this article, we've discussed several best practices and examples to help you improve your React development process:

  1. Component Structure and Organization:

Organize your components into Containers, Components, Pages, Higher-Order Components (HOC), Hooks, Services, and Utils for better separation of concerns and maintainability.
Use a consistent file structure to organize your code. 2. Component Patterns:

Distinguish between Container and Presentational Components to keep your code organized and maintainable.
Utilize Higher-Order Components (HOC), Render Props, and Hooks for sharing common functionality between components. 3. Use an Object Instead of a Switch Inside the Reducer:

Replace switch statements in reducers with an object literal for better readability and maintainability. 4. Code Splitting:

Use React.lazy for lazy-loading components, which can significantly improve performance in large applications. 5. Keeping State Business Logic Away from Components:

Separate state management logic from UI logic to improve reusability, testability, and maintainability. 6. Use TypeScript:

Adopt TypeScript to add static typing and enhance your code's reliability. 7. Use PropTypes:

Document your components and ensure proper prop types using PropTypes for enhanced code clarity. 8. Use ESLint:

Employ ESLint to enforce coding standards and catch potential issues early. 9. Use Prettier:

Use Prettier for code formatting to maintain a consistent code style. 10. Use Husky:

Implement Husky to enforce a consistent commit message format and maintain a clean version control history.
By following these best practices, you can write clean, organized, and maintainable React code, which will improve collaboration among developers, reduce the likelihood of bugs, and make it easier to scale and extend your React applications. Incorporating these practices into your development workflow will contribute to the success of your React projects.

Top comments (0)