07/09/2018, 16:55

Quản lý state trong React & React Native không sử dụng State management library

Đối với những bạn đã biết và sử dụng React & React Native thì việc dùng kèm 1 thư viện để quản lý state như Redux là điều rất phổ biến. Mình cũng sử dụng React được một thời gian tuy nhiên thời gian đầu mình hay sử dụng theo hướng thuần - không sử dụng thư viện riêng biệt để quản lý state (lấy ...

Đối với những bạn đã biết và sử dụng React & React Native thì việc dùng kèm 1 thư viện để quản lý state như Redux là điều rất phổ biến. Mình cũng sử dụng React được một thời gian tuy nhiên thời gian đầu mình hay sử dụng theo hướng thuần - không sử dụng thư viện riêng biệt để quản lý state (lấy Redux làm đại diện). Trong bài viết này mình sẽ chia sẻ và trao đổi một số tips khi làm việc với React & React Native theo hướng thuần.

Lưu ý:

  • Bài viết chỉ đơn giản là chia sẻ và trao đổi, không khuyến khích các bạn bỏ Redux, do vậy hãy giữ tư tưởng mở.
  • Để hiểu rõ bài viết thì nên có kiến thức về React, đối với những bạn mới tiếp xúc với React / React Native nếu gặp khó khăn trong việc học Redux (phải học trước một đống thứ như React, JSX, Babel, Webpack, Native Component & Native Module dẫn đến ngợp), thì hy vọng bài viết này sẽ cung cấp cho các bạn 1 cái gì đó để thoải mái hơn và vững vàng hơn trước khi học những cái mới.

Nhắc lại vấn đề quản lý state của React

  • Luồng dữ liệu pass data down, pass event up. làm cho phải truyền data và function qua props khá nhiều, khi app phức tạp dần lên thì rất khó quản lý. (Vấn đề quản lý dữ liệu giữ component cha - con )
  • Truyền qua props thì các component không cùng cây thư mục rất khó giao tiếp (Vấn đề giao tiếp giữa các component)
  • Vấn đề đồng bộ dữ liệu giữa các component khi 1 thông tin chung thay đổi.

Giải pháp? Redux ? Redux giải quyết các vấn đề trên khá OK, tuy nhiên nó kèm theo một đống thứ khác nữa khiến việc học và vận dụng trở nên khó khăn. Bài viết sẽ cung cấp 2 hướng tiếp cận và 1 loạt các tool sử dụng trong các trường hợp.

Hướng tiếp cận 1: Global Store - Single Source of Truth

Global Store là chính tư tưởng của Redux, tuy nhiên nó có thể được thực hiện chỉ cần sử dụng React.

1. Context

context là một phần ít được đề cập trong React. Sử dụng context thì có thể giao tiếp giữa component cha với component con mà không cần pass qua các component ở giữa. Thông tin về context các bạn có thể đọc thêm ở đây. Luồng làm việc với context sẽ như sau:

  1. Khai báo context ở Component to nhất (Tạm gọi là Root )
  2. Tạo 1 method ở Root có thể get/set state.
  3. Truyền method đó qua context
  4. Khai báo nhận context ở các Component con (Tạm gọi là Child). Child chỉ việc sử dụng method này để get/set state của Root.
  5. Mọi thông tin cần thiết đều lưu vào state của Root. Khi có thay đổi thì Root sẽ render lại dẫn đến UI được cập nhật một cách tự động.

Tham khảo Gist:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Root extends Component {
static childContextTypes = {
appState: PropTypes.func
};
getChildContext = () => {
return {
appState: this.appState
}
}
state = {
message: "Hello World"
}
appState = (objOrFunc, func) => {
if (typeof objOrFunc === 'string') return this.state[obj];
const isParamValid = typeof objOrFunc === 'object' || typeof obj === 'function';
if (isParamValid === true && typeof func === 'undefined') {
this.setState(objOrFunc);
} else if (isParamValid === true && typeof func === 'function') {
this.setState(objOrFunc, () => {
func();
})
} else {
return false;
}
}
}
class Child extends Component {
componentWillMount() {
this.store = this.context.appState;
typeof this.willMount === "function" && this.willMount();
}
static contextTypes = {
appState: PropTypes.func
}
}
/*
When creating Components, make sure extends Root & Child instead of Component (only extends Root once)
Then we can use this.store at Child. For this example:
this.store("message") will return "Hello World"
this.store({ message: "Hi" }) will setState Root and re-render
*/
view raw contextRootChild.js hosted with ❤ by GitHub

Vấn đề với việc sử dụng context: Bản thân Facebook cũng không khuyến khích sử dụng context, vì nó có thể thay đổi trong tương lai, cũng như nếu sử dụng không chuẩn sẽ rất rối. Tuy nhiên chúng ta hãy lướt qua các thư viện nổi tiếng:

  • react-redux sử dụng context (không đáng kể)
  • react-router sử dụng context
  • react-navigation sử dụng context
  • Nhiều thư viện về UI sử dụng context

Mặc dù không nhiều tuy nhiên context vẫn được sử dụng. Vấn đề là cách sử dụng thế nào mà thôi. Mặt khác gói trong Root và Child thì sẽ không làm việc trực tiếp với context (this.store), nếu context bị loại bỏ ở React trong các phiên bản sau, chúng ta hoàn toàn có thể cập nhật lại 2 file Child và Root (bằng giải pháp tiếp dưới đây) là xong.

2. Export method setState

Giải pháp này hoàn toàn có thể thay thế context, ngoài ra còn vượt trội hơn khá nhiều khi có thể truy cập được ngoài phạm vi React, giữa các React Application với nhau (trường hợp website hỗn hợp React và các cách xây dựng UI khác).
Luồng làm việc với export method setState sẽ như sau:

  1. Tạo 1 BaseComponent
  2. Thiết lập biến static cho BaseComponent
  3. Trong componentWillMount, gán biến static đó vào một function mới tạo để có thể get/set state
  4. Viết lại biến static để có thể dùng với nhiều instance. (Có thể quản lý thông qua id)
  5. Khi tạo các component thì extends từ BaseComponent, sau đó có thể import và gọi trực tiếp biến static để get/set state cho component (dĩ nhiên là component đó phải được mount vào rồi)

Bằng với cách này, chúng ta có thể quản lý mọi component ở bất cứ đâu. Nếu theo hướng Global Store, thì chỉ cần quản lý component Root là đủ.

Tham khảo gist:

import { Component } from "react";
import PropTypes from "prop-types";
export default class BaseComponent extends Component {
static propTypes = {
id: PropTypes.string
}
static States = {};
static getId = id => BaseComponent.States[id];
static setStateAllInstances = async objOrFunc => {
let promiseArr = [];
for ( let id in BaseComponent.States) {
promiseArr.push(BaseComponent.States[id].setState(objOrFunc));
}
await Promise.all(promiseArr);
}
_setupStateById = () => {
if (!this.props.id) return;
const setState = obj => this.set(obj);
const state = () => this.state;
BaseComponent.States[this.props.id] = {
setState: setState,
state: state
};
};
mounted = false;
set = objOrFunc => this.mounted === false
? Promise.resolve()
: new Promise((resolve, reject) => {
try {
this.setState(objOrFunc, () => {
resolve();
});
} catch (err) {
reject(err);
}
});
componentWillMount() {
this._setupStateById();
typeof this.willMount === "function" && this.willMount();
}
componentDidMount() {
this.mounted = true;
typeof this.didMount === "function" && this.didMount();
}
componentWillUnmount() {
this.mounted = false;
typeof this.willUnmount === "function" && this.willUnmount();
delete BaseComponent.States[this.props.id];
}
}
/*
Assume you have a component (that extends BaseComponent):
<ContainerA id="cA" />
if you want to get state of this ContainerA: do the following
```
import ContainerA from "./somewhere";
console.log(ContainerA.getId("cA").state())
```
if you want to set state of this ContainerA: do the following
```
import ContainerA from "./somewhere";
ContainerA.getId("cA").setState({ foo: "bar" })
```
*/
view raw BaseComponent.js hosted with ❤ by GitHub

Example app về phần này mình đang hoàn thiện và sẽ cập nhật sau.

Hướng tiếp cận 2: Quản lý dữ liệu trong phạm vi service

service đơn giản là cách gọi một hoạt động được thực hiện không liên quan đến UI (headless) và đảm nhận một công việc gì đó, nôm na là sẽ viết riêng phần xử lý logic của app ra bên ngoài và trả dữ liệu cho UI để hiển thị.

Luồng hoạt động của service:

  1. Không xử lý logic trong Component
  2. Tạo một service module có thể chạy độc lập, headless. service dùng để làm tính năng cho app (ví dụ như wrapper của server API, socket, local DB, thực hiện một số function native).
  3. Bên component lấy kết quả từ API trả về để cập nhật UI
  4. Viết event listener cho service, để cập nhật dữ liệu ngay cho UI

1 ví dụ khá tiêu biểu cho việc sử dụng service là firebase. Như dùng firebase thì login xong nó cũng không trả về token j cho mình, chỉ biết là đã login, khi nào hoạt động gì cũng sử dụng qua function của nó. Tất cả những dữ liệu đều được gói gọn bên trong và chỉ trả về ra bên ngoài những gì cần thiết. Khi firebase database có dữ liệu thay đổi, thì nó cũng cung cấp 1 vài hàm để xử lý dữ liệu mới kịp thời.

Một ví dụ về cách cấu tạo 1 service, sử dụng class và state để quen thuộc với React

class ServiceA {
    constructor() {
      this.syncData();
    }

    state = {}; 
    // lưu 1 state trực tiếp bằng cách this.state.something = "something"

    syncData = () => {
       // sync data với local khi mở lại app nếu cần
    }

    login = (id, pass) => {
      // call api or something
    }

    logout = () => {

    }

    onLoggedOut = ( callback ) => {
      // listener dùng để chạy callback khi đã logout
    }
}

// export với new để sử dụng singleton
export default new ServiceA();

Ngoài việc dùng event listener trong service, bạn có thể sử dụng trực tiếp để giao tiếp giữa các component với nhau (VD: Chuyển modal alert từ dạng component sang dạng function). Tham khảo react-native-event-listeners và js-events-listener

Tạm kết lại

Bài viết mới dừng lại ở lý thuyết, mình sẽ cập nhật example code và giải thích sâu hơn sau nếu được. Tuy nhiên cũng khá rõ ràng để có thể suy nghĩ và xem xét rồi.

Vậy so với Redux thì sao?

Như đã nói ở trước Redux ngoài việc giải quyết các vấn đề hay gặp của React thì còn một số thứ khác nữa. Nó đề cập nhiều đến các vấn đề như architecture (từ Flux cho đến Redux), các khái niệm mới khi implement (action & reducer), và ecosystem để dev theo the Redux way. So với bài viết thì chỉ đơn giản cung cấp cho bạn 1 vài công cụ để có thể làm được nhiều thứ hơn với React thuần. Bạn có thể sử dụng tự do theo ý thích. (Thực tế sử dụng Redux không tốt cũng rất dễ rối do số lượng action và reducer ngày càng tăng. Điều quan trọng vẫn là cách tổ chức của dev).

Hãy thử tạm quên Redux đi và suy nghĩ về các công cụ trên :)

0