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
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;
`;
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;
`;
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!
Top comments (0)