Dependency Injection trong React
reactjs
48
dependency injection
10
White

Huỳnh Minh Tú viết ngày 13/11/2019

Dependency Injection là một pattern phổ biến được implement trong rất nhiều framework và library. Nhìn sơ qua thì có vẻ React không có. Nhưng thật sự là React đã hỗ trợ built-in dependency injection là JSX, chắc hẳn là bạn đang dùng nó đó. :joy:

Dependency Injection in short

Dependency Injection giải quyết một bài toán phổ biến: "hardcoded dependencies" - nghĩa là các phần dependency được fix cứng trong code. Khi một object A phụ thuộc vào một object B và sau đó tạo ra một object thứ hai, thì lúc này dependency không thể thay đổi (vì đã bị hardcode).

Ví dụ, class Calculator tạo một logger service, được fix cứng là ConsoleLogger, không thể thay đổi sau này:

import ConsoleLogger from "./ConsoleLogger";

class Calculator {
  constructor() {
    this.logger = new ConsoleLogger();
  }

  add(a, b) {
    this.logger.log(`Adding ${a} to ${b}`);
    return a + b;
  }
}

Tuy nhiên khi thực hiện test class này, developers muốn "mock" thành phần logger service này (không log ra console nữa chẳng hạn). Hoặc khi chạy production, họ muốn xài SysLogger hoặc một logger khác ví dụ của Sentry (SaaS logger) thì đều không được vì dependency đã được hardcode.

Chúng ta có thể giải quyết vấn đề trên bằng một commit xài Dependency Injection như sau:

-import ConsoleLogger from './ConsoleLogger';

class Calculator {
-   constructor() {
-       this.logger = new ConsoleLogger();
-   }
+   constructor(logger) {
+       this.logger = logger;
+   }

    add(a, b) {
        this.logger.log(`Adding ${a} to ${b}`);
        return a+b;
    }
}

Giờ đây dependency không còn phụ thuộc vào lúc object A tạo object B nữa:

const logger = new ConsoleLogger();
const calculator = new Calculator(logger);
const result = calculator.add(1, 2); // console hiển thị "Adding 1 to 2"

Và như vậy các developers có thể dễ dàng thay đổi dependency sau này:

const logger = new NullLogger();
const calculator = new Calculator(logger);
const result = calculator.add(1, 2); // console shows nothing

Nhờ vào Dependency Injection, code dường như được clean hơn, modular hoá rõ ràng hơn. Angular có một buil-in Dependency Injection Container, SymfonySpring cũng thế. Nhưng có vẻ như React.js không có?

JSX hỗ trợ Dependency Injection

React hỗ trợ Dependency Injection mà không cần dependency injection container (như Angular), nhờ vào JSX.

Hãy nhìn vào component render product reviews trong table dưới đây:

Đây là đoạn code render table:

const ReviewList = props => (
  <List resource="reviews" perPage={50} {...props}>
    <Datagrid rowClick="edit">
      <DateField source="date" />
      <CustomerField source="customer_id" />
      <ProductField source="product_id" />
      <RatingField source="rating" />
      <TextField source="body" label="Comment" />
      <StatusField source="status" />
    </Datagrid>
  </List>
);

Component lấy dữ liệu từ route /review/ trong một REST API và truyền vào thêm một parameter là perPage. Nhưng không thực hiện viện render bảng reviews. Thay vào đó, nó để việc render cho các child components làm: . Điều đó có nghĩa rằng phụ thuộc vào cho phần render, một paren-child relationship đã được tạo ra ở đây, chính là Dependency Injection.

Và như tất cả các dạng Dependency Injection nào khác, ta có thể đổi phần "dependency" rất dễ dàng. Chẳng hạn trong ví dụ trên, nếu ta muốn đổi thành phần render thành một "simple list" thay vì một "data grid", ta chỉ cần thay thế child của bằng một component khác:

import { List, Datagrid, TextField, DateField } from 'react-admin';

const PostList = props => (
    <List resource="posts" perPage={50}>
        <SimpleList
            primaryText={review => <ItemTitle record={review} />}
            secondaryText={review => review.body}
        >
    </List>
)

React thậm chí còn cho phép inject nhiều hơn một dependency trong một element. Đầu tiên, bởi vì một element có thể có nhiều hơn một child. Trong ví dụ trước, nhận vào một list của các columns cần được hiển thị:

const ReviewList = props => (
  <List resource="reviews" perPage={50}>
    <Datagrid rowClick="edit">
      <DateField source="date" />
      <CustomerField source="customer_id" />
      <ProductField source="product_id" />
      <RatingField source="rating" />
      <TextField source="body" label="Comment" />
      <StatusField source="status" />
    </Datagrid>
  </List>
);

