DEV Community

Cover image for 8 Best Practices for React.js Component Design
Blossom Babs
Blossom Babs

Posted on

8 Best Practices for React.js Component Design

React is one of the most popular JavaScript libraries for building user interfaces, and one of the reasons it gained so much popularity is its Component-Based Architecture. React encourages building UI in reusable components, allowing developers to build complex user interfaces more efficiently.

Since we would be dealing with components in react, it is essential to follow best practices for component design. In this article, we'll explore 10 best practices that will help you write cleaner, more maintainable, and reusable React components:

1 - consistent formatting: There are several ways to create a functional component in a react application, some of them include - arrow functions, functions declarations and function expression. All of these are valid ways to create a component but it is important to stick with 1 way of declaring a component in your application. Inconsistent component functions can make your code difficult to read.

function declaration

function expression

arrow function

2 - Follow a design pattern and stick to it: Following an already established design pattern in your react application keeps your components organised, easy to read, test and make changes. Two of already established practices include:

a. Container/Presentational Pattern: Following this approach ensures a strict separation of concern between the business logic and user interface. Where the container components manages data and state, while presentational components focus on rendering the UI.easier to test.

b. Flux Pattern: Flux pattern was introduced by the facebook team to introduce a unidirectional data flow in your react application, this is commonly enforced using state management libraries like redux.

3 - Single Responsibility Principle: Each component in your app should have a single responsibility, focusing on one specific functionality. Following this principle makes the component more reusable and less prone to bugs. For example, in most cases, a "Button" component should handle ONLY rendering and user interactions.

4 - Prop Types and Default Props: Defining prop types and default values ensures component reliability. PropTypes validate the expected types of props, catching potential bugs early. Default props provide fallback values if a prop is not explicitly passed, avoiding unexpected behaviour.

Do not use typescript in your project? This is totally fine, you can still achieve this using the propTypes library.

npm install --save prop-types

import PropTypes from 'prop-types';

Button.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
};

 function Button(props) {
  const {name, age} = props
  return (
    <div className='App'>
      <h1>Hello {name}</h1>
      <h2>I am {age}</h2>
    </div>
  );
}

export default Button;
Enter fullscreen mode Exit fullscreen mode

5 - Destructuring Props: Take advantage of the object destructing to access props. This can reduce verbosity in the code and enhance readability. It also improves the clarity of component interfaces and makes it easier to identify which props are used.

An example of this can be seen in the Button component code shared above: const {name, age} = props

6 - Prop and state: It is crucial to understand the difference between your props and your state. Props are static data passed around within components. They are immutable and do not change. State is used to manage dynamic data within a component. It represents the internal state of a component, allowing it to handle and respond to user interactions, events, or changes in the component's own logic.

7 - Styling: Styling is an important aspect of React component design. The most important thing here is to pick a styling of choice and remain consistent with it across components in the library. Some of the most popular styling choices include:

  • Use CSS-in-JS libraries: CSS-in-JS libraries like styled-components can make it easier to style your components and create reusable styles.
  • Use CSS modules: CSS modules allow you to create component-level styles that don’t clash with styles from other components.
  • Use utility-first CSS framework like tailwind css
  • Use UI libraries such as material-ui, ant design, chakra etc.
  • Create reusable styles: When creating custom styles with css, sass etc. It is important to prioritise reusable styles that can be used across your application. This can help you create a consistent visual style and make your components more maintainable.

8 - Testing: Testing is perhaps one of the most severely underrated aspects when it comes to building react components. Here are some best practices to consider:

  • Use libraries like Jest and cypress to write unit tests for your components.
  • Test props and state: Test that your components handle props and state correctly. This can help you catch bugs that might not be apparent in the UI.
  • Test end to end user interactions: Test that your components handle user interactions correctly. This can help you ensure that your components are user-friendly and responsive.

Conclusion:

By following these best practices for React component design, you'll be able to create cleaner, more maintainable, and reusable components. Each practice reinforces important principles such as single responsibility, reusability, prop validation, and performance optimisation. Incorporating these practices into your React projects will contribute to better code quality, improved developer experience, and ultimately, more robust applications.

