The 100% correct way to structure a React app (or why there’s no such thing)

Khi bạn phải xậy dựng một React Application, tạo được một code structure tốt sẽ hữu ích khá nhiều cho quá trình phát triển sản phẩm.

Trong bài viết này, tôi sẽ nói cho bạn biết cách tôi sẽ xậy dựng structure cho một trong các React app của tôi, và điều gì quyết định việc đó. Cùng với đó sẽ là một vài quan điểm mà tôi đã không áp dụng bởi nó không thích hợp, nhưng biết đâu lại có ích cho bạn thì sao.

Điều Gì Phù Hợp Với Bạn

Hãy tưởng tượng một app có duy nhất một file chứa tất cả các components, reducers, store, utilities, ... Một ý tưởng tệ hại, tất nhiên. Nhưng tệ hại chỗ nào mới được chứ?

Bạn có dừng lại và suy ngẫm tại sao không. Hay là bạn chỉ nghe theo quan điểm của tôi và cho rằng đó là một ý tưởng tệ hại. Vấn để của việc gom tất cả vào một file là thật khó cho việc tìm kiếm.

Nhưng nếu chúng ta có các "Bookmarks" cho mỗi đoạn code thì sao? có thể là mỗi bookmarks cho một function chẳng hạn. Đồng thời chúng ta có thể thu gọn các bookmarks lại cho dễ nhìn nữa. Và có một bảng các bookmarks của file.

Điều này nghe có vẻ điên rồ. Nhưng thực chất khi bạn tạo ra các "files", hay một file structure, chính là để dễ dàng navigate trong code của bạn hơn, phải không. Các "files" đó không gì hơn chính là các bookmarks mà cuối cùng cũng trở thành một đoạn code JS.

Và đó cũng là nguyên nhân tại sao bạn không bao giờ có được một câu trả lời thỏa đang cho câu hỏi: 'đâu là cách tốt nhất để cấu trúc app của bạn'. Bởi điều đó phụ thuộc rất nhiều vào sở thích và thói quen navigate (tìm kiếm, đánh dấu) của bạn.

Để trả lời cho câu hỏi "Đâu là cách tốt nhất để cấu trúc app của mình", tôi có liệt kê các hoạt động liên quan đến các file (phục vụ cho quá trình code) của mình:

  • Create a new component (Tạo một component mới)
  • Import one module into another (Import một module)
  • Jump to source (Tìm kiếm một file chứa một component hoặc một đoạn code nào đó)
  • Open a known file (Mở file)
  • Browse for a file I don’t know the name of (Xem lướt qua các file (ở đây là do tôi không biết file mà mình muốn tìm tên là gì), tôi muốn xem lướt qua các folder để tìm nó )
  • Change tab to another open file (Chuyển giữa các file đang được mở. (navigate giữa các opened file))

Tiếp theo, tôi thống kê xem tần suất tôi thực thiện các hoạt động trên:

Directory Structure

Đây là cấu trúc thu mục mà tôi lập ra:

<HeaderNav> sẽ chỉ bị import bên trong component <Header>, vì vậy nó nằm ngay trong folder Header. Một <Button> có thể bị gọi ở bất cứ đâu, vì thế nó được đặt ở ở mức cao nhất trong folder components.

Quy tắc đó khá tốt, nhưng việc tuân thủ chặt chẽ một quy tắc như vậy nhiều khi gây phiền toái cho chúng ta. Về mặt kĩ thuật thì mọi thứ nằm trong App và trong page cũng tương tự vậy. Nhưng tôi không thể hiện điều đó ở đây bởi tôi không muốn.

Ngoài ra thì còn một vấn đề nữa, đó là các "container". Có 2 cách giải quyết vấn đề này:

  1. Tôi để nó ra ngoài folder Header, và wrap tất cả trong 1 folder là HeaderContainer

  2. Cách 2 là tôi ném nó trực tiếp vào folder Header, coi nó như 1 component bình thường.

Tôi thấy cả 2 đều ổn, nhưng thường thì tôi sử dụng cách đầu tiên.

Self-contained Components

My rule: nếu tôi có môt project với rất nhiều component, tôi sẽ đặt mỗi component đi kèm với một file CSS và 1 file test.

File Naming

Một quy tắc mà tôi thấy rất hữu ích đó là:

đặt tên file giống với cái mà bạn định export từ file đó

