Tips for writing concise React components

  • 2021-11-01 23:41:30
  • OfStack

Directories avoid passing props using extension operators
Encapsulate function parameters into 1 object
For an event handler, use the handler as the return value of the function
Component rendering uses map instead of if/else
Hook component
Component splitting
Using wrappers
Separation of concerns
Each component is encapsulated as a separate file

This article originates from the translation article Simple tips for writing clean React components, and the original author Iskander Samatov

In this article, we review a few simple tips that will help us write simpler React components and better extend 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 really write components faster. But it also makes it difficult to locate bug in the code. It makes us lose confidence in the components we write, makes it harder to refactor them, and can lead to bug that is difficult to troubleshoot.

Encapsulate function parameters into 1 object

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


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

Writing a function signature in this way has several significant advantages:

You don't have to worry about the order of parameter passing. I have made the mistake of bug several times because of the order of function parameter transmission. For editors configured with smart hints (most of them now), automatic filling of function parameters can be done well.

For an event handler, use the handler as the return value of the function

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

Let's 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>
  )
}

As you can see, writing handler functions in this way can keep 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 map instead of if/else statements.

Here is an example of 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>
  )
}

Here is an example of 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>
  )
}

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

Hook component

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

You may find yourself using many components in your application. If they require a state to function, you can encapsulate them as an hook to provide that state. A good example of these components is a pop-up box, an toast notification, or a simple modal dialog box. For example, here is an 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 my post.

Component splitting

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

Using wrappers

If you're trying to find a way to break up complex components, look at the functionality provided by each element of your component. Some elements provide unique functions, such as drag and drop.

Here is an example of a drag-and-drop component using react-beautiful-dnd:


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

Now, look at the component after we 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>
  )
}

Here's 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>
  )
}

Therefore, you can see the function of components at a higher level more intuitively. All the functions for dragging and dropping are in the wrapper, which makes the code easier to understand.

Separation of concerns

This is my favorite method of splitting larger components.

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

This separation of concerns is the main reason for the introduction of hooks. You can encapsulate the logic of all methods or global state connections with 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() { 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>
  )
}

The following is a reconstructed version of it, using the custom hook split code:


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>
  )
}
0

This is the code for the hook itself:


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>
  )
}
1

Each component is encapsulated as a separate file

Usually people write code like this:


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>
  )
}
2

While writing React components in this way is not a big problem, it is not a good practice. Moving the ItemDisplay component to a separate file makes your component loosely coupled and easy to extend.

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 neat React components. I found these patterns very useful in my project, and I hope you will do the same!

These are the tips for writing concise React components. For more information on writing React components, please pay attention to other related articles on this site!


Related articles: