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 …


This content originally appeared on DEV Community and was authored by SeongKuk Han

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

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

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

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

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

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

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

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

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

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

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

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

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


This content originally appeared on DEV Community and was authored by SeongKuk Han


Print Share Comment Cite Upload Translate Updates
APA

SeongKuk Han | Sciencx (2022-06-03T00:22:27+00:00) How I dealt with Props Drilling in Atomic Design. Retrieved from https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/

MLA
" » How I dealt with Props Drilling in Atomic Design." SeongKuk Han | Sciencx - Friday June 3, 2022, https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/
HARVARD
SeongKuk Han | Sciencx Friday June 3, 2022 » How I dealt with Props Drilling in Atomic Design., viewed ,<https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/>
VANCOUVER
SeongKuk Han | Sciencx - » How I dealt with Props Drilling in Atomic Design. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/
CHICAGO
" » How I dealt with Props Drilling in Atomic Design." SeongKuk Han | Sciencx - Accessed . https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/
IEEE
" » How I dealt with Props Drilling in Atomic Design." SeongKuk Han | Sciencx [Online]. Available: https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/. [Accessed: ]
rf:citation
» How I dealt with Props Drilling in Atomic Design | SeongKuk Han | Sciencx | https://www.scien.cx/2022/06/03/how-i-dealt-with-props-drilling-in-atomic-design/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.