Membangun Reusable Design System Dengan React
() translation by (you can also view the original English article)
React telah banyak melakukan penyederhanaan dalam proses pengembangan website. React's component-based arsitektur pada prinsipnya memudahkan untuk mendekomposisi dan menggunakan kembali kode. Namun, hal tersebut tidak selalu jelas bagi para pengembang tentang bagaimana membagikan komponen mereka pada seluruh proyek. Dalam postingan ini, saya akan menunjukkan beberapa cara untuk memperbaikinya.
React telah membuatnya lebih mudah untuk menulis kode yang indah dan ekspresif. Namun, tanpa pola yang jelas untuk penggunaan kembali komponen, kode menjadi berbeda dari waktu ke waktu dan menjadi sangat sulit untuk dipelihara. Saya telah melihat codebases dimana elemen UI yang sama memiliki sepuluh penerapan yang berbeda! Masalah lainnya adalah, lebih sering daripada tidak, para pengembang cenderung memasangkan UI dan fungsi bisnis terlalu ketat dan berusaha keras nantinya ketika UI berubah.
Hari ini, kita akan melihat bagaimana kita dapat membuat UI komponen yang dapat dibagikan dan bagaimana membangun bahasa desain yang konsisten diseluruh aplikasi Anda.
Permulaan
Anda perlu sebuah priyek react yang kosong untuk memulai. Cara tercepat untuk melakukan ini adalah dengan create-react-app, tetapi perlu upaya untuk menyiapkan Sass dengan ini. Saya telah membuat kerangka aplikasi, yang dapat Anda clone melalui GitHub. Anda juga dapat menemukan proyek akhir tutorial dalam repositori GitHub kami.
Untuk menjalankan, lakukan yarn-install
untuk menarik semua dependensi, lalu jalankan yarn start
untuk memunculkan aplikasi.
Semua komponen visual akan berada di bawah folder design_system system bersama dengan style yang sesuai. Setiap global style atau variabel akan berada di bawah src/styles.



