React’s new Context API

Một cách tiện lợi hơn, không còn là tính năng thử nghiệm, React context API hiện tại đã là “first-class” API và nó sử dụng “render props”

Bạn đã từng nghe về  Context API trong React? Nếu bạn đã nghe hoặc sử dụng, bạn có giống những lập trình viên JS khác đều dè chừng khi sử dụng kỹ thuật này bởi vì bạn thấy điều này trên trang docs chính thức của Facebook:

Kết quả đầu tiên hiện lên là “Why Not To Use Context” làm chúng ta không mấy tự tin khi sử dụng “context API”. Để làm cho mọi thứ thuyết phục hơn, bài viết đó nói thêm:

Nếu bạn muốn ứng dụng của bạn được ổn định, đừng sử dụng context . Nó là một API thử nghiệm và nó có khả năng sẽ thay đổi trong các bản phát hành trong tương lai của React.

Vậy thì tại sao lại sử dụng context

Bạn đã từng trải nghiệm “nỗi đau” khi cố gắng lấy state của component cha từ component “cháu chắt”? “This pain” được gọi là “prop drilling” và nó cực kỳ phiền toái. Bạn cần phải truyền props thông qua các components trung gian, những cái mà không quan tâm về dữ liệu, chỉ để bạn có thể gửi nó xuống các components đích. Và khi bạn di chuyển, thay đổi vị trí các components, sự phiền toái này được đẩy lên đỉnh điểm.

Các bạn có thể giảm thiểu sự phiền toái này bằng cách sử dụng các thư viện quản lý state như FluxRedux…, nó giúp chúng ta lấy dữ liệu từ một store dễ dàng từ bất kỳ component nào trong DOM tree. Những việc bạn cần làm là sử dụng một thứ gọi là

<Provider />

và một điều kì diệu xuất hiện, store data (kho dữ liệu) của bạn được truy xuất bởi bất kỳ component nào mà “connected” (được kết nối đến store).

Nếu tôi nói cho bạn biết rằng <Provider /> cũng đã sử dụng experimental context feature (tính năng thử nghiệm context API). Thật sự thì đúng là vậy, provider component đặt dữ liệu bên trong context, và connect Higher Order Component kéo dữ liệu ra khỏi context. Vậy nên trong thực tế, redux không làm công việc là cho phép dữ liệu được truy xuất từ bất kỳ vị trí nào trong DOM tree… context đã làm việc đó.

Nếu vậy, tại sao bạn nên sử dụng context API? Có lẽ bạn đã và đang sử dụng nó một cách gián tiếp hoặc trực tiếp mà không ý thức được việc đó. Ứng dụng của bạn có đang sử dụng các npm như: React-redux, MobX-react, react-router…, nếu có, bạn đang sử dụng context API một cách gián tiếp.

Sự tái sinh của Context (Context Reborn)

Bạn có nhớ cảnh báo: “nó có khả năng sẽ thay đổi trong các bản phát hành trong tương lai của React“. Ngày đó đã tới và bạn sẽ thích sự thay đổi này.( Tại thời điểm dịch bài này, React đã update lên version 16.3)

Vậy thì nó trông như thế nào? Wow, nó trực quan hơn cả triệu lần so với context API cũ. Dưới đây là ví dụ đơn giản nhất giúp các bạn hình dung:

https://codesandbox.io/embed/n4r0qq898j

const ThemeContext = React.createContext('light')
class ThemeProvider extends React.Component {
  state = {theme: 'light'}
  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children}
      </ThemeContext.Provider>
    )
  }
}
class App extends React.Component {
  render() {
    return (
      <ThemeProvider>
        <ThemeContext.Consumer>
          {val => <div>{val}</div>}
        </ThemeContext.Consumer>
      </ThemeProvider>
    )
  }
}

The new context API bao gồm 3 phần chính:

  • React.createContext phần mà được truyền như là giá trị ban đầu. Hàm này trả về một object với một Provider và một Consumer
  • The Provider component được sử dụng tại tầng cao hơn trong DOM tree, và cho phép một prop gọi giá trị (cái mà có thể là một value, function…)
  • The Consumer component được sử dụng ở bất cứ đâu phía dưới của Provider trong DOM tree và cho phép một prop gọi “children” cái mà phải là một hàm mà chập nhận giá trị và phải trả về một react element(còn gọi là JSX).

Tôi vô cùng phấn khích với API mới này. React team sẽ bỏ warning về nội dung rằng context là tính năng thử nghiệm bởi vì hiện tại nó là “first-class feature” của framework. Điều này có nghĩa là chúng ta hi vọng sẽ ít cần bận tâm hơn về việc sử dụng nó để giúp giải quyết vấn đề “prop-drilling” trong các ứng dụng, hi vọng sẽ giúp mọi người cảm thấy rằng thay vì họ phải tìm hiểu redux sớm để giải quyết vấn đề đó, họ có thể sử dụng React thuần.

Practical Context

Một câu hỏi mà tôi thấy rất nhiều về context API mới là: “Làm thế nào để kết hợp các provider và consumer với nhau”. Khi bạn muốn đặt một tá các render prop component với nhau trong một single render method, mọi thứ có thể được nested.

render() {
   return (
    <AsyncProps>
       activeItem={this.state.activeItem}
       query={this.state.query}
       data={this.state.data}
       defaultProps={{activeItem: null}}
       {asyncProps => (
         <AsyncBoundary>
            {isDetailLoading => (
             <Debounce value={isDetailLoading}>
                {loadingItem => (
                  <MasterDetail
                     header={}
                    >
                     
                  </MasterDetail>
)}
            </Debounce>
)}
         </AsyncBoundary>
)}
 </AsyncProps>
)
}

 

 

Vậy chúng ta có thể làm gì để tránh điều này? Nếu nó làm phiền bạn, bạn có thể giải quyết nó giống như cách bạn giải quyết vấn đề trong Javascript thông thường: các hàm hoặc components tiện ích. Dưới đây là 1 ví dụ:

const ThemeContext = React.createContext('light')
class ThemeProvider extends React.Component {/* code */}
const ThemeConsumer = ThemeContext.Consumer
const LanguageContext = React.createContext('en')
class LanguageProvider extends React.Component {/* code */}
const LanguageConsumer = LanguageContext.Consumer
function AppProviders({children}) {
  return (
    <LanguageProvider>
      <ThemeProvider>
        {children}
      </ThemeProvider>
    </LanguageProvider>
  )
}
function ThemeAndLanguageConsumer({children}) {
  return (
    <LanguageConsumer>
      {language => (
        <ThemeConsumer>
          {theme => children({language, theme})}
        </ThemeConsumer>
      )}
    </LanguageConsumer>
  )
}
class App extends React.Component {
  render() {
    return (
      <AppProviders>
        <ThemeAndLanguageConsumer>
          {({theme, language}) => <div>{theme} and {language}</div>}
        </ThemeAndLanguageConsumer>
      </AppProviders>
    )
  }
}

Mục đích ở đây là lấy các common use case và tạo ra các hàm/components đặc biệt để làm cho các use case đó tiện lợi hơn. Cũng giống như những gì bạn làm trong JS thông thường, nghe có vẻ hợp lý phải không? Dù sao thì  tôi hi vọng vậy!

Một ví dụ khác dưới đây sẽ chỉ ra điều tồi tệ gì bạn có thể gặp phải và làm thế nào để sử dụng một thư viện tiện ích như react-composer: 

// this file has three examples of composing nexted
// providers and consumers in a single component
// normally you wouldn't have your entire app in a single component like this
// and you wouldn't normally put a consumer right below a provider
// (consumers will likely be lower in your tree) so the nesting wont
// often be as big of a problem.
// however, if you ever do face huge nesting problems,
// react-composer is here to help!
import React from 'react'
import {render} from 'react-dom'
import Composer from 'react-composer'

const ThemeContext = React.createContext('light')
class ThemeProvider extends React.Component {
  state = {theme: 'light'}
  toggleTheme = () => {
    this.setState(({theme}) => ({
      theme: theme === 'light' ? 'dark' : 'light',
    }))
  }
  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children({toggleTheme: this.toggleTheme})}
      </ThemeContext.Provider>
    )
  }
}
// only doing this to shield end-users from the
// implementation detail of "context"
const ThemeConsumer = ThemeContext.Consumer

const LanguageContext = React.createContext('en')
class LanguageProvider extends React.Component {
  state = {lang: 'en'}
  setLanguage = lang => {
    this.setState({lang})
  }
  render() {
    return (
      <LanguageContext.Provider value={this.state.lang}>
        {this.props.children({setLanguage: this.setLanguage})}
      </LanguageContext.Provider>
    )
  }
}
const LanguageConsumer = LanguageContext.Consumer

const styles = {
  light: {
    padding: 20,
    backgroundColor: 'white',
    color: 'black',
  },
  dark: {
    padding: 20,
    backgroundColor: 'black',
    color: 'white',
  },
}

const translations = {
  en: {
    light: 'light',
    dark: 'dark',
  },
  es: {
    light: 'claro',
    dark: 'oscuro',
  },
  de: {
    light: 'hell',
    dark: 'dunkel',
  },
}

// nested big time
function AppNested() {
  return (
    <LanguageProvider>
      {({setLanguage}) => (
        <ThemeProvider>
          {({toggleTheme}) => (
            <LanguageConsumer>
              {lang => (
                <ThemeConsumer>
                  {theme => (
                    <div
                      style={{
                        zoom: 2,
                        textAlign: 'center',
                        border: '1px solid',
                      }}
                    >
                      <div style={styles[theme]}>
                        <button onClick={toggleTheme}>
                          {translations[lang][theme]}
                        </button>
                        <select
                          value={lang}
                          onChange={e => setLanguage(e.target.value)}
                        >
                          <option value="en">en</option>
                          <option value="es">es</option>
                          <option value="de">de</option>
                        </select>
                      </div>
                    </div>
                  )}
                </ThemeConsumer>
              )}
            </LanguageConsumer>
          )}
        </ThemeProvider>
      )}
    </LanguageProvider>
  )
}

// using composer inline
function AppComposed() {
  return (
    <Composer components={[<LanguageProvider />, <ThemeProvider />]}>
      {([{setLanguage}, {toggleTheme}]) => (
        <Composer components={[<LanguageConsumer />, <ThemeConsumer />]}>
          {([lang, theme]) => (
            <div
              style={{
                zoom: 2,
                textAlign: 'center',
                border: '1px solid',
              }}
            >
              <div style={styles[theme]}>
                <button style={{cursor: 'pointer'}} onClick={toggleTheme}>
                  {translations[lang][theme]}
                </button>
                <select
                  value={lang}
                  onChange={e => setLanguage(e.target.value)}
                >
                  <option value="en">en</option>
                  <option value="es">es</option>
                  <option value="de">de</option>
                </select>
              </div>
            </div>
          )}
        </Composer>
      )}
    </Composer>
  )
}

// extracting the composer part
function AppSuperComposeRenderer({children}) {
  return (
    <Composer components={[<LanguageProvider />, <ThemeProvider />]}>
      {([{setLanguage}, {toggleTheme}]) => (
        <Composer components={[<LanguageConsumer />, <ThemeConsumer />]}>
          {([lang, theme]) => children({setLanguage, toggleTheme, lang, theme})}
        </Composer>
      )}
    </Composer>
  )
}

// so we can shove that nesting asside!
function AppSuperCompose() {
  return (
    <AppSuperComposeRenderer>
      {({setLanguage, toggleTheme, lang, theme}) => (
        <div
          style={{
            zoom: 2,
            textAlign: 'center',
            border: '1px solid',
          }}
        >
          <div style={styles[theme]}>
            <button style={{cursor: 'pointer'}} onClick={toggleTheme}>
              {translations[lang][theme]}
            </button>
            <select value={lang} onChange={e => setLanguage(e.target.value)}>
              <option value="en">en</option>
              <option value="es">es</option>
              <option value="de">de</option>
            </select>
          </div>
        </div>
      )}
    </AppSuperComposeRenderer>
  )
}

render(<AppSuperCompose />, document.getElementById('root'))

Tôi nên đề cập rằng tôi không mong muốn bạn cần phải nest các render props component rất nhiều trong thực tế. Bất cứ khi nào bạn làm điều đó, bạn có thể tạo một component đơn giản cái mà compose chúng lại với nhau và sử dụng component đó thay vì nested.

Còn rất nhiều điều chờ đón phía trước! Chúc may mắn, guys! 🙂

 

Nguồn: https://medium.com/dailyjs/reacts-%EF%B8%8F-new-context-api-70c9fe01596b

Comments

Let’s make a great impact together

Be a part of BraveBits to unlock your full potential and be proud of the impact you make.