Cấu trúc thư mục của một dự án React đúng chuẩn www.Airbnb.com

Cấu trúc thư mục của một dự án React đúng chuẩn www.Airbnb.com

Cách viết component chuẩn trong React. Mục tiêu là làm sao cho dự án dễ quản lý, cũng như dễ hiểu để cộng tác làm việc với nhau, cũng như tăng tính tái sử dụng của một component React, việc áp dụng theo hay không thì tùy bạn – tất nhiên, nhưng nếu bạn không sử dụng một quy luật nào đó thống nhất, một ngày không xa bạn sẽ cảm thấy không quản lý hết được dự án của mình khi ngày nó càng phình ra.

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, chứ mình thì chịu! (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, mình 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.

Mình 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 reducersstores, 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

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

Học theo những kinh nghiệm của các đàn anh là cách tiếp cận thông minh nhất :), bạn hãy đọc sơ lược, nếu chưa nhớ rõ áp dụng hết những thứ trong đó thì cứ làm theo từng cái thôi. Nhưng tốt hơn hết là đọc 1 lượt kỹ càng, sau đó viết 2-3 components, cái này quên quen bay qua coi lại, chừng 5-6 components là bạn sẽ quen cách tổ chức và viết component đúng chuẩn rồi.

Hãy xây dựng component từng dòng một.

Nạp CSS

import React, { Component } from 'react' import { observer } from 'mobx-react' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css'

import React, { Component } from 'react'

import { observer } from 'mobx-react'

import ExpandableForm from './ExpandableForm'

import './styles/ProfileContainer.css'

Về mặt lý thuyết thì tôi thích đặt CSS trong JavaScript. Nhưng nó vẫn là một ý tưởng mới. Trước khi ý tưởng đó hoàn thiện, chúng ta vẫn nhập một file CSS vào mỗi component.

Chúng tôi cũng tách dependency imports từ local imports bằng một dòng mới.

Khởi tạo State

Bạn cũng có thể sử dụng cách tiếp cận cũ hơn để khởi tạo state trong constructor. Thêm vào đó ở đây. Chúng tôi thích cách gọn gàng hơn.

import React, { Component } from 'react'

import { observer } from 'mobx-react'

import ExpandableForm from './ExpandableForm'

import './styles/ProfileContainer.css'