Menyiapkan Desain Baseline
Kapan terakhir kali Anda melihat seperti hal yang buruk itu dari rekan design Anda, untuk mendapatkan suatu padding yang salah dengan setengah pixel, atau tidak dapat membedakan berbagai warna abu-abu? (Ada perbedaan antara #eee
dan #efefef
, saya diberitahu, dan saya bermaksud untuk menemukannya suatu hari nanti.)
Salah satu tujuan membangun UI library adalah untuk meningkatkan hubungan antara tim desain dan pengembang. Front-end developer telah berkoordinasi dengan API designer untuk sementara waktu dan untuk membangun kontrak API yang baik. Tapi untuk beberapa alasan, ini menghindari kita saat berkoordinasi dengan tim desain. Jika anda memikirikan itu, hanya ada beberapa jenis UI element yang ada. Jika kita ingin mendesain komponen Header misalnya, dapat berupa apa saja antara h1
dan h6
dapat dicetak tebal, dicetak miring, atau digaris bawahi. Harus mudah untuk melakukan pengkodean.
Sistem Grid
Langkah pertama sebelum memulai desain proyek adalah memahami bagaimana struktur grid. Pada kebanyakkan aplikasi, banyak yang tidak terduga. Hal ini menyebabkan sistem spasi (jarak) menjadi tersebar dan membuatnya sangat sulit bagi pengembang untuk mengukur sistem spasi yang akan digunakan. Jadi pilih sistemnya! Saya jatuh cinta dengan sistem grid 4px - 8px ketika saya pertama kali membacanya. Berpegang teguhlah pada hal itu telah membantu menyederhanakan banyak masalah dalam proses styling.
Mari kita mulai dengan menyiapkan sistem grid dasar dalam melalui kode. Kita akan mulai dengan app component yang menetapkan layout.
1 |
//src/App.js
|
2 |
|
3 |
import React, { Component } from 'react'; |
4 |
import logo from './logo.svg'; |
5 |
import './App.scss'; |
6 |
import { Flex, Page, Box, BoxStyle } from './design_system/layouts/Layouts'; |
7 |
|
8 |
class App extends Component { |
9 |
render() { |
10 |
return ( |
11 |
<div className="App"> |
12 |
<header className="App-header"> |
13 |
<img src={logo} className="App-logo" alt="logo" /> |
14 |
<h1 className="App-title">Build a design system with React</h1> |
15 |
</header> |
16 |
<Page> |
17 |
<Flex lastElRight={true}> |
18 |
<Box boxStyle={BoxStyle.doubleSpace} > |
19 |
A simple flexbox |
20 |
</Box> |
21 |
<Box boxStyle={BoxStyle.doubleSpace} >Middle</Box> |
22 |
<Box fullWidth={false}>and this goes to the right</Box> |
23 |
</Flex> |
24 |
</Page> |
25 |
</div> |
26 |
);
|
27 |
}
|
28 |
}
|
29 |
|
30 |
export default App; |
Selanjutnya, kita mendefinisikan sejumlah style dan wrapper components.
1 |
//design-system/layouts/Layout.js
|
2 |
import React from 'react'; |
3 |
import './layout.scss'; |
4 |
|
5 |
export const BoxBorderStyle = { |
6 |
default: 'ds-box-border--default', |
7 |
light: 'ds-box-border--light', |
8 |
thick: 'ds-box-border--thick', |
9 |
}
|
10 |
|
11 |
export const BoxStyle = { |
12 |
default: 'ds-box--default', |
13 |
doubleSpace: 'ds-box--double-space', |
14 |
noSpace: 'ds-box--no-space' |
15 |
}
|
16 |
|
17 |
export const Page = ({children, fullWidth=true}) => { |
18 |
const classNames = `ds-page ${fullWidth ? 'ds-page--fullwidth' : ''}`; |
19 |
return (<div className={classNames}> |
20 |
{children} |
21 |
</div>); |
22 |
|
23 |
};
|
24 |
|
25 |
export const Flex = ({ children, lastElRight}) => { |
26 |
const classNames = `flex ${lastElRight ? 'flex-align-right' : ''}`; |
27 |
return (<div className={classNames}> |
28 |
{children} |
29 |
</div>); |
30 |
};
|
31 |
|
32 |
export const Box = ({ |
33 |
children, borderStyle=BoxBorderStyle.default, boxStyle=BoxStyle.default, fullWidth=true}) => { |
34 |
const classNames = `ds-box ${borderStyle} ${boxStyle} ${fullWidth ? 'ds-box--fullwidth' : ''}` ; |
35 |
return (<div className={classNames}> |
36 |
{children} |
37 |
</div>); |
38 |
};
|
Akhirnya, kita akan mendefinisikan CSS sytle di SCSS.
1 |
/*design-system/layouts/layout.scss */
|
2 |
@import '../../styles/variables.scss'; |
3 |
$base-padding: $base-px * 2; |
4 |
|
5 |
.flex { |
6 |
display: flex; |
7 |
&.flex-align-right > div:last-child { |
8 |
margin-left: auto; |
9 |
}
|
10 |
}
|
11 |
|
12 |
.ds-page { |
13 |
border: 0px solid #333; |
14 |
border-left-width: 1px; |
15 |
border-right-width: 1px; |
16 |
&:not(.ds-page--fullwidth){
|
17 |
margin: 0 auto; |
18 |
max-width: 960px; |
19 |
}
|
20 |
&.ds-page--fullwidth { |
21 |
max-width: 100%; |
22 |
margin: 0 $base-px * 10; |
23 |
}
|
24 |
}
|
25 |
|
26 |
.ds-box { |
27 |
border-color: #f9f9f9; |
28 |
border-style: solid; |
29 |
text-align: left; |
30 |
&.ds-box--fullwidth { |
31 |
width: 100%; |
32 |
}
|
33 |
|
34 |
&.ds-box-border--light { |
35 |
border: 1px; |
36 |
}
|
37 |
&.ds-box-border--thick { |
38 |
border-width: $base-px; |
39 |
}
|
40 |
|
41 |
&.ds-box--default { |
42 |
padding: $base-padding; |
43 |
}
|
44 |
|
45 |
&.ds-box--double-space { |
46 |
padding: $base-padding * 2; |
47 |
}
|
48 |
|
49 |
&.ds-box--default--no-space { |
50 |
padding: 0; |
51 |
}
|
52 |
}
|
Ada banyak hal yang perlu dibuka di sini. Mari kita mulai dari bawah. variables.scss adalah tempat kita mendefinisikan variabel global kita seperti warna dan mempersiapkan grid. Karena kita menggunakan grid 4px-8px, dasar kita adalah 4px. Komponen induknya adalah Page
, dan ini mengatur flow dari halaman. Kemudian level terendah elemen adalah Box
, yang menentukan bagaimana konten ditampilkan dalam suatu halaman. Ini hanya div
yang tahu bagaimana membuat dirinya secara kontekstual.
Sekarang, kita membutuhkan komponen Container
yang merekatkan beberapa div
. Kita telah memilih flex-box
, maka secara kreatif bernama komponen Flex
.
Mendefinisikan Tipe dari Sistem
Tipe dari sistem merupakan komponen penting dari aplikasi apa pun. Biasanya, kita mendefinisikan sesuatu berdasarkan dari global style dan merubahnya sesuai dengan saat kita membutuhkannya. Ini sering mengarah pada inkonsistensi dalam desain. Mari kita lihat bagaimana hal ini dapat dengan mudah dipecahkan dengan menambahkan ke desain library.
Pertama, kita akan mendefinisikan beberapa konstanta style dan class wrapper.
1 |
// design-system/type/Type.js
|
2 |
import React, { Component } from 'react'; |
3 |
import './type.scss'; |
4 |
|
5 |
export const TextSize = { |
6 |
default: 'ds-text-size--default', |
7 |
sm: 'ds-text-size--sm', |
8 |
lg: 'ds-text-size--lg' |
9 |
};
|
10 |
|
11 |
export const TextBold = { |
12 |
default: 'ds-text--default', |
13 |
semibold: 'ds-text--semibold', |
14 |
bold: 'ds-text--bold' |
15 |
};
|
16 |
|
17 |
export const Type = ({tag='span', size=TextSize.default, boldness=TextBold.default, children}) => { |
18 |
const Tag = `${tag}`; |
19 |
const classNames = `ds-text ${size} ${boldness}`; |
20 |
return <Tag className={classNames}> |
21 |
{children} |
22 |
</Tag> |
23 |
};
|
Selanjutnya, kita akan mendefinisikan CSS style yang akan digunakan untuk elemen teks.
1 |
/* design-system/type/type.scss*/
|
2 |
|
3 |
@import '../../styles/variables.scss'; |
4 |
$base-font: $base-px * 4; |
5 |
|
6 |
.ds-text { |
7 |
line-height: 1.8em; |
8 |
|
9 |
&.ds-text-size--default { |
10 |
font-size: $base-font; |
11 |
}
|
12 |
&.ds-text-size--sm { |
13 |
font-size: $base-font - $base-px; |
14 |
}
|
15 |
&.ds-text-size--lg { |
16 |
font-size: $base-font + $base-px; |
17 |
}
|
18 |
&strong, &.ds-text--semibold { |
19 |
font-weight: 600; |
20 |
}
|
21 |
&.ds-text--bold { |
22 |
font-weight: 700; |
23 |
}
|
24 |
}
|
Ini adalah komponen Text
sederhana yang mewakili beberapa jenis teks UI yang ada. Kita dapat memperluas ini lebih lanjut untuk menangani interaksi kecil seperti render tooltip ketika teks terpotong, atau render nugget yang berbeda untuk kasus-kasus tertentu seperti email, waktu, dan lain-lain.
Atom Form Molekul
Sejauh ini, kita hanya membangun elemen paling dasar yang terdapat pada aplikasi web, dan mereka tidak ada gunanya sendiri. Mari kita memperluas contoh ini dengan membangun modal window sederhana.
Pertama, kita mendefinisikan komponent class untuk modal window.
1 |
// design-system/Portal.js
|
2 |
import React, {Component} from 'react'; |
3 |
import ReactDOM from 'react-dom'; |
4 |
import {Box, Flex} from './layouts/Layouts'; |
5 |
import { Type, TextSize, TextAlign} from './type/Type'; |
6 |
import './portal.scss'; |
7 |
|
8 |
export class Portal extends React.Component { |
9 |
constructor(props) { |
10 |
super(props); |
11 |
this.el = document.createElement('div'); |
12 |
}
|
13 |
|
14 |
componentDidMount() { |
15 |
this.props.root.appendChild(this.el); |
16 |
}
|
17 |
|
18 |
componentWillUnmount() { |
19 |
this.props.root.removeChild(this.el); |
20 |
}
|
21 |
|
22 |
render() { |
23 |
return ReactDOM.createPortal( |
24 |
this.props.children, |
25 |
this.el, |
26 |
);
|
27 |
}
|
28 |
}
|
29 |
|
30 |
|
31 |
export const Modal = ({ children, root, closeModal, header}) => { |
32 |
return <Portal root={root} className="ds-modal"> |
33 |
<div className="modal-wrapper"> |
34 |
<Box> |
35 |
<Type tagName="h6" size={TextSize.lg}>{header}</Type> |
36 |
<Type className="close" onClick={closeModal} align={TextAlign.right}>x</Type> |
37 |
</Box> |
38 |
<Box> |
39 |
{children} |
40 |
</Box> |
41 |
</div> |
42 |
</Portal> |
43 |
}
|
Selanjutnya, kita dapat menentukan CSS style untuk modal.
1 |
#modal-root { |
2 |
.modal-wrapper { |
3 |
background-color: white; |
4 |
border-radius: 10px; |
5 |
max-height: calc(100% - 100px); |
6 |
max-width: 560px; |
7 |
width: 100%; |
8 |
top: 35%; |
9 |
left: 35%; |
10 |
right: auto; |
11 |
bottom: auto; |
12 |
z-index: 990; |
13 |
position: absolute; |
14 |
}
|
15 |
> div { |
16 |
background-color: transparentize(black, .5); |
17 |
position: absolute; |
18 |
z-index: 980; |
19 |
top: 0; |
20 |
right: 0; |
21 |
left: 0; |
22 |
bottom: 0; |
23 |
}
|
24 |
.close { |
25 |
cursor: pointer; |
26 |
}
|
27 |
}
|
Untuk yang tidak tahu, createPortal
sangat mirip dengan method render
, kecuali itu akan melakukan render pada suatu bagian ke dalam node yang ada diluar dari hirarki DOM dari komponen utama. Itu diperkenalkan di React 16.
Menggunakan Komponen Modal
Sekarang komponen didefinisikan, mari kita lihat bagaimana kita dapat menggunakannya dalam konteks bisnis.
1 |
//src/App.js
|
2 |
|
3 |
import React, { Component } from 'react'; |
4 |
//...
|
5 |
import { Type, TextBold, TextSize } from './design_system/type/Type'; |
6 |
import { Modal } from './design_system/Portal'; |
7 |
|
8 |
class App extends Component { |
9 |
constructor() { |
10 |
super(); |
11 |
this.state = {showModal: false} |
12 |
}
|
13 |
|
14 |
toggleModal() { |
15 |
this.setState({ showModal: !this.state.showModal }); |
16 |
}
|
17 |
|
18 |
render() { |
19 |
|
20 |
//...
|
21 |
<button onClick={this.toggleModal.bind(this)}> |
22 |
Show Alert |
23 |
</button> |
24 |
{this.state.showModal && |
25 |
<Modal root={document.getElementById("modal-root")} header="Test Modal" closeModal={this.toggleModal.bind(this)}> |
26 |
Test rendering |
27 |
</Modal>} |
28 |
//....
|
29 |
}
|
30 |
}
|
Kita dapat menggunakan modal itu di mana saja dan mempertahankan pada suatu pemanggil. Sederhana, kan? Tapi ada bug di sini. Tombol tutup tidak berfungsi. Itu karena kita telah membangun semua komponen sebagai sistem yang tertutup. Itu hanya menempati props yang dibutuhkan dan mengabaikan sisanya. Dalam konteks ini, komponen text mengabaikan onClick
dari event handler. Untungnya, ini adalah perbaikan yang mudah.
1 |
// In design-system/type/Type.js
|
2 |
|
3 |
export const Type = ({ tag = 'span', size= TextSize.default, boldness = TextBold.default, children, className='', align=TextAlign.default, ...rest}) => { |
4 |
const Tag = `${tag}`; |
5 |
const classNames = `ds-text ${size} ${boldness} ${align} ${className}`; |
6 |
return <Tag className={classNames} {...rest}> |
7 |
{children} |
8 |
</Tag> |
9 |
};
|
ES6 memiliki cara praktis untuk mengekstrak parameter yang tersisa sebagai array. Hanya menerapkan itu dan menyebarkannya ke komponen.
Membuat komponen dapat ditemukan
Saat tim Anda mempertimbangkan, sulit untuk menyinkronkan semua orang tentang komponen yang tersedia. Storybooks adalah cara yang bagus untuk membuat komponen Anda dapat ditemukan. Mari menyiapkan komponen dasar Storybooks.
Untuk memulai, jalankan:
1 |
npm i -g @storybook/cli
|
2 |
|
3 |
getstorybook |
Ini mengatur konfigurasi yang diperlukan untuk storybook. Dari sini, sangat mudah untuk melakukan sisa pengaturan. Mari tambahkan simple story untuk memperlihatkan jenis yang berbeda dari Type
.
1 |
import React from 'react'; |
2 |
import { storiesOf } from '@storybook/react'; |
3 |
|
4 |
import { Type, TextSize, TextBold } from '../design_system/type/Type.js'; |
5 |
|
6 |
|
7 |
storiesOf('Type', module) |
8 |
.add('default text', () => ( |
9 |
<Type> |
10 |
Lorem ipsum |
11 |
</Type> |
12 |
)).add('bold text', () => ( |
13 |
<Type boldness={TextBold.semibold}> |
14 |
Lorem ipsum |
15 |
</Type> |
16 |
)).add('header text', () => ( |
17 |
<Type size={TextSize.lg}> |
18 |
Lorem ipsum |
19 |
</Type> |
20 |
));
|
21 |
|
22 |
Dasar dari API itu sederhana. storiesOf
mendefinisikan story baru, khususnya komponen Anda. Anda kemudian dapat membuat bab baru dengan add
, untuk menampilkan perbedaan status pada komponen ini.



Tentu saja, ini cukup mendasar, tetapi storybooks memiliki beberapa add-on yang akan membantu Anda menambahkan fungsionalitas ke dokumen Anda. Dan apakah saya menyebutkan bahwa mereka memiliki dukungan emoji? 😲