Thử dùng RxJS thay thế Redux

Trải qua 1 thời gian khá dài trải nghiệm Angular, mình thấy RxJS khá hay. Bài này sẽ quay trở lại với bài toán Counter thần kì của React/Redux, nhưng thay vì dùng Redux mình sẽ dùng thử RxJS xem sao. Mục đích của bài viết nhằm mục đích cũng cố lại kiến thức về Redux thông qua RxJS, mình không khuyến khích dùng cách này trong production (tuy nhiên bạn vẫn có thể dùng nếu thích) :)

Nhập môn RxJS:

RxJS đã có khá nhiều bài viết, mình chỉ điểm lại 1 số khái niệm căn bản nhất

Trong RxJS không có khái niệm store, thay vào đó ta có Observer ( nhận data) và Observable (phát data). Observable đơn giản là 1 dòng các sự kiện phát sinh

...
Ví dụ đầu tiên:

//Count ở đây là 1 observable, bất kì 1 observer nào khi subscribe sẽ đều nhận về 1 stream 1,2,3,4
const count = of(1,2,3,4);
// 1 ,2 ,3 ,4
count.subscribe( val => console.log(val));
// 1 ,2 ,3 ,4
count.subscribe( val => console.log(val));

Trong ví dụ trên, Observer là 1 func next =>console.log(next). Bất kì 1 Observer nào subscribe vào count đều nhận được 1 stream (1,2,3,4) như trên.

Ngoài ra còn có 1 khái niệm khác quan trọng không kém là Subject, Subject thật chất là 1 Observable có thể đóng vai trò vừa là nguồn phát lẫn nguồn nhận:

const count = new Subject()
// 1 ,2 ,3 ,4
count.subscribe( val =>console.log(val));
// 1 ,2 ,3 ,4
count.subscribe( val => console.log(val));
count.next(1);
count.next(2);
count.next(3);
count.next(4);
  • BehaviorSubject: là 1 Subject nhưng nó sẽ lưu lại giá trị cuối cùng:

    • So sánh Subject:
const count = new Subject()
count.next(1);
count.next(2);
// Ở đây, observer chỉ thấy đc  stream (3,4), (1,2) sẽ bị bỏ qua
count.subscribe( val =>console.log(val));
count.next(3);
count.next(4);
  • Và BehaviorSubject:
const count = new BehaviorSubject(0);
// 0,1,2,3,4
count.subscribe( val =>console.log(val));
count.next(1);
count.next(2);
// bất kì 1 observer nào subscribe vào sẽ nhận thêm giá trị cuối cùng của stream
// 2 , 3 , 4
count.subscribe( val =>console.log(val));
count.next(3);
count.next(4);

Ngoài ra còn có ReplaySubject, AsyncSubject ...

Nhiêu thế là đủ chiến rồi, bây giờ ta sẽ thử dùng thử RxJS quản lý State thông qua ứng dụng Counter xem sao:

Ứng dụng Counter dùng React:


class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  decr() {
    this.setState({ count: this.state.count-1 })
  }

  incr() {
    this.setState({ count: this.state.count+1 })
  }
  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.incr}>Increase</button>
        <button onClick={this.decr}>Decrease</button>
      </div>);
  }
} 

Store

Sử dụng RxJS chúng ta có thể dùng BehaviorSubject như 1 Store, bạn có thể lưu store như 1 single source of truth theo cách Redux hoặc lưu thành nhiều state nhỏ khác nhau (ví dụ trong Angular, thay vì dùng 1 single store, bạn hoàn toàn có thể lưu state trên nhiều services khác nhau). RxJS hoàn toàn đủ mạnh để có có thể đáp ứng cả 2 cách trên, nếu dùng thành thạo bạn gần như có thể thiên biến vạn hóa.

Trong ví dụ này, mình sẽ sử dụng 1 state chứa 1 giá trị duy nhất count cho đơn giản:

const counterService = new (class CounterService {
  constructor() {
    this.count = new BehaviorSubject(0);
  }
  decr() {
    this.count.next(this.count.getValue() - 1);
  }

  incr() {
    this.count.next(this.count.getValue() + 1);
  }
  getCount() {
    return this.count;
  }
})();


class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
  }
  componentDidMount() {
    counterService
      .getCount()
      .subscribe(it => this.setState({ count: it }))
  }

  decr() {
    counterService.decr();
  }

  incr() {
    counterService.incr();
  }
  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.incr}>Increase</button>
        <button onClick={this.decr}>Decrease</button>
      </div>);
  }
} 

Bạn có thể decouple Counter Component thành 2 Component dumb and smart riêng biệt nếu thích.

Như thế là xong, chúng ta đã có 1 state count tách biệt hoàn toàn khỏi Component, bất kì Component nào nếu muốn subscribe vào state có thể làm tương tự như trên.

 <div>
       {/* 3 component này sẽ nhận về cùng 1 state đồng bộ với nhau */}
        <Counter/>
        <Counter/>
        <Counter/>
</div>

Tuy nhiên cách trên chỉ có thể áp dụng với những state đơn giản, khi state trở nên phức tạp thì mọi thứ sẽ trở nên phức tạp hơn. Lúc này bạn có thể dùng pattern tương tự như Redux

scan()

Sử dụng scan() ta có thể áp dụng pattern Action -> Reducer -> Store -> UI -> Action hệt như Redux:

const dispatcher = new BehaviorSubject(null);
const reducer = (currentState, action) => {
    switch (action) {
        case "INCR":
            return { ...currentState, count: currentState.count + 1 }
            break;
        case "DECR":
            return { ...currentState, count: currentState.count - 1 }
            break;
        default:
            return currentState;
            break;
    }
}
const store = dispatcher.pipe(scan((reducer),
    //initial store
    { count: 0 }));
// store : 0
store.subscribe(val => console.log(val));
// store : 1
dispatcher.next("INCR")
// store : 0
dispatcher.next("DECR")
// store : 1
dispatcher.next("INCR")

Tương tự với ví dụ Counter, ta hoàn toàn có thể viết lại bằng cách này:


const reduxCounterService = new (class CounterService {
  static get DECR(){ return "DECR" };
  static get INCR(){ return "INCR" };
  constructor() {
    this.dispatcher = new BehaviorSubject("");
    this.store = this.dispatcher
      .pipe(scan(this.reducer,
        //initial store
        { counter: 0 }));
  }
  reducer(state, action) {
    switch (action) {
      case CounterService.INCR: return { ...state, counter: state.counter + 1 };
      case CounterService.DECR: return { ...state, counter: state.counter - 1 };
      default: return state;
    }
  }
  decr() {
    this.dispatcher.next(CounterService.INCR);
  }

  incr() {
    this.dispatcher.next(CounterService.DECR);
  }
  getStore() {
    return this.store;
  }
})();

pluck() và map()

2 toán tử này rất hữu ích trong việc quản lý 1 state object phức tạp.

const state = of({
    "name": "John",
    "age": 30,
    "cars": {
        "car1": "Ford",
    }
})
//Ford
state.pipe(map(it => it && it.cars && it.cars.car1 ? it.cars.car1 : undefined))
    .subscribe(it => console.log(it))
//Ford
state.pipe(pluck('cars','car1')).subscribe(it=>console.log(it))

Như vậy, sử dụng 2 toán tử này, bạn hoàn toàn có thể select bất kì property nào từ 1 complex object và biến đổi nó theo ý mình.

Logging

    zip(this.dispatcher, this.store)
      .pipe(
      map(([action, state]) => ({ action, state })),
      pairwise(),
      map(([previousState, currentState]) => ({ previousState, currentState })),
    )
      .subscribe(it => console.log(it));

Sử dụng zippairwise, ta có thể log lại tất cả mọi action và state phát sinh.

Kết

Như bạn thấy, chúng ta hoàn toàn có thể dùng RxJS như 1 cách tiếp cận khác trong việc quản lý state. Tuy nhiên vẫn có một vài điểm hạn chế so với Redux như:

  1. Không có time travel debugging
  2. Không có react-redux hỗ trợ việc connect store và component
  3. Không có hot module replacement
  4. Không có browser ext.

Những khuyết điểm này có thể khắc phục nếu bạn dùng các lib hỗ trợ, chẳng hạn Frintjs

Source ứng dụng Counter sử dụng Rxjs trong bài bạn có thể tham khảo tại đây: Demo

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

cdxf

1 bài viết.
1 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Bài viết liên quan
White
3 1
Chào các bạn Mình vừa mới làm một side project để cập nhật công nghệ mới nhất về React stack. Shopping Cart của mình được build bằng TypeScript, N...
Đinh Viễn viết 5 tháng trước
3 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

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


White
{{userFollowed ? 'Following' : 'Follow'}}
1 bài viết.
1 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á!