DEV Community

Cover image for How I dealt with Props Drilling in Atomic Design
SeongKuk Han
SeongKuk Han

Posted on

How I dealt with Props Drilling in Atomic Design

I was in charge of refactoring components at my company. A team leader of the project told me that Atomic Design would be well-suited for the project, so, I started to work on that.

In this post, I will talk about what problems I encountered and how I dealt with them. I figured it fitted my project, I'm not sure whether it would work out on your projects as well or not. I just want you to be able to get some ideas from this.


Atomic Design Methodology

"Atomic design is a methodology composed of five distinct stages working together to create interface design systems in a more deliberate and hierarchical manner."

For more detail, I recommend you to read bradfrost.

Some people also change the stages for their projects because the stages aren't suitable for every project.


I implemented the five distinct stages following the below descriptions.

Atoms - smallest components that can't be divided, such as button, input, label and etc.

Molecules - components that follow the single responsibility principle, they have only one role, it might sound similar to atoms, but it's different. Imagine an icon button. an icon and a button can be as atoms and an icon button can consist of them. If then, the icon button is still a button, it has one role, so, it is treated as a molecule.

Organisms - components that consist of two more atoms or molecules or have two more roles like list, post, etc.

Templates - components that define where components should be located.

Pages - components that handle all of the data flow and render atoms, molecules, organisms on templates.


Props Drilling

Image description

The biggest problem was Prop Drilling that I encountered while working on it. It made code that is in Pages complicated.

I searched to know how others approach to the problem and also asked a colleague who I had worked with before(thanks Robert).

And then, I found two methods that are appropriate for my situation.

Containers - provide fixed props and handle APIs themselves.

Custom Hooks - provide states and handlers that need when rendering organisms.

These are implemented in organisms.

Using Containers

Let's say,

There are components Post and PostList, PostList is a group of Post, and three APIs.

url returns
/notice notice posts
/freeboard freeboard posts
/discuss discuss posts

Post

import styled from '@emotion/styled/macro';

export interface PostProps {
  id: string;
  title: string;
  content: string;
}

export const Post = ({ id, title, content }: PostProps) => {
  return (
    <Wrapper key={id}>
      <div>{title}</div>
      <hr />
      <div>{content}</div>
    </Wrapper>
  );
};

const Wrapper = styled.div`
  box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
  padding: 24px;
  width: 150px;
  height: 100px;

  > hr {
    margin: 8px 8px;
  }

  > div {
    text-align: center;
  }
`;
Enter fullscreen mode Exit fullscreen mode

PostList

import styled from '@emotion/styled/macro';
import { Post, PostProps } from '../../molecules/Post';

interface PostListProps {
  posts: PostProps[];
}

export const PostList = ({ posts }: PostListProps) => {
  return (
    <Wrapper>
      {posts.map((post) => (
        <Post key={post.id} {...post} />
      ))}
    </Wrapper>
  );
};

const Wrapper = styled.div`
  display: flex;
  column-gap: 16px;
  row-gap: 16px;
`;
Enter fullscreen mode Exit fullscreen mode

Because all of the data flow should be implemented in pages, I might write code like this.

Page

import { useState, useEffect } from 'react';
import styled from '@emotion/styled/macro';
import { PostList } from '../organisms/PostList';

const fetchPosts = async (type: 'notice' | 'freeboard' | 'discuss') => {
  const res = await fetch(`http://localhost:4000/${type}`);
  return await res.json();
};

export const PostListWithoutContainer = () => {
  const [noticeList, setNoticeList] = useState([]);
  const [freeboardList, setFreeboardList] = useState([]);
  const [discussList, setDiscussList] = useState([]);

  useEffect(() => {
    fetchPosts('notice').then((posts) => setNoticeList(posts));
    fetchPosts('freeboard').then((posts) => setFreeboardList(posts));
    fetchPosts('discuss').then((posts) => setDiscussList(posts));
  }, []);

  return (
    <Page>
      <PostList posts={noticeList} />
      <hr />
      <PostList posts={freeboardList} />
      <hr />
      <PostList posts={discussList} />
    </Page>
  );
};

const Page = styled.div`
  padding: 16px;
`;
Enter fullscreen mode Exit fullscreen mode

