Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJS and React multiple clicked elements to form single data array

So I just started trying to learn rxjs and decided that I would implement it on a UI that I'm currently working on with React (I have time to do so, so I went for it). However, I'm still having a hard time wrapping my head around how it actually works... Not only "basic" stuff like when to actually use a Subject and when to use an Observable, or when to just use React's local state instead, but also how to chain methods and so on. That's all too broad though, so here's the specific problem I have.

Say I have a UI where there's a list of filters (buttons) that are all clickeable. Any time I click on one of them I want to, first of all, make sure that the actions that follow will debounce (as to avoid making network requests too soon and too often), then I want to make sure that if it's clicked (active), it will get pushed into an array and if it gets clicked again, it will leave the array. Now, this array should ultimately include all of the buttons (filters) that are currently clicked or selected.

Then, when the debounce time is done, I want to be able to use that array and send it via Ajax to my server and do some stuff with it.

import React, { Component } from 'react';
import * as Rx from 'rx';

export default class CategoryFilter extends Component {
 constructor(props) {
    super(props);

    this.state = {
        arr: []
    }

    this.click = new Rx.Subject();
    this.click
    .debounce(1000)
    // .do(x => this.setState({
    //  arr: this.state.arr.push(x)
    // }))
    .subscribe(
       click => this.search(click),
       e => console.log(`error ---> ${e}`),
       () => console.log('completed')
    );
 }

search(id) {
    console.log('search --> ', id);
    // this.props.onSearch({ search });
}

clickHandler(e) {
    this.click.onNext(e.target.dataset.id);
}

render() {
    return (
        <section>
            <ul>
                {this.props.categoriesChildren.map(category => {
                    return (
                        <li
                            key={category._id}
                            data-id={category._id}
                            onClick={this.clickHandler.bind(this)}
                        >
                            {category.nombre}
                        </li>
                    );
                })}
            </ul>
        </section>
    );
 }
}

I could easily go about this without RxJS and just check the array myself and use a small debounce and what not, but I chose to go this way because I actually want to try to understand it and then be able to use it on bigger scenarios. However, I must admit I'm way lost about the best approach. There are so many methods and different things involved with this (both the pattern and the library) and I'm just kind of stuck here.

Anyways, any and all help (as well as general comments about how to improve this code) are welcome. Thanks in advance!

---------------------------------UPDATE---------------------------------

I have implemented a part of Mark's suggestion into my code, but this still presents two problems:

1- I'm still not sure as to how to filter the results so that the array will only hold IDs for the buttons that are clicked (and active). So, in other words, these would be the actions:

  • Click a button once -> have its ID go into array
  • Click same button again (it could be immediately after the first click or at any other time) -> remove its ID from array.

This has to work in order to actually send the array with the correct filters via ajax. Now, I'm not even sure that this is a possible operation with RxJS, but one can dream... (Also, I'm willing to bet that it is).

2- Perhaps this is an even bigger issue: how can I actually maintain this array while I'm on this view. I'm guessing I could use React's local state for this, just don't know how to do it with RxJS. Because as it currently is, the buffer returns only the button/s that has/have been clicked before the debounce time is over, which means that it "creates" a new array each time. This is clearly not the right behavior. It should always point to an existing array and filter and work with it.

Here's the current code:

import React, { Component } from 'react';
import * as Rx from 'rx';

export default class CategoryFilter extends Component {
 constructor(props) {
    super(props);

    this.state = {
        arr: []
    }

    this.click = new Rx.Subject();
    this.click
    .buffer(this.click.debounce(2000))
    .subscribe(
        click => console.log('click', click),
        e => console.log(`error ---> ${e}`),
        () => console.log('completed')
    );
 }

search(id) {
    console.log('search --> ', id);
    // this.props.onSearch({ search });
}

clickHandler(e) {
    this.click.onNext(e.target.dataset.id);
}

render() {
    return (
        <section>
            <ul>
                {this.props.categoriesChildren.map(category => {
                    return (
                        <li
                            key={category._id}
                            data-id={category._id}
                            onClick={this.clickHandler.bind(this)}
                        >
                            {category.nombre}
                        </li>
                    );
                })}
            </ul>
        </section>
    );
 }
}

Thanks, all, again!

like image 968
deathandtaxes Avatar asked Nov 03 '16 00:11

deathandtaxes


1 Answers

Make your filter items an Observable streams of click events using Rx.Observable.fromevent (see https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/events.md#converting-a-dom-event-to-a-rxjs-observable-sequence) - it understands a multi-element selector for the click handling.

You want to keep receiving click events until a debounce has been hit (user has enabled/disabled all filters she wants to use). You can use the Buffer operator for this with a closingSelector which needs to emit a value when to close the buffer and emit the buffered values.

But leaves the issue how to know the current actual state.

UPDATE

It seems to be far easier to use the .scan operator to create your filterState array and debounce these.

const sources = document.querySelectorAll('input[type=checkbox]');
const clicksStream = Rx.Observable.fromEvent(sources, 'click')
  .map(evt => ({
        name:  evt.target.name,
        enabled: evt.target.checked
  }));

const filterStatesStream = clicksStream.scan((acc, curr) => {
  acc[curr.name] = curr.enabled;
  return acc
}, {})
.debounce(5 * 1000)

filterStatesStream.subscribe(currentFilterState => console.log('time to do something with the current filter state: ', currentFilterState);

(https://jsfiddle.net/crunchie84/n1x06016/6/)

like image 109
Mark van Straten Avatar answered Oct 27 '22 00:10

Mark van Straten