Remember, mastering these best practices requires practice and continuous learning. Stay up-to-date with the evolving React ecosystem and always strive for code that is efficient, readable, and easy to maintain.

Happy coding ☕️

Top comments (18)

Collapse
 
lukeshiru profile image
Luke Shiru

Adding to the article:

Item 4: prop-types is not longer needed. You can simply use TypeScript or JSDocs instead:

// TypeScript

type ExampleProps = {
    readonly name?: string;
    readonly age?: number;
}

const Example = ({ name, age }: ExampleProps) => {
    // ...
};

// Or JSDocs

/**
 * @typedef ExampleProps
 * @property {string} [name]
 * @property {number} [age]
 */

/**
 * @param {ExampleProps} props
 */
const Example = ({ name, age }) => {
    // ...
};
Enter fullscreen mode Exit fullscreen mode

Item 5: You can deconstruct properties in the header of the function, no need to create an extra const declaration:

// instead of ...
const Example = props => {
    const { name, age } = props;
    // ...
};

// do this...
const Example = ({ name, age }) => {
    // ...
};
Enter fullscreen mode Exit fullscreen mode

Item 6: State/props aren't immutable, but indeed should be treated as such. So you can mutate them, but you shouldn't. One wat to help with this is to use the readonly keyword in TypeScript, so the editor tells you when you're mutating something you shouldn't.

Cheers!

Collapse
 
jacksonkasi profile image
Jackson Kasi

hi @lukeshiru it's good to go with typescript, but inn our team just few people only know typescript!
So as for now we continue the prop-types but if i component with PropTypes.oneOf this is not auto suggest. so i feel it's useless ☹️

This is my code! can u give me suggestion or idea why oneOf not auto suggest when i import & use this component?

import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';

const Button = ({
  children,
  variant = 'default',
  size = 'md',
  color,
  shape = 'round',
  disabled = false,
  iconStart,
  iconEnd,
  active = false,
  loading = false,
  block = false,
  className,
  position = 'center',
  link,
  target,
  ...props
}) => {
  const positionClasses = classNames({
    '!justify-start': position === 'start',
    '!justify-center': position === 'center',
    '!justify-end': position === 'end',
  });

  const buttonClasses = classNames(
    'inline-flex items-center justify-center font-poppins font-semibold transition-all duration-200  transform active:scale-95',
    positionClasses,
    {
      'bg-primary-blue text-white border-2 border-transparent hover:bg-primary-blue':
        variant === 'default',
      'bg-transparent text-primary-blue border-2 border-primary-blue hover:text-primary-blue':
        variant === 'twoTone',
      'bg-transparent text-current': variant === 'plain',
      'bg-white text-primary-blue border-2 border-primary-blue hover:bg-primary-blue hover:text-white':
        variant === 'outline',
      'px-5 py-2 text-base sm:leading-8 sm:text-lg': size === 'md',
      'px-4 py-2 text-sm sm:leading-7 sm:text-base': size === 'sm',
      'px-3 py-1 text-xs sm:leading-6 sm:text-sm': size === 'xs',
      'px-6 py-3 text-xl sm:leading-9 sm:text-2xl': size === 'lg',
      'rounded-lg': shape === 'round',
      'rounded-full': shape === 'circle',
      'rounded-none': shape === 'none',
      'cursor-not-allowed': disabled || loading,
      'w-full': block,
    },
    color && `text-${color} border-${color}`,
    !disabled &&
      !loading &&
      color &&
      variant === 'outline' &&
      `hover:bg-${color} hover:text-white`,
  );

  return link ? (
    <Link
      to={link}
      target={target}
      className={`${buttonClasses} ${className}`}
      role="button"
      aria-disabled={disabled || loading}
      {...props}
    >
      {iconStart && <span className="mr-2">{iconStart}</span>}
      {loading ? 'Loading...' : children}
      {iconEnd && <span className="ml-2">{iconEnd}</span>}
    </Link>
  ) : (
    <button
      className={`${buttonClasses} ${className}`}
      disabled={disabled || loading}
      {...props}
    >
      {iconStart && <span className="mr-2">{iconStart}</span>}
      {loading ? 'Loading...' : children}
      {iconEnd && <span className="ml-2">{iconEnd}</span>}
    </button>
  );
};

