SoFunction
Updated on 2025-04-07

Tips for writing concise React components

This article originates from a translated articleSimple tips for writing clean React components, original authorIskander Samatov

In this post, we will review some simple tips that will help us write cleaner React components and better scale our projects.

Avoid passing props using extension operators

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

Passing props in this way can indeed write components faster. But this also makes it difficult for us to locate bugs in the code. This will make us lose confidence in the components we write, make it more difficult for us to refactor components, and may lead to difficult bugs to troubleshoot.

Encapsulate function parameters into an object

If a function receives multiple parameters, it is best to encapsulate them into one object. For example:

export const sampleFunction = ({ param1, param2, param3 }) => {
  ({ param1, param2, param3 });
}

Writing function signatures in this way has several significant advantages:

  1. You don't have to worry about the order of parameters being passed. I have made several mistakes that I have a bug due to the order of passing the arguments of the function.
  2. For editors configured with smart prompts (most of them nowadays have them), auto-filling of function parameters can be done well.

For event handling functions, use the handling function as the return value of the function

If you are familiar with functional programming, this programming technique is similar to function currying because some parameters have been 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, )
  }

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

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

Component rendering uses map instead of if/else

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

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 ( === 'teacher') {
    Component = Teacher
  } else if ( === 'guardian') {
    Component = Guardian
  }

  return (
    <div>
      <Component name={} />
    </div>
  )
}

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[]

  return (
    <div>
      <Component name={} />
    </div>
  )
}

Using this simple little strategy can make your components more readable and easier to understand. And it also makes logical extensions easier.

Hook Components

As long as it is not abused, this model is very useful.

You may find yourself using a lot of components in your application. If they need a state to work, you can encapsulate them as a hook to provide that state. Some good examples of these components are pop-up boxes, 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,
  };
}

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;

Extracting components in this way can avoid writing a lot of boilerplate code for state management. If you want to know more about React hooks, please check it outMy post

Component splitting

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

Using a wrapper

If you are struggling to find a way to split up complex components, look at the functionality that each element in your component provides. Some elements provide unique features, such as drag and drop.

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

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

Now, take a look at the components after we move all the drag logic to the wrapper:

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

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) { 
    ({ result });
  }
  function handleDragUpdate({ destination }) { 
    ({ destination });
  }
  const handleDragEnd = ({ source, destination }) => { 
    ({ source, destination });
  };
  return (
    <DragDropContext 
      onDragEnd={handleDragEnd}
      onDragStart={handleDragStart} 
      onDragUpdate={handleDragUpdate}
    >
      <Droppable droppableId="droppable" direction="horizontal"> 
        {(provided) => (
          <div {...}  ref={}> 
            {children}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  )
}

Therefore, it is possible to see more intuitively the functionality of components at a higher level. All the functions for dragging are in the wrapper, making the code easier to understand.

Separation of concerns

This is my favorite way to split larger components.

From a React perspective, separation of concerns means separating the parts of the component that are responsible for obtaining and changing data and the parts that are purely responsible for displaying elements.

This method of separating concerns is the main reason for introducing hooks. You can encapsulate the logic of all methods or global state connections with a custom hook.

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() { ('Delete!'); }
  function handleAdd() { ('Add!'); }
  const handleEdit = () => { ('Edit!'); };
  return (
    <div>
      <div>
        {(item => <ItemDisplay item={item} />)} 
      </div>
      <div>
        <button onClick={handleDelete} /> 
        <button onClick={handleAdd} /> 
        <button onClick={handleEdit} /> 
      </div>
    </div>
  )
}

Here is its refactored version, splitting the code using a custom hook:

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

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() { ('Delete!'); }
  function handleAdd() { ('Add!'); }
  const handleEdit = () => { ('Edit!'); };
  return { handleEdit, handleAdd, handleDelete, data }
}

Each component is encapsulated as a separate file

Usually people 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>
        {(item => <ItemDisplay item={item} />)}
      </div>
    </div> 
  )
}

While there is no big problem writing React components this way, it is not a good practice. Moving the ItemDisplay component to a separate file can make your components loosely coupled and easy to scale.

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

The above is the detailed content of the tips for writing concise React components. For more information on the tips for writing React components, please pay attention to my other related articles!