This page looks best with JavaScript enabled

Simple tips for writing clean React components

 ·  ☕ 9 min read  ·  ✍️ Iskander Samatov

React components



In this post, let’s go over some simple tips that will help you write cleaner React components and scale your project better.

Avoid passing props with the spread operator

Let’s first start with an antipattern that you should avoid. Unless there’s a specific and justified reason to do it, you should avoid passing props down the component tree using a spread operator, like so: {...props}.

Passing props this way indeed makes writing components faster. However, it also makes it very hard to pin down the bugs in your code. You lose confidence in your components, which makes it harder to refactor them, and as a result, the bugs will start creeping in a lot sooner.

Wrap your function parameters in an object

If your function accepts multiple parameters, it’s a good idea to wrap them in an object. Here’s an example:

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

Writing your function signature this way provides several notable advantages:

  1. You no longer need to worry about the order in which you’re passing your arguments. I’ve made this mistake several times, where I would introduce a bug and pass function arguments in the wrong order.
  2. For editors with IntelliSense configured (most of them nowadays), you’ll get a nice autocomplete feature for your function arguments.

For event handlers, use functions that return handler functions

If you’re familiar with functional programming, this technique resembles currying since you’re pre-setting some of the parameters ahead of time.

Let’s take a look at the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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, by writing the handler functions this way, you can keep the component tree cleaner.

Use maps over if/else

When you need to render different elements based on the custom logic, I suggest using maps over if/else statements.

Here’s an example using if/else:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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>
    )
}

And here’s an example that uses maps:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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>
    )
}

Using this simple strategy, your components become more declarative and easier to comprehend. It also makes it simpler to extend the logic and add more items to it.

Hook components

I find this pattern useful, as long as you don’t abuse it. 

You might find yourself using some of the components all over your app. If they need a state to function, you can wrap them with a hook that provides that state. Some good examples of such components are popups, toast notifications, or simple modals. For example, here’s a component hook for a simple confirmation modal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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,
    };

}

Then you can use your component hook like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;

Abstracting the component this way saves you from writing a lot of boilerplate state management code. If you want to learn more about useful React hooks check out my post here .

Splitting components

The following three tips are about intelligently splitting your components. From my experience, keeping your components small is the best way to keep your project manageable.

Use wrappers

If you’re struggling to find a way to split your big component, look at the functionality each element of your component provides. Some elements are there to provide a distinct functionality, like drag and drop handlers.

Here’s an example of a component that implements drag-and-drop using react-beautiful-dnd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 check out the component after we moved all the drag-and-drop logic to a wrapper component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React from 'react'

export default function DraggableSample() {
    return (
        <div>
            <DragWrapper>
                {columns.map((column, index) => {
                    return (
                        <ColumnComponent
                            key={index}
                            column={column}
                        />
                    );
                })}
            </DragWrapper>
        </div>
    )
}

And here’s the code for the wrapper:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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>
    )
}

As a result, it’s easier to glance at the component and understand what it does at a high level. All the functionality for drag-and-drop lives in the wrapper and is much easier to reason about.

Separation of concerns

This is my favorite method of splitting larger components.

In the context of React, the separation of concerns means separating the parts of the components that are responsible for fetching and mutating the data and the ones responsible purely for displaying the element tree.

This method of separation of concerns is the main reason the hook pattern was introduced. You can and should wrap all the logic that manages APIs or global state connections with a custom hook.

For example, let’s take a look at his component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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>
    )
}

Now here’s the refactored version of it with the code split using custom hooks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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>
    )
}

And here’s the hook itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 }
}

Separate file for each component

Often people write code like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react'


export default function SampleComponent({ data }) {

    export const ItemDisplay = ({ name, date }) => (
        <div>
            <h3>{name}</h3>
            <p>{date}</p>
        </div>
    )

    return (
        <div>
            <div>
                {data.map(item => <ItemDisplay item={item} />)}
            </div>
        </div>
    )
}

While there’s nothing terribly wrong with writing a code like that, it’s not a good practice to follow. There are no downsides to moving ItemDisplay to a separate file, and the upsides are that your components are loosely coupled and easier to extend.

Writing clean code for the most part comes down to being mindful and taking the time to follow good patterns and avoiding antipatterns. So if you take the time to follow these patterns, it will help your write cleaner React components. I find these patterns very useful in my project and I hope you do as well!

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.