Button.propTypes = {
  children: PropTypes.node,
  variant: PropTypes.oneOf(['solid', 'twoTone', 'plain', 'default', 'outline']),
  position: PropTypes.oneOf(['start', 'center', 'end']),
  size: PropTypes.oneOf(['lg', 'md', 'sm', 'xs']),
  color: PropTypes.string,
  shape: PropTypes.oneOf(['round', 'circle', 'none']),
  disabled: PropTypes.bool,
  icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  active: PropTypes.bool,
  loading: PropTypes.bool,
  block: PropTypes.bool,
  link: PropTypes.string,
  target: PropTypes.string,
};

export default Button;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lukeshiru profile image
Luke Shiru • Edited

As I mentioned in my original comment you can use TypeScript or JSDocs. JSDocs will add some "type safety" and auto-completion without going full TypeScript (you can read more about it here).

So the code of your component using JSDoc (plus other improvements such as using to instead of link, setting the content once, removing shortcircuits and improving overall setup of the className) could look something like this:

import classNames from "classnames";
import { Link } from "react-router-dom";

/**
 * @typedef ButtonProps
 *
 * @property {"solid" | "twoTone" | "plain" | "default" | "outline"} [variant]
 * @property {"start" | "center" | "end"} [position]
 * @property {"lg" | "md" | "sm" | "xs"} [size]
 * @property {string} [color]
 * @property {"round" | "circle" | "none"} [shape]
 * @property {boolean} [disabled]
 * @property {React.ReactNode} [iconStart]
 * @property {React.ReactNode} [iconEnd]
 * @property {boolean} [active]
 * @property {boolean} [loading]
 * @property {boolean} [block]
 */

/**
 * @param {ButtonProps &
 *  (import("react-router-dom").LinkProps |
 *    ({ to?: undefined } & JSX.IntrinsicElements["button"]))} props
 */
export const Button = ({
    active = false,
    block = false,
    children,
    className,
    color,
    disabled = false,
    iconEnd,
    iconStart,
    loading = false,
    position = "center",
    shape = "round",
    size = "md",
    variant = "default",
    ...props
}) => {
    const buttonClassName = classNames(
        className,
        "inline-flex items-center justify-center font-poppins font-semibold transition-all duration-200  transform active:scale-95",
        `!justify-${position}`,
        {
            "bg-primary-blue text-white border-2 border-transparent hover:bg-primary-blue":
                variant === "default",
            "bg-transparent text-primary-blue border-2 border-primary-blue hover:text-primary-blue":
                variant === "twoTone",
            "bg-transparent text-current": variant === "plain",
            "bg-white text-primary-blue border-2 border-primary-blue hover:bg-primary-blue hover:text-white":
                variant === "outline",
            "px-5 py-2 text-base sm:leading-8 sm:text-lg": size === "md",
            "px-4 py-2 text-sm sm:leading-7 sm:text-base": size === "sm",
            "px-3 py-1 text-xs sm:leading-6 sm:text-sm": size === "xs",
            "px-6 py-3 text-xl sm:leading-9 sm:text-2xl": size === "lg",
            "rounded-lg": shape === "round",
            "rounded-full": shape === "circle",
            "rounded-none": shape === "none",
            "cursor-not-allowed": disabled || loading,
            "w-full": block,
            [`text-${color} border-${color}`]: color,
            [`hover:bg-${color} hover:text-white`]:
                !disabled && !loading && color && variant === "outline",
        },
    );

    const content = (
        <>
            {iconStart ? <span className="mr-2">{iconStart}</span> : undefined}
            {loading ? "Loading..." : children}
            {iconEnd ? <span className="ml-2">{iconEnd}</span> : undefined}
        </>
    );

    return props.to !== undefined ? (
        <Link
            aria-disabled={disabled || loading}
            className={buttonClassName}
            role="button"
            {...props}
        >
            {content}
        </Link>
    ) : (
        <button
            className={buttonClassName}
            disabled={disabled || loading}
            {...props}
        >
            {content}
        </button>
    );
};
Enter fullscreen mode Exit fullscreen mode

