Simple Tips for Writing Clean React Components by Iskander Samatov

In this article, we’ll review some simple tips that will help us write simpler React components and extend our projects better.

Avoid using extension operators to pass props

First, let’s start with an anti-pattern that should be avoided. Unless there is a clear reason to do so, avoid using extension operators to pass props in a component tree, such as: {… Props}.

Passing props this way really makes writing components faster. But it also makes it hard to spot bugs in your code. It makes it harder to refactor components, and it can lead to bugs that are hard to troubleshoot.

Encapsulate function arguments into an object

If a function takes multiple arguments, it is best to wrap them into an object. Here’s an example:

export const sampleFunction = ({ param1, param2, param3 }) = > {
  console.log({ param1, param2, param3 });
}
Copy the code

Writing function signatures in this way has several significant advantages:

  1. You don’t have to worry about the order in which arguments are passed. I’ve made a few buggy mistakes with the order of function arguments.
  2. For editors equipped with smart hints (and most of them do today), automatic filling of function parameters can be done nicely.

For an event handler, the handler is used as the return value of the function

If you are familiar with functional programming, this technique is similar to function currification, because some parameters are set in advance.

Let’s take a look at this example:

import React from 'react'

export default function SampleComponent({ onValueChange }) {

  const handleChange = (key) = > {
    return (e) = > onValueChange(key, e.target.value)
  }

  return (
    <form>
      <input onChange={handleChange('name')} / >
      <input onChange={handleChange('email')} / >
      <input onChange={handleChange('phone')} / >
    </form>)}Copy the code

As you can see, writing handler functions in this way keeps the component tree simple.

Component rendering uses map instead of if/else

When you need to render different elements based on custom logic, I recommend using maps instead of if/else statements.

Here is an example using if/else:

import React from 'react'

const Student = ({ name }) = > <p>Student name: {name}</p>
const Teacher = ({ name }) = > <p>Teacher name: {name}</p>
const Guardian = ({ name }) = > <p>Guardian name: {name}</p>

export default function SampleComponent({ user }) {
  let Component = Student;
  if (user.type === 'teacher') {
    Component = Teacher
  } else if (user.type === 'guardian') {
    Component = Guardian
  }

  return (
    <div>
      <Component name={user.name} />
    </div>)}Copy the code

Here is an example using map:

import React from 'react'

const Student = ({ name }) = > <p>Student name: {name}</p>
const Teacher = ({ name }) = > <p>Teacher name: {name}</p>
const Guardian = ({ name }) = > <p>Guardian name: {name}</p>

const COMPONENT_MAP = {
  student: Student,
  teacher: Teacher,
  guardian: Guardian
}

export default function SampleComponent({ user }) {
  const Component = COMPONENT_MAP[user.type]

  return (
    <div>
      <Component name={user.name} />
    </div>)}Copy the code

Use this simple little strategy to make your components more readable and easier to understand. And it makes logical extension much easier.

Hook component

This pattern is useful as long as it is not overused.

You may find yourself using many components in your application. If they need a state to function, you can encapsulate them as a hook to provide that state. Some good examples of these components are pop-ups, toast notifications, or simple Modal dialogs. For example, here is a hook component for a simple confirmation dialog:

import React, { useCallback, useState } from 'react';
import ConfirmationDialog from 'components/global/ConfirmationDialog';

export default function useConfirmationDialog({ headerText, bodyText, confirmationButtonText, onConfirmClick, }) {
  const [isOpen, setIsOpen] = useState(false);

  const onOpen = () = > {
    setIsOpen(true);
  };

  const Dialog = useCallback(
    () = > (
      <ConfirmationDialog
        headerText={headerText}
        bodyText={bodyText}
        isOpen={isOpen}
        onConfirmClick={onConfirmClick}
        onCancelClick={()= > setIsOpen(false)}
        confirmationButtonText={confirmationButtonText}
      />
    ),
    [isOpen]
  );

  return {
    Dialog,
    onOpen,
  };
}
Copy the code

You can use hook components like this:

import React from "react";
import { useConfirmationDialog } from './useConfirmationDialog'

function Client() {
  const { Dialog, onOpen } = useConfirmationDialog({
    headerText: "Delete this record?".bodyText:
      "Are you sure you want delete this record? This cannot be undone.".confirmationButtonText: "Delete".onConfirmClick: handleDeleteConfirm,
  });

  function handleDeleteConfirm() {
    //TODO: delete
  }

  const handleDeleteClick = () = > {
    onOpen();
  };

  return (
    <div>
      <Dialog />
      <button onClick={handleDeleteClick} />
    </div>
  );
}

export default Client;
Copy the code

Extracting components in this way avoids writing a lot of boilerplate code for state management. If you want to learn more about React hooks check out my post.

Component split

The following three tips are about how to break up components smartly. In my experience, keeping components simple is the best way to keep your project manageable.

Use a wrapper

If you’re struggling to find a way to break down complex components, look at what each element in your component provides. Some elements offer unique functionality, such as drag and drop.

Here is an example of a component that uses react-beautiful-dnd for drag and drop:

import React from 'react'
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
export default function DraggableSample() {
  function handleDragStart(result) { 
    console.log({ result });
  }
  function handleDragUpdate({ destination }) { 
    console.log({ destination });
  }
  const handleDragEnd = ({ source, destination }) = > { 
    console.log({ source, destination });
  };
  return (
    <div>
      <DragDropContext
        onDragEnd={handleDragEnd}
        onDragStart={handleDragStart}
        onDragUpdate={handleDragUpdate}
      >
        <Droppable 
          droppableId="droppable"
          direction="horizontal"
        >
          {(provided) => (
            <div {. provided.droppableProps} ref={provided.innerRef}> 
              {columns.map((column, index) => {
                return (
                  <ColumnComponent
                    key={index}
                    column={column}
                  />
                );
              })}
            </div>
          )}
        </Droppable>
      </DragDropContext>
    </div>)}Copy the code

Now, look at the component after we have moved all the drag logic to the wrapper:

import React from 'react'
export default function DraggableSample() {
  return (
    <div>
      <DragWrapper> 
      {columns.map((column, index) => { 
        return (
          <ColumnComponent key={index} column={column}/>
        );
      })}
      </DragWrapper>
    </div>)}Copy the code

Here is the code for the wrapper:

import React from 'react'
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
export default function DragWrapper({children}) {
  function handleDragStart(result) { 
    console.log({ result });
  }
  function handleDragUpdate({ destination }) { 
    console.log({ destination });
  }
  const handleDragEnd = ({ source, destination }) = > { 
    console.log({ source, destination });
  };
  return (
    <DragDropContext 
      onDragEnd={handleDragEnd}
      onDragStart={handleDragStart} 
      onDragUpdate={handleDragUpdate}
    >
      <Droppable droppableId="droppable" direction="horizontal"> 
        {(provided) => (
          <div {. provided.droppableProps}  ref={provided.innerRef}> 
            {children}
          </div>
        )}
      </Droppable>
    </DragDropContext>)}Copy the code

As a result, you can more intuitively see the functionality of components at a higher level. All the drag-and-drop functionality is in the wrapper, making the code easier to understand.

Separation of concerns

This is my favorite way to break up larger components.

From the React perspective, separation of concerns means separating the parts of the component that are responsible for getting and changing data from the parts that are solely responsible for displaying elements.

This separation of concerns is the main reason that hooks were introduced. You can use custom hooks to encapsulate logic for all methods or global state connections.

For example, let’s look at the following components:

import React from 'react'
import { someAPICall } from './API' 
import ItemDisplay from './ItemDisplay'
export default function SampleComponent() { 
  const [data, setData] = useState([])
  useEffect(() = > { 
    someAPICall().then((result) = > { setData(result)})
  }, [])
  function handleDelete() { console.log('Delete! '); }
  function handleAdd() { console.log('Add! '); }
  const handleEdit = () = > { console.log('Edit! '); };
  return (
    <div>
      <div>
        {data.map(item => <ItemDisplay item={item} />)} 
      </div>
      <div>
        <button onClick={handleDelete} /> 
        <button onClick={handleAdd} /> 
        <button onClick={handleEdit} /> 
      </div>
    </div>)}Copy the code

Here is a refactored version of it, using the custom hook split code:

import React from 'react'
import ItemDisplay from './ItemDisplay'
export default function SampleComponent() {
  const { data, handleDelete, handleEdit, handleAdd } = useCustomHook()
  return (
    <div>
      <div>
        {data.map(item => <ItemDisplay item={item} />)} 
      </div>
      <div>
        <button onClick={handleDelete} /> 
        <button onClick={handleAdd} /> 
        <button onClick={handleEdit} /> 
      </div>
    </div>)}Copy the code

Here is the code for the hook itself:

import { someAPICall } from './API'
export const useCustomHook = () = > { 
  const [data, setData] = useState([])
  useEffect(() = > { 
    someAPICall().then((result) = > { setData(result)})
  }, [])
  function handleDelete() { console.log('Delete! '); }
  function handleAdd() { console.log('Add! '); }
  const handleEdit = () = > { console.log('Edit! '); };
  return { handleEdit, handleAdd, handleDelete, data }
}
Copy the code

Each component is packaged into a separate file

Usually you write code like this:

import React from 'react'
export default function SampleComponent({ data }) {
  const ItemDisplay = ({ name, date }) = > ( 
    <div>
      <h3>{name}</h3>
      <p>{date}</p>
    </div> 
  )
  return (
    <div>
      <div>
        {data.map(item => <ItemDisplay item={item} />)}
      </div>
    </div>)}Copy the code

While there’s nothing wrong with writing React components this way, it’s not a good practice. Moving the ItemDisplay component into a separate file makes your component loosely coupled and easy to expand.

In most cases, writing clean and tidy code requires attention and time to follow good patterns and avoid anti-patterns. So if you take the time to follow these patterns, it helps you write clean React components. I have found these patterns very useful in my projects and hope you do the same!


Click the public account KooFE Front-end Team