How I dealt with Props Drilling in Atomic Design
Published: 2022-06-03 00:22:27

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](https://atomicdesign.bradfrost.com/).

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3q76c7jihcekid4veub.png)
 
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**

```typescript
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;
  }
`;
```


**PostList**

```typescript
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;
`;
```

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

**Page**

```typescript
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;
`;
```

![preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a0m7ii5wggue6qm9k1gw.png)

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

**PostListContainer**

```typescript
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} />;
};
```

**Page**

```typescript
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;
`;
```

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**

```typescript
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;
  }
`;
```

**Label**

```typescript
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;
`;
```

**TextFieldWithLabel**

```typescript
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;
`;
```

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

**EditProfileForm**

```typescript
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;
`;
```

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

**Page**

```typescript
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;
`;
```

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**

```typescript
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,
  };
};
```

**Page**

```typescript
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;
`;
```

![Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rg28ovs1vrixnvtokkp3.png)

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](https://github.com/hsk-kr/atomic-design-props-drilling)
[StoryBook](https://hsk-kr.github.io/atomic-design-props-drilling/)