You can see it working and try the autocompletion and type checking in this CodeSandbox.

Cheers!

Thread Thread
 
jacksonkasi profile image
Jackson Kasi

wow! really thanks @lukeshiru 😊

Thread Thread
 
blossom profile image
Blossom Babs

@lukeshiru thank you for introducing JSdocs! Great perspective.

Typescripts is the default type safety for most websites building for prop-types is good to know for those who are yet to adopt TS

Collapse
 
mickmister profile image
Michael Kochell

Typescript is definitely worth learning. Your teammates that currently don't know Typescript will start writing much better code if they learn it! Also you can combine Typescript with runtime prop types checks if you're using babel github.com/milesj/babel-plugin-typ...

Thread Thread
 
jacksonkasi profile image
Jackson Kasi

it's looks interesting!

Collapse
 
devnaqvi profile image
devnaqvi

The reason you shouldn't mutate props is that props are immutable in the user interface.

Collapse
 
chantal profile image
Chantal • Edited

That's right @lukeshiru and Typescript makes things easy and smart.

Collapse
 
mickmister profile image
Michael Kochell

For item #1, I typically use arrow functions, but that doesn't allow you to have a one liner for a default export. i.e. this invalid syntax:

export default const MyComponent = () => {
Enter fullscreen mode Exit fullscreen mode

But this works:

export default function MyComponent() {
Enter fullscreen mode Exit fullscreen mode

and this works:

const MyComponent = () => {
}

export default MyComponent;
Enter fullscreen mode Exit fullscreen mode

I prefer the one liner so I typically choose function approach for default exports. You can still do named exports with const though:

export const MyComponent = () => {
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lukeshiru profile image
Luke Shiru

That's not quite right, you can export default an arrow function like this:

export default () => {
    console.log("Hello world!");
};
Enter fullscreen mode Exit fullscreen mode

Tho I generally advocate to not use default exports at all, and just export everything with a name (makes refactoring way easier).

Collapse
 
mickmister profile image
Michael Kochell

You can have a nameless default export like that, but then your editor doesn't know how to import it by name when using autocomplete to resolve something that's not imported yet.

Tho I generally advocate to not use default exports at all, and just export everything with a name (makes refactoring way easier).

That makes sense, though I like the clean look on the import side in the case of default exports. When I read the top of a file, I generally think that named exports in an import statement are "extras" of what that file is exporting, whereas the default is the "main" thing to import. It's also slightly less things for our eyes to parse when scanning imports, when the curly braces are absent. Definitely all preference though. I think pragmatically have them all named makes sense, as you suggested.

Thread Thread
 
fjones profile image
FJones

You can have a nameless default export like that, but then your editor doesn't know how to import it by name when using autocomplete to resolve something that's not imported yet.

This is very much an IDE issue, not a code issue. There's no technical reason not to autocomplete the folder or file name for the import (and, I could be wrong, but I thought jetbrains IDEs did exactly that?).

That said, I'm personally not a fan of immediate default exports anyway. Having a named const that later gets default-exported allows, for instance, attaching further properties to the exported object without needing to rearrange code.

Thread Thread
 
mickmister profile image
Michael Kochell

This is very much an IDE issue, not a code issue.

Totally agree. Nice points here, thank you 👍

Collapse
 
jake0011 profile image
JAKE

i enjoyed every bit of the article and even more the discussions in the comments.

thank you all guys, it’s helping my newbie career.

Collapse
 
dev_bre profile image
Andy

Great overview article. I really liked you mentioned container components, might have been good to expand on that and mention custom hooks to handle logic and state.

Collapse
 
blossom profile image
Blossom Babs

Great suggestion, I should expand on this

Collapse
 
stretch0 profile image
Andrew McCallum • Edited

Some great tips in there.

I'd argue point 1 is negligible though. I don't think mixing between how you define your components makes it harder to read. Messy maybe, but hard to read, no.

As @mickmister alluded to, different ways of defining them affect how you export and ultimately import your component so maybe allowing both patterns is advantageous in different scenarios?