preview

What if PostList do the same action in most cases?
In this case, you can make Container for that.

PostListContainer

import { useState, useEffect } from 'react';
import { PostList } from '.';

export interface PostListContainerProps {
  type: 'notice' | 'freeboard' | 'discuss';
}

const fetchPosts = async (type: 'notice' | 'freeboard' | 'discuss') => {
  const res = await fetch(`http://localhost:4000/${type}`);
  return await res.json();
};

export const PostListContainer = ({ type }: PostListContainerProps) => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetchPosts(type).then((posts) => setPosts(posts));
  }, [type]);

  return <PostList posts={posts} />;
};
Enter fullscreen mode Exit fullscreen mode

Page

import styled from '@emotion/styled/macro';
import { PostListContainer } from '../organisms/PostList/PostListContainer';

export const PostListWithContainer = () => {
  return (
    <Page>
      <PostListContainer type="notice" />
      <hr />
      <PostListContainer type="freeboard" />
      <hr />
      <PostListContainer type="discuss" />
    </Page>
  );
};

const Page = styled.div`
  padding: 16px;
`;
Enter fullscreen mode Exit fullscreen mode

Code in Pages has gotten looking simple and PostList is still there. If you want to use PostList in another project, just take the component except container components to the project.

The containers can be made for respective purposes.

Using Custom Hooks

I'm going to make a profile edit form.

For that, I've created two atoms TextField, Label and a molecule TextFieldWithLabel.

TextField

import { InputHTMLAttributes } from 'react';
import styled from '@emotion/styled/macro';

type TextFieldProps = InputHTMLAttributes<HTMLInputElement>;

export const TextField = (props: TextFieldProps) => (
  <StyledTextField type="text" {...props} />
);

const StyledTextField = styled.input`
  outline: 0;
  border: 1px solid #a3a3a3;
  border-radius: 4px;
  padding: 8px;

  &:focus {
    border: 2px solid #3b49df;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Label

import { LabelHTMLAttributes } from 'react';
import styled from '@emotion/styled/macro';

type LabelProps = LabelHTMLAttributes<HTMLLabelElement>;

export const Label = (props: LabelProps) => <StyledLabel {...props} />;

const StyledLabel = styled.label`
  font-size: 14px;
`;
Enter fullscreen mode Exit fullscreen mode

TextFieldWithLabel

import { ChangeEventHandler } from 'react';
import styled from '@emotion/styled/macro';

import { Label } from '../../atoms/Label';
import { TextField } from '../../atoms/TextField';

interface TextFieldWithLabelProps {
  id?: string;
  value?: string;
  onChange?: ChangeEventHandler<HTMLInputElement>;
  label?: string;
}

export const TextFieldWithLabel = ({
  id,
  value,
  onChange,
  label,
}: TextFieldWithLabelProps) => {
  return (
    <Wrapper>
      <Label htmlFor={id}>{label}</Label>
      <TextField id={id} value={value} onChange={onChange} />
    </Wrapper>
  );
};

const Wrapper = styled.div`
  display: flex;
  flex-direction: column;
  row-gap: 8px;
