Cấu trúc thư mục và cách viết component chuẩn trong React

Một dự án web app đơn giản nếu muốn đạt mức độ thành phẩm thì sẽ có từ 20-30 components, và theo tiêu chí component càng nhỏ càng tốt vì nó có tính tái sử dụng cao, với cấu trúc thư mục bên dưới webapp có thể chứa khoảng 200-300 components. Đối với mức 20,000 React components thì phải hỏi chính đội ngũ của Facebook mới được (trích dẫn “the Facebook codebase has over 20,000 React components, and that’s not even counting React Native”)

CẤU TRÚC THƯ MỤC CỦA MỘT DỰ ÁN REACT

/actions/...
/components/common/Link.js
/components/common/...
/components/forms/TextBox.js
/components/forms/TextBox.container.js /* Container component */
/components/forms/TextBox.res/style.css
/components/forms/TextBox.locale/vi-VN/...
/components/forms/...
/components/layout/App.js - The top-level React component
/components/layout/App.res/style.css
/components/layout/App.locale/en-US/...
/components/layout/Navigation.js
/components/layout/Navigation.res/style.css
/components/layout/Navigation.res/data.json
/components/layout/Navigation.locale/en-US/...
/components/layout/...
/components/pages/Home.js
/components/pages/Home.css
/components/pages/Account/index.js
/components/pages/Account/index.css
/components/pages/...
/core/...
/constants/...
/helpers/...
/reducers/...
/stores/...

Về Component có nhận dữ liệu từ máy chủ hoặc từ store và nhận dữ liệu từ thao tác người dùng, chia làm 2 component với 2 chức năng riêng biệt:

  • Container: nhận dữ liệu từ server, component này không render và cũng không nhận bất kỳ thao tác nào của người dùng
  • Presentation: chỉ đảm nhiệm việc render, chỉ nhận dữ liệu thông qua props, không có state

Cách tiếp cận này (Container component và Presentation) được Dan Abramov (tác giả Redux và đang là thành viên của React) giới thiệu qua bài viết: Smart and Dump components, và cấu trúc thư mục thì dựa theo Gist này.

Sử dụng component stateful và stateless độc lập như trên để dễ quản lý luồng dữ liệu. Trong cấu trúc trên có sử dụng Redux, tuy nhiên chỉ để tham khảo là chính, nếu bạn có sử dụng Redux thì có thêm các thư mục reducers, stores, actions. Còn thư mục core thì có thể chứa business logic, helpers dùng để chứa các hàm hỗ trợ, constants để chứa các biến hằng số.

Thật ra, nếu bạn không sử dụng Redux, có thể chia làm 3 components – đã áp dụng thấy rất ổn:

  • Container: như trên
  • Presentation: như trên
  • Interactive: nhận các thao tác từ người dùng, component này sẽ khai báo các hàm handleClick, handleChange,…và truyền cho presentation component thông qua props.

Với cách tổ chức 3 components như trên thì ưu điểm là phân rõ rạch ròi nhiệm vụ cho từng component: lấy dữ liệu, xuất dữ liệu và tương tác người dùng. Khuyết điểm của cách tiếp cận này là dữ liệu có thể bị trùng lặp ở component Interactive, vì bản thân nó không sử dụng mà truyền xuống cho Presentation, tuy nhiên đây không phải quá tệ để cho cách tổ chức component dễ hiểu và quản lý luồng dữ liệu, cách này được xem là tương tự actions -> store <=> reduce.

Trong thư mục chính components, được chia làm các thư mục con, các thư mục con này cũng là các react component, tuy nhiên được phân loại theo từng cấp độ lớn nhỏ: pages chứa các component theo phân loại chức năng trang, trong một trang (page component) chứa rất nhiều component nhỏ khác và được bọc trong layout component (layouts)

CÁCH VIẾT MỘT COMPONENT REACT ĐÚNG CHUẨN AIRBNB

Quy tắc cơ bản

  • Chỉ bao gồm một React component cho mỗi file. Tuy nhiên, cho phép nhiều Stateless, hoặc Pure, Components cho mỗi file.
  • Luôn luôn sử dụng cú pháp JSX. Không sử dụng React.createElement trừ khi bạn đang khởi tạo ứng dụng từ file không phải là JSX.

Class vs React.createClass vs stateless Nếu bạn có internal state và/hoặc refs, prefer class extends React.Component bỏ qua React.createClass.

// bad
const Listing = React.createClass({
  // ...
  render() {
    return <div>{this.state.hello}</div>;
  }
});

// good
class Listing extends React.Component {
  // ...
  render() {
    return <div>{this.state.hello}</div>;
  }
}

Và nếu bạn không có state hoặc refs, prefer các function thông thường bỏ qua các class:

// bad
class Listing extends React.Component {
  render() {
    return <div>{this.props.hello}</div>;
  }
}

// bad (relying on function name inference is discouraged)
const Listing = ({ hello }) => (
  <div>{hello}</div>
);

// good
function Listing({ hello }) {
  return <div>{hello}</div>;
}

Mixins Không sử dụng mixins. Tại sao? Mixins nhét vào các phụ thuộc ngầm, gây xung đột tên và gây ra sự phức tạp. Hầu hết các trường hợp sử dụng mixins có thể được thực hiện theo những cách tốt hơn thông qua các component, các component bậc cao, hoặc các utility module. Đặt tên

  • Extensions: Sử dụng phần mở rộng .jsx cho React components.
  • Filename: Sử dụng PascalCase cho tên file. Ví dụ: ReservationCard.jsx.
  • Reference Naming: Sử dụng PascalCase cho React components và camelCase cho các instances của chúng.
  • Đặt tên Component: Sử dụng filename làm tên component. Ví dụ: ReservationCard.jsx nên có tên tham chiếu của ReservationCard. Tuy nhiên, đối với các component gốc của một thư mục, hãy sử dụng index.jsx làm filename và sử dụng tên thư mục làm tên component
  • Đặt tên Component bậc cao: Sử dụng một hỗn hợp của tên của component bậc cao và tên của component được truyền như displayName trên component được tạo ra. Ví dụ, component bậc cao withFoo(), khi passed một component Bar sẽ tạo ra một component với displayName của withFoo(Bar). Tại sao? Tên hiển thị của component có thể được sử dụng bởi các công cụ phát triển hoặc trong các thông báo lỗi, và có một giá trị thể hiện rõ ràng mối quan hệ này sẽ giúp mọi người hiểu điều gì đang xảy ra.
// bad
export default function withFoo(WrappedComponent) {
  return function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }
}

// good
export default function withFoo(WrappedComponent) {
  function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }

  const wrappedComponentName = WrappedComponent.displayName
    || WrappedComponent.name
    || 'Component';

  WithFoo.displayName = `withFoo(${wrappedComponentName})`;
  return WithFoo;
}
  • Props Naming: Tránh sử dụng tên thuộc tính DOM component cho các mục đích khác. Tại sao? Mọi người mong đợi props như style và className có ý nghĩa là một điều cụ thể. Thay đổi API này cho một tập hợp con của ứng dụng làm cho code khó đọc hơn và khó bảo trì hơn, và có thể gây ra lỗi.
// bad
<MyComponent style="fancy" />
// good
<MyComponent variant="fancy" />

Declaration Không sử dụng displayName để đặt tên các component. Thay vào đó, hãy đặt tên cho component bằng cách tham khảo.

// bad
export default React.createClass({
  displayName: 'ReservationCard',
  // stuff goes here
});

// good
export default class ReservationCard extends React.Component {
}

Alignment Làm theo các alignment styles này cho cú pháp JSX.

// bad
<Foo superLongParam="bar"
     anotherSuperLongParam="baz" />

// good
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
/>

// if props fit in one line then keep it on the same line
<Foo bar="bar" />

// children get indented normally
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
>
  <Quux />
</Foo>

Quotes Luôn luôn sử dụng dấu ngoặc kép ("") cho các thuộc tính JSX, nhưng dấu nháy đơn (') cho tất cả các JS khác. Các thuộc tính HTML thông thường thường sử dụng dấu ngoặc kép thay vì đơn, vì vậy các thuộc tính JSX phản chiếu quy ước này. Spacing Luôn luôn bao gồm một dấu cách duy nhất trong thẻ tự đóng. Không chèn dấu cách vào ngoặc nhọn. Props

  • Luôn luôn sử dụng camelCase cho tên prop.
  • Bỏ giá trị của prop khi nó rõ ràng true.
  • Luôn luôn bao gồm một thẻ alt trên thẻ <img>. Nếu hình ảnh là presentational, alt có thể là một chuỗi rỗng hoặc <img> phải có role="presentation".
  • Không sử dụng các từ như "image", "photo", hoặc "picture" trong thuộc tính alt của <img>. Trình đọc màn hình đã thông báo các yếu tố img dưới dạng hình ảnh, vì vậy không cần bao gồm thông tin này trong văn bản alt.
  • Chỉ sử dụng các ARIA role không trừu tượng và hợp lệ.
  • Không sử dụng accessKey trong các phần tử.
  • Tránh sử dụng một chỉ số của mảng như là thuộc tính khóa.
  • Luôn xác định defaultProps rõ ràng cho tất cả các prop không required.
// bad
function SFC({ foo, bar, children }) {
  return <div>{foo}{bar}{children}</div>;
}
SFC.propTypes = {
  foo: PropTypes.number.isRequired,
  bar: PropTypes.string,
  children: PropTypes.node,
};

// good
function SFC({ foo, bar, children }) {
  return <div>{foo}{bar}{children}</div>;
}
SFC.propTypes = {
  foo: PropTypes.number.isRequired,
  bar: PropTypes.string,
  children: PropTypes.node,
};
SFC.defaultProps = {
  bar: '',
  children: null,
};

Methods

  • Sử dụng arrow functions để đóng các biến cục bộ.
function ItemList(props) {
  return (
    <ul>
      {props.items.map((item, index) => (
        <Item
          key={item.key}
          onClick={() => doSomethingWith(item.name, index)}
        />
      ))}
    </ul>
  );
}
  • Không sử dụng tiền tố gạch dưới cho các methods nội bộ của một React component. Tại sao? Các tiền tố gạch dưới đôi khi được sử dụng như một quy ước trong các ngôn ngữ khác để biểu thị sự riêng tư. Tuy nhiên, không giống như những ngôn ngữ đó, không có sự hỗ trợ riêng cho tính riêng tư trong JavaScript, mọi thứ đều công khai. Bất kể ý định của bạn, việc thêm tiền tố gạch dưới vào thuộc tính của bạn không thực sự làm cho chúng trở nên riêng tư, và bất kỳ thuộc tính nào (tiền tố dấu gạch dưới hoặc không) phải được coi là công khai.
React.createClass({
  _onClickSubmit() {
    // do stuff
  },

  // other stuff
});

// good
class extends React.Component {
  onClickSubmit() {
    // do stuff
  }

  // other stuff
}
  • Đảm bảo trả lại giá trị trong phương thức render của bạn
// bad
render() {
  (<div />);
}

// good
render() {
  return (<div />);
}

Tham khảo tại https://github.com/airbnb/javascript/tree/master/react