Đừng làm những điều như này: Để rồi đến khi mở các file đó ra: Bạn có thể tiết kiệm vài kí tự khi import, nhưng sẽ thật khó chịu cho những ai (bao gồm cả bạn) mở 1 đống file index ra mà chẳng biết file nào với file nào (và bao gồm cả search nữa). Nhưng để tôi làm cho rõ điều này ... Tôi import 1 module vào module khác, trung bình, 18 lần 1 tuần. Nhưng tôi mở một file bằng cách gõ tên của nó ... 840 lần 1 tuần. Và tôi tìm tên của file đấy trên phần tab bar của editor 1892 lần 1 tuần. Vì vậy hãy thêm vài kí tự vào phần import giúp tôi, cám ơn. Còn nếu bạn vẫn muốn dòng import module của bạn ngắn đi, mà không gây phiền hà khi phải phân biệt một đống file index.js. Thì bạn có thể làm như sau, thêm 1 file index.js vào mỗi folder component.

.js vs .jsx extensions

Trước đây thì tôi luôn dùng .jsx cho các file chứa JSX và sử dụng .js cho vanilla javascript. Điều này giúp tôi phân biệt rõ ràng khi mở/xem một file, thêm nữa, github sẽ highlight syntax cho JSX. Tuy vậy gần đây thì Facebook có khuyên không nên dùng .jsx extension. Vì vậy tôi đã chuyển hết về .js, và thú thực là cũng không có nhiều khác biệt cho lắm.

Index files for utils

Tôi thường tạo một file index.js cho các hàm utils của mình, như thế này: để tôi có thể import chúng như sau:

import {
  formatDate,
  getAtPath,
  toNumber,
  toString,
} from '../../../../utils';

Gọn gàng đúng không. Bất cứ khi nào tôi thêm một file util, tôi chỉ việc tạo file và tạo một entry ở file index.js. Nhưng dẫn dần thì tôi nhận ra, thực chất việc viết nó clear như sau lại tốt hơn:

import formatDate from '../../../../utils/formatDate';
import getAtPath from '../../../../utils/getAtPath';
import toNumber from '../../../../utils/toNumber';
import toString from '../../../../utils/toString';

Ít dòng code hơn, ít hơn 1 file phải thêm, và đỡ phải giải thích cho các new dev. Nhưng thực sự việc viết lặp đi lặp lại thế này làm tôi khó chịu. Vì thế hãy cùng đi vào 1 vài solutions sau: Solution 1 là sử dụng Webpack's alias resolving để tham chiếu tới thư mục utils mà không cần đường dẫn tương đối. Tôi đã map Utils với src/app/utils, và kết quả quá tốt:

Đây là config mà tôi đã sử dụng:

/*  --  webpack.config.shared.js  --  */
export const sharedConfig = {
  alias: {
    'Utils': path.resolve(__dirname, '../src/app/utils/'),
    'Components': path.resolve(__dirname, '../src/app/components/'),
  },
};
  

/*  --  webpack.config.dev.js  --  */
import { sharedConfig } from './webpack.config.shared.js';

const config = {
  // development config
  resolve: {
    alias: sharedConfig.alias,
  },
};


/*  --  webpack.config.prod.js  --  */
import { sharedConfig } from './webpack.config.shared.js';

const config = {
  // production config
  resolve: {
    alias: sharedConfig.alias,
  },
};


/*  --  SomeComponent.js  --  */
import toNumber from 'Utils/toNumber';
import toString from 'Utils/toString';

Solution 2 là ... kệ nó, tôi sẽ tự thôi miên bản thân rằng mấy cái chấm và gạch chéo ấy sẽ chẳng thể làm tôi bận lòng được.

Index files for components

Một cách thức (thực ra đã được trình bày ở trên) khác trong cuộc chiến chống lại mấy cái chấm và gạch ngược. Bạn có mấy chục cái component phải import vào một module và phải gõ 500 cái chấm. Đây là giải pháp:

import React from 'react';
import {
  Button,
  Footer,
  Header,
  Page,
} from 'Components';

Setup webpack config:

const config = {
  // other stuff
  resolve: {
    alias: {
      'Components': path.resolve(__dirname, '../src/app/components/'),
    },
  },
};

Thêm 1 file index.js trong folder chứa các components:

Summary

Thú thực là tôi luôn gặp khó khăn với phần tóm tắt. Mà tôi vừa viết hẳn một bài dài như thế này rồi, bạn còn mong gì ở tôi nữa. Cám ơn vì đã đọc

Nguồn

https://hackernoon.com/the-100-correct-way-to-structure-a-react-app-or-why-theres-no-such-thing-3ede534ef1ed


All Rights Reserved