<strong class="markup--strong markup--pre-strong">export default class ProfileContainer extends Component {

  state = { expanded: false }</strong>

Chúng tôi cũng đảm bảo export class mặc định.

propTypes và defaultProps

import React, { Component } from 'react'

import { observer } from 'mobx-react'

import { string, object } from 'prop-types'

import ExpandableForm from './ExpandableForm'

import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {

  state = { expanded: false }


  static propTypes = {

    model: object.isRequired,

    title: string

  }


  static defaultProps = {

    model: {

      id: 0

    },

    title: 'Your Name'

  }

PropTypes và defaultProps là các thuộc tính tĩnh, được khai báo càng cao càng tốt trong component code. Chúng phải được hiển thị ngay cho các dev khác đọc file vì chúng được dùng làm tài liệu.

Nếu sử dụng React 15.3.0 hoặc cao hơn, hãy sử dụng gói prop-types thay vì React.PropTypes bởi cấu trúc độc đáo (dĩ nhiên).

Tất cả các component của bạn nên có propTypes.

Methods

Với các class component, khi bạn chuyển các method thành các tiểu hợp phần, bạn phải đảm bảo rằng chúng có quyền này khi chúng được gọi. Điều này thường đạt được bằng cách truyền this.handleSubmit.bind (this) vào tiểu hợp phần.

import React, { Component } from 'react'

import { observer } from 'mobx-react'

import { string, object } from 'prop-types'

import ExpandableForm from './ExpandableForm'

import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {

  state = { expanded: false }


  static propTypes = {

    model: object.isRequired,

    title: string

  }


  static defaultProps = {

    model: {

      id: 0

    },

    title: 'Your Name'

  }

  handleSubmit = (e) => {

    e.preventDefault()

    this.props.model.save()

  }  

  handleNameChange = (e) => {

    this.props.model.changeName(e.target.value)

  }  

  handleExpand = (e) => {

    e.preventDefault()

    this.setState({ expanded: !this.state.expanded })

  }

Chúng tôi nghĩ rằng cách tiếp cận này gọn và dễ dàng hơn mà vẫn giúp sửa chữa context tự động thông qua chức năng arrow của ES6.

Bỏ qua một hàm trong setState 

Trong ví dụ này, chúng tôi thực hiện việc này:

this.setState({ expanded: !this.state.expanded })

Đây là bí mật về setState, nó thực sự không đồng bộ. React thay đổi state vì các lý do hiệu suất, do đó state không thể thay đổi ngay sau khi setState được gọi.

Điều đó có nghĩa là bạn không nên dựa vào state hiện tại khi gọi setState – vì bạn không thể chắc chắn state  đó sẽ là gì!

Đây là giải pháp – bỏ qua một hàm trong setState, với state trước đó như là một đối số.

this.setState(prevState =&gt; ({ expanded: !prevState.expanded }))

Destructuring các Props

import React, { Component } from 'react'

import { observer } from 'mobx-react'

import { string, object } from 'prop-types'

import ExpandableForm from './ExpandableForm'

import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {

  state = { expanded: false }


  static propTypes = {

    model: object.isRequired,

    title: string

  }


  static defaultProps = {

    model: {

      id: 0

    },

    title: 'Your Name'

  }

handleSubmit = (e) => {

    e.preventDefault()

    this.props.model.save()

  }

  

  handleNameChange = (e) => {

    this.props.model.changeName(e.target.value)

  }

  

  handleExpand = (e) => {

    e.preventDefault()

    this.setState(prevState => ({ expanded: !prevState.expanded }))

  }

  

  render() {

    const {

      model,

      title

    } = this.props

    return (

      <ExpandableForm

        onSubmit={this.handleSubmit}

        expanded={this.state.expanded}

        onExpand={this.handleExpand}>

        <div>

          <h1>{title}</h1>

          <input

            type="text"

            value={model.name}

            onChange={this.handleNameChange}

            placeholder="Your Name"/>

        </div>

      </ExpandableForm>

    )

  }

}

Các component với nhiều props cần phải có mỗi bước trên một dòng mới, như ở trên.

Decorators

<a class="markup--anchor markup--pre-anchor" title="Twitter profile for @observer" href="http://twitter.com/observer" target="_blank" rel="noopener" data-href="http://twitter.com/observer">@observer</a>

export default class ProfileContainer extends Component {

Nếu bạn đang sử dụng một cái gì đó như mobx, bạn có thể decorator các class component của bạn như vậy – tương tự như chuyển component vào một hàm.

Decorator là cách linh hoạt và có thể đọc được việc sửa đổi component chức năng. Chúng tôi sử dụng chúng rộng rãi, với mobx và thư viện các mô hình mobx của chúng ta.

Nếu bạn không muốn sử dụng decorator, hãy làm như sau:

class ProfileContainer extends Component {

  // Component code

}

export default observer(ProfileContainer)

Closures

Tránh chuyển closures mới thành các component con, như sau:

&lt;input

            type="text"

            value={model.name}

            // onChange={(e) =&gt; { model.name = e.target.value }}

            // <strong class="markup--strong markup--pre-strong">^ Not this. Use the below:</strong>

            onChange={this.handleChange}

            placeholder="Your Name"/&gt;

Đây là lý do tại sao: mỗi khi component gốc được hiển thị, một chức năng mới được tạo và truyền cho đầu vào.

Nếu đầu vào là một component React, điều này sẽ tự động kích hoạt nó để render lại, bất kể các props khác của nó đã thay đổi như thế nào.

Reconciliation là phần tốt nhất của React. Đừng khiến nó trở nên khó hơn mức cần thiết! Cộng thêm, bỏ qua class method khiến nó dễ đọc, debug và dễ thay đổi hơn.

Đây là component đầy đủ của chúng tôi:

import React, { Component } from 'react'

import { observer } from 'mobx-react'

import { string, object } from 'prop-types'

// Separate local imports from dependencies

import ExpandableForm from './ExpandableForm'

import './styles/ProfileContainer.css'


// Use decorators if needed

@observer

export default class ProfileContainer extends Component {

  state = { expanded: false }

  // Initialize state here (ES7) or in a constructor method (ES6)


  // Declare propTypes as static properties as early as possible

  static propTypes = {

    model: object.isRequired,

    title: string

  }


  // Default props below propTypes

  static defaultProps = {

    model: {

      id: 0

    },

    title: 'Your Name'

  }


  // Use fat arrow functions for methods to preserve context (this will thus be the component instance)

  handleSubmit = (e) => {

    e.preventDefault()

    this.props.model.save()

  }

  

  handleNameChange = (e) => {

    this.props.model.name = e.target.value

  }

  

  handleExpand = (e) => {

    e.preventDefault()

    this.setState(prevState => ({ expanded: !prevState.expanded }))

  }

  

  render() {

    // Destructure props for readability

    const {

      model,

      title

    } = this.props

    return (

      <ExpandableForm

        onSubmit={this.handleSubmit}

        expanded={this.state.expanded}

        onExpand={this.handleExpand}>

        // Newline props if there are more than two

        <div>

          <h1>{title}</h1>

          <input

            type="text"

            value={model.name}

            // onChange={(e) => { model.name = e.target.value }}

            // Avoid creating new closures in the render method- use methods like below

            onChange={this.handleNameChange}

            placeholder="Your Name"/>

        </div>

      </ExpandableForm>

    )

  }

}

Những Component hàm

Các component này không có state và không có method. Chúng là nguyên mẫu và dễ dàng để hiểu được. Sử dụng chúng càng thường xuyên càng tốt.

propTypes

import React from 'react'

import { observer } from 'mobx-react'

import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {

  onSubmit: func.isRequired,

  expanded: bool

}

// Component declaration

Ở đây, chúng ta chỉ định các propTypes trước khai báo component, do đó chúng có thể nhìn thấy ngay lập tức. Chúng tôi có thể làm điều này vì chức năng hoist của JavaScript.

Destructure Props và defaultProps

import React from 'react'

import { observer } from 'mobx-react'

import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {

  onSubmit: func.isRequired,

  expanded: bool,

  onExpand: func.isRequired

}

function ExpandableForm(props) {

  const formStyle = props.expanded ? {height: 'auto'} : {height: 0}

  return (

    <form style={formStyle} onSubmit={props.onSubmit}>

      {props.children}

      <button onClick={props.onExpand}>Expand</button>

    </form>

  )

}

Component của chúng ta là một chức năng, lấy các props của nó làm đối số của nó. Chúng ta có thể mở rộng chúng như sau:

import React from 'react'

import { observer } from 'mobx-react'

import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {

  onSubmit: func.isRequired,

  expanded: bool,

  onExpand: func.isRequired

}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {

  const formStyle = expanded ? {height: 'auto'} : {height: 0}

  return (

    <form style={formStyle} onSubmit={onSubmit}>

      {children}

      <button onClick={onExpand}>Expand</button>

    </form>

  )

}

Lưu ý rằng chúng tôi cũng có thể sử dụng đối số mặc định để hoạt động như defaultProp. Nếu mở rộng không xác định, chúng tôi sẽ đặt nó là false. (Trong một ví dụ bắt buộc, vì đó là một boolean, nhưng đó là cách tốt để tránh lỗi ‘Cannot read <property> of undefined’ với các đối tượng).

Tránh cú pháp ES6 sau:

<strong class="markup--strong markup--pre-strong">const ExpandableForm = ({ onExpand, expanded, children }) =&gt; {</strong>

Nhìn có vẻ rất hiện đại, nhưng chức năng ở đây thực sự không có tên.

Việc thiếu tên sẽ không là vấn đề nếu Babel của bạn được thiết lập đúng – nhưng nếu không, bất kỳ lỗi nào cũng có thể xuất hiện trong <<anonymous>> và đó là điều khủng khiếp khi debug.

Các chức năng không tên cũng có thể gây ra vấn đề với Jest, thư viện kiểm thử của Javascript. Do vấn đề khó khăn để hiểu lỗi (và sự thiếu lợi ích thực sự), chúng tôi khuyên bạn nên sử dụng function thay vì const.

Wrapping

Vì bạn không thể sử dụng decorator với các component chức năng, bạn chỉ cần bỏ qua nó như một đối số:

import React from 'react'

import { observer } from 'mobx-react'

import { func, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {

  onSubmit: func.isRequired,

  expanded: bool,

  onExpand: func.isRequired

}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {

  const formStyle = expanded ? {height: 'auto'} : {height: 0}

  return (

    <form style={formStyle} onSubmit={onSubmit}>

      {children}

      <button onClick={onExpand}>Expand</button>

    </form>

  )

}

export default observer(ExpandableForm)

Đây là component đầy đủ của chúng tôi:

import React from 'react'

import { observer } from 'mobx-react'

import { func, bool } from 'prop-types'

// Separate local imports from dependencies

import './styles/Form.css'


// Declare propTypes here, before the component (taking advantage of JS function hoisting)

// You want these to be as visible as possible

ExpandableForm.propTypes = {

  onSubmit: func.isRequired,

  expanded: bool,

  onExpand: func.isRequired

}


// Destructure props like so, and use default arguments as a way of setting defaultProps

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {

  const formStyle = expanded ? { height: 'auto' } : { height: 0 }

  return (

    <form style={formStyle} onSubmit={onSubmit}>

      {children}

      <button onClick={onExpand}>Expand</button>

    </form>

  )

}


// Wrap the component instead of decorating it

export default observer(ExpandableForm)

Điều kiện trong JSX

Rất có thể bạn đang thực hiện rất nhiều việc render có điều kiện. Dưới đây là những gì bạn muốn tránh khỏi:

Điều kiện trong JSX

Tuy nhiên, đặt chúng lồng vào nhau không phải là một ý tưởng hay.

Có một số thư viện giải quyết vấn đề này (JSX-Control Statements), nhưng thay vì giới thiệu một dependency khác, chúng tôi giải quyết theo cách tiếp cận này cho các điều kiện phức tạp:

Điều kiện trong JSX

Sử dụng dấu ngoặc nhọn đóng gói một IIFE và sau đó đưa câu lệnh if của bạn vào bên trong, trả về bất cứ điều gì bạn muốn render. Lưu ý rằng IIFE như thế này có thể gặp tác dụng phụ, nhưng trong nhiều trường hợp nó sẽ không đủ sức ảnh hưởng làm mất đi các factor dễ hiểu.

Bạn thấy bài viết này như thế nào?: 
Average: 7.9 (14 votes)

Bình luận (0)

 

Add Comment

Plain text

  • No HTML tags allowed.
  • Các địa chỉ web và email sẽ tự động được chuyển sang dạng liên kết.
  • Tự động ngắt dòng và đoạn văn.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
6 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.

Advertisement

 

jobsora

Dich vu khu trung tphcm

Dich vu diet chuot tphcm

Dich vu diet con trung

Quảng Cáo Bài Viết

 
Tại sao và làm như thế nào với RESTful API trong Drupal năm 2015
Tại sao và làm như thế nào với RESTful API trong Drupal năm 2015

Ok, so what’s different about Headless Drupal in comparison to standard ‘vanilla’ Drupal? From the website visitor’s

Quản lý Drupal Content Layout với Panelizer

By default, the fields in a content type are displayed from vertically, one field after another. For simple content types, this might not be a bad thing, but if you want some variation in the way your content fields lay out, you have options.

Kiến trúc cơ bản Drupal continuous integration với Docker
Kiến trúc cơ bản Drupal continuous integration với Docker

Being early adopters of Docker (0.7.6) for our QA and Staging platform we thought it was time to take our CI environment to the next level!