`;
Enter fullscreen mode Exit fullscreen mode

Then, I've created a form component in Organisms.

EditProfileForm

import { ChangeEventHandler } from 'react';
import styled from '@emotion/styled/macro';

import { TextFieldWithLabel } from '../../molecules/TextFieldWithLabel';

interface EditProfileFormProps {
  formTitle?: string;
  name?: string;
  nameLabel?: string;
  onNameChange?: ChangeEventHandler<HTMLInputElement>;
  email?: string;
  emailLabel?: string;
  onEmailChange?: ChangeEventHandler<HTMLInputElement>;
  username?: string;
  usernameLabel?: string;
  onUsernameChange?: ChangeEventHandler<HTMLInputElement>;
  websiteUrl?: string;
  websiteUrlLabel?: string;
  onWebsiteUrlChange?: ChangeEventHandler<HTMLInputElement>;
  location?: string;
  locationLabel?: string;
  onLocationChange?: ChangeEventHandler<HTMLInputElement>;
  bio?: string;
  bioLabel?: string;
  onBioChange?: ChangeEventHandler<HTMLInputElement>;
}

export const EditProfileForm = ({
  formTitle,
  name,
  nameLabel,
  onNameChange,
  email,
  emailLabel,
  onEmailChange,
  username,
  usernameLabel,
  onUsernameChange,
  websiteUrl,
  websiteUrlLabel,
  onWebsiteUrlChange,
  location,
  locationLabel,
  onLocationChange,
  bio,
  bioLabel,
  onBioChange,
}: EditProfileFormProps) => {
  return (
    <Form>
      <h3>{formTitle}</h3>
      <TextFieldWithLabel
        label={nameLabel}
        value={name}
        onChange={onNameChange}
      />
      <TextFieldWithLabel
        label={emailLabel}
        value={email}
        onChange={onEmailChange}
      />
      <TextFieldWithLabel
        label={usernameLabel}
        value={username}
        onChange={onUsernameChange}
      />
      <TextFieldWithLabel
        label={websiteUrlLabel}
        value={websiteUrl}
        onChange={onWebsiteUrlChange}
      />
      <TextFieldWithLabel
        label={locationLabel}
        value={location}
        onChange={onLocationChange}
      />
      <TextFieldWithLabel label={bioLabel} value={bio} onChange={onBioChange} />
    </Form>
  );
};

const Form = styled.form`
  padding: 24px;
  width: 300px;
  display: flex;
  flex-direction: column;
  row-gap: 12px;
  box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px,
    rgb(209, 213, 219) 0px 0px 0px 1px inset;
`;
Enter fullscreen mode Exit fullscreen mode

When you render this form in Pages, you might write code like this.

Page

import React, { useState } from 'react';
import styled from '@emotion/styled/macro';
import { EditProfileForm } from '../organisms/EditProfileForm';

interface EditProfileFormValues {
  name: string;
  email: string;
  username: string;
  websiteUrl: string;
  location: string;
  bio: string;
}

export const EditProfileFormWithoutCustomHook = () => {
  const [values, setValues] = useState<EditProfileFormValues>({
    name: '',
    email: '',
    username: '',
    websiteUrl: '',
    location: '',
    bio: '',
  });

  const handleValueChange =
    (key: keyof EditProfileFormValues) =>
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setValues((prevValues) => ({
        ...prevValues,
        [key]: e.target.value,
      }));
    };

  return (
    <Page>
      <EditProfileForm
        formTitle="Edit Profile"
        nameLabel="name"
        emailLabel="email"
        usernameLabel="username"
        websiteUrlLabel="websiteUrl"
        locationLabel="location"
        bioLabel="bio"
        onNameChange={handleValueChange('name')}
        onEmailChange={handleValueChange('email')}
        onUsernameChange={handleValueChange('username')}
        onWebsiteUrlChange={handleValueChange('websiteUrl')}
        onLocationChange={handleValueChange('location')}
        onBioChange={handleValueChange('bio')}
        {...values}
      />
    </Page>
  );
};

const Page = styled.div`
  padding: 16px;
`;
Enter fullscreen mode Exit fullscreen mode

But if you render this form in other pages too, you would write the same code in the pages.

in this case, you can use custom hooks.

EditProfileForm

import {
  useState,
  useCallback,
  useMemo,
  ChangeEvent,
  ChangeEventHandler,
} from 'react';
import styled from '@emotion/styled/macro';

import { TextFieldWithLabel } from '../../molecules/TextFieldWithLabel';

interface EditProfileFormValues {
  name: string;
  email: string;
  username: string;
  websiteUrl: string;
  location: string;
  bio: string;
}

interface EditProfileFormProps {
  formTitle?: string;
  name?: string;
  nameLabel?: string;
  onNameChange?: ChangeEventHandler<HTMLInputElement>;
  email?: string;
  emailLabel?: string;
  onEmailChange?: ChangeEventHandler<HTMLInputElement>;
  username?: string;
  usernameLabel?: string;
  onUsernameChange?: ChangeEventHandler<HTMLInputElement>;
  websiteUrl?: string;
  websiteUrlLabel?: string;
  onWebsiteUrlChange?: ChangeEventHandler<HTMLInputElement>;
  location?: string;
  locationLabel?: string;
  onLocationChange?: ChangeEventHandler<HTMLInputElement>;
  bio?: string;
  bioLabel?: string;
  onBioChange?: ChangeEventHandler<HTMLInputElement>;
}

export const EditProfileForm = ({
  formTitle,
  name,
  nameLabel,
  onNameChange,
  email,
  emailLabel,
  onEmailChange,
  username,
  usernameLabel,
  onUsernameChange,
  websiteUrl,
  websiteUrlLabel,
  onWebsiteUrlChange,
  location,
  locationLabel,
  onLocationChange,
  bio,
  bioLabel,
  onBioChange,
}: EditProfileFormProps) => {
  return (
    <Form>
      <h3>{formTitle}</h3>
      <TextFieldWithLabel
        label={nameLabel}
        value={name}
        onChange={onNameChange}
      />
      <TextFieldWithLabel
        label={emailLabel}
        value={email}
        onChange={onEmailChange}
      />
      <TextFieldWithLabel
        label={usernameLabel}
        value={username}
        onChange={onUsernameChange}
      />
      <TextFieldWithLabel
        label={websiteUrlLabel}
        value={websiteUrl}
        onChange={onWebsiteUrlChange}
      />
      <TextFieldWithLabel
        label={locationLabel}
        value={location}
        onChange={onLocationChange}
      />
      <TextFieldWithLabel label={bioLabel} value={bio} onChange={onBioChange} />
    </Form>
  );
};

const Form = styled.form`
  padding: 24px;
  width: 300px;
  display: flex;
  flex-direction: column;
  row-gap: 12px;
  box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px,
    rgb(209, 213, 219) 0px 0px 0px 1px inset;
`;

export const useEditProfileForm = () => {
  const [editProfileFormValues, setEditProfileFormValues] =
    useState<EditProfileFormValues>({
      name: '',
      email: '',
      username: '',
      websiteUrl: '',
      location: '',
      bio: '',
    });

  const handleEditProfileFormValueChange =
    (key: keyof EditProfileFormValues) =>
    (e: ChangeEvent<HTMLInputElement>) => {
      setEditProfileFormValues((prevValues) => ({
        ...prevValues,
        [key]: e.target.value,
      }));
    };

  const labels = useMemo(
    () => ({
      nameLabel: 'name',
      emailLabel: 'email',
      usernameLabel: 'username',
      websiteUrlLabel: 'websiteUrl',
      locationLabel: 'location',
      bioLabel: 'bio',
    }),
    []
  );

  return {
    formTitle: 'Edit Profile',
    labels,
    handleEditProfileFormValueChange,
    editProfileFormValues,
    setEditProfileFormValues,
  };
};
Enter fullscreen mode Exit fullscreen mode

Page

import styled from '@emotion/styled/macro';
import {
  EditProfileForm,
  useEditProfileForm,
} from '../organisms/EditProfileForm';

export const EditProfileFormWithCustomHook = () => {
  const {
    formTitle,
    labels,
    editProfileFormValues,
    handleEditProfileFormValueChange,
  } = useEditProfileForm();

  return (
    <Page>
      <EditProfileForm
        formTitle={formTitle}
        {...labels}
        {...editProfileFormValues}
        onNameChange={handleEditProfileFormValueChange('name')}
        onEmailChange={handleEditProfileFormValueChange('email')}
        onUsernameChange={handleEditProfileFormValueChange('username')}
        onWebsiteUrlChange={handleEditProfileFormValueChange('websiteUrl')}
        onLocationChange={handleEditProfileFormValueChange('location')}
        onBioChange={handleEditProfileFormValueChange('bio')}
      />
    </Page>
  );
};

const Page = styled.div`
  padding: 16px;
`;
Enter fullscreen mode Exit fullscreen mode

Result

code in the page has been reduced and you can use the hook in other pages in the same way.


Conclusion

That's it.
Actually, there were no new methods. I just wanted to share my experience with you that how I dealt with the problem.
I hope it will be helpful for someone.

Happy Coding!


You can see the code in github and components in storybook.
here!

Github
StoryBook

Discussion (0)