Ngoài inject bằng các child components, ta còn có thể inject thông qua props, như bên dưới ta cũng có thể inject vào một DataGrid body khác:

const ReviewList = props => (
    <List resource="reviews" perPage={50}>
-       <Datagrid rowClick="edit">
+       <Datagrid rowClick="edit" body={<MyDatagridBody />} >
            <DateField source="date" />
            <CustomerField source="customer_id" />
            <ProductField source="product_id" />
            <RatingField source="rating" />
            <TextField source="body" label="Comment"/>
            <StatusField source="status" />
        </Datagrid>
    </List>
)

Và vì mỗi dependency là một component, ta cũng có thể inject dependency vào dependency, như vầy:

const ReviewList = props => (
    <List resource="reviews" perPage={50}>
        <Datagrid
            rowClick="edit"
-           body={<MyDatagridBody />}
+           body={<MyDatagridBody withBulkActions />}
        >
            <DateField source="date" />
            <CustomerField source="customer_id" />
            <ProductField source="product_id" />
            <RatingField source="rating" />
            <TextField source="body" label="Comment"/>
            <StatusField source="status" />
        </Datagrid>
    </List>
)

Như vậy, JSX đã có những tính năng cơ bản của một Dependency Injection Container: cho phép di chuyển, thay thế linh động các dependency và cấu hình chúng.

React Context

Ở trên ta đã thấy việc dùng JSX để implement Dependency Injection trong JSX Template, vậy còn ở các service function thì sao? React cũng khuyến khích xài components cho các services luôn. Ví dụ, ta có thể truyền một translation service dùng props của component:

const englishTranslator = message => {
  if (message == "hello.world") {
    return "Hello, World!";
  }
  return "Not yet translated";
};

const Greeting = ({ translate }) => {
  return <div>{translate("hello.world")}.</div>;
};

const App = () => <Greeting translate={englishTranslator} />;

Tuy nhiên các viết này có thể cồng kềnh nếu code nhiều và có các lớp nested. Vì vậy React cung cấp một thành phần khác để khai báo Dependency Injection là context:

import React, { useContext } from "react";

const englishMessages = message => {
  if (message == "hello.world") {
    return "Hello, World!";
  }
  return "Not yet translated";
};

const TranslationContext = React.createContext();

const Greeting = () => {
  const translate = useContext(TranslationContext);
  return <div>{translate("hello.world")}.</div>;
};

const App = () => (
  <TranslationContext.Provider value={englishMessages}>
    <Greeting />
  </TranslationContext.Provider>
);

Tạm kết

Vì sao React không cung cấp một Dependency Injection Container như Angular? Đơn giản vì không cần thiết, JSX và context đã quá đủ để giúp các React app trở nên modular và testable hơn rồi.

Đây chính là một điểm mạnh của React: ngay cả với các ứng dụng lớn và large-scale, React vẫn làm tốt được mọi thứ mà không đỏi hỏi những framework lớn và cồng kềnh (như Angular :joy:):

References

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Huỳnh Minh Tú

10 bài viết.
70 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
50 11
Ngày nay khi mà MVC dần trở thành một thứ nhà quê "lạc hậu", APIs được thiết kế chung cho nhiều client app khác nhau (web app, mobile app or deskto...
Huỳnh Minh Tú viết 10 tháng trước
50 11
White
26 4
Việc phát triển web frontend side ở một số hệ thống lớn lâu nay vẫn được làm theo kiểu monoliths (dù có thể backend đã được microservice hoá), ai c...
Huỳnh Minh Tú viết 10 tháng trước
26 4
White
15 1
Cookie, session, token, JWT, lưu token ở đâu, các mối quan tâm về xác thực trong một hệ thống SinglePage Application... tất cả mọi thứ bạn cần biết...
Huỳnh Minh Tú viết 10 tháng trước
15 1
Bài viết liên quan
White
3 1
(Ảnh) Giả sử tại một trang, bạn render ra danh sách 100 items trong khi màn hình của user chỉ hiển thị được tối đa 3 items thôi, việc này sẽ làm g...
Lý Thành Nhân viết 2 tháng trước
3 1
White
12 2
(Link) (Link) &mdash; tracks device battery state. (Link) (Link) &mdash; tracks geo location state of user's device. (Link) (Link) &mdash; tr...
Đinh Viễn viết 4 tháng trước
12 2
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
10 bài viết.
70 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!