React - 在复杂的应用程序中巧妙地控制异步调用而没有任何副作用

时间:2017-09-16 16:25:09

标签: javascript ajax reactjs

React - Controlling multiple Ajax Calls提出的codelayer1提出的解决方案存在直接在动作创建者中访问状态的问题 - 反模式。

所以,如果我不能访问我的动作创建者内部的状态,我将会在我的组件中监听batchRequestCompleted状态。当组件prop batchRequestCompleted将变为true(表示先前的请求已完成)时,我将检查是否有任何待处理的请求。如果是,我将派遣行动处理下一个请求。所以基本上saga调用动作,反过来修改状态。一旦修改了状态,就会从组件中调度另一个处理进一步请求的操作。通过这种方式,saga永远不会访问该状态。

上面的解决方案听起来不错,但却以Route change before action creator completes中提到的问题为代价。也就是说,如果有人在清除队列之前导航到不同的路由,那么队列内的请求会发生什么。

我是否可以解决React - Controlling multiple Ajax Calls中提到的问题,而无需访问动作创建者内部的状态,也无需将组件重新放入图片中以便调度动作以清除待处理队列。

注意:我创建了一个新问题,因为React - Controlling multiple Ajax Calls中提到的问题已经解决,但有副作用,这个问题主要集中在达到解决方案,以消除这种副作用。

1 个答案:

答案 0 :(得分:1)

我做了一些回购github.com/adz5a/so-stream-example来说明我将如何解决你的问题。

此仓库使用两个库xstreamrecompose。前者提供ObservableStreams及其运算符的实现,后者使用React连接它。

在所有事情之前都需要一个概念:ES Observables。在this等文章中深入介绍了这些内容(我强烈建议您阅读和收听Ben Lesh过去关于此主题的文章/会谈)。

Observabes是一个懒惰的原语,用于随时间推移对值进行建模。在JS中,我们有另一个用于执行异步的原语:Promises。那些模型eventual value or error因此不是懒惰而是渴望。对于React组件(或更一般的UI),我们对lazyness感兴趣,因为事情可能出错:用户可能想要中断长时间运行的进程,它可能崩溃,更改路由等。 ..

那么,我们如何解决您的问题:通过用户交互控制可以中断(获取大量行)的长时间运行流程?

首先,用户界面:

export class AnswerView extends React.Component {
    static propTypes = {
        // called when the user make a batch 
        // of request
        onStart: PropTypes.func.isRequired,
        // called when you want to stop the processing
        // of requests ( when unmounting or at the request
        // of the user )
        onStop: PropTypes.func.isRequired,
        // number of requests completed, 0 by default
        completedRequests: PropTypes.number.isRequired,
        // whether it's working right now or not
        processing: PropTypes.bool.isRequired
    };
    render () {

        // displays a form if no work is being done,
        // else the number of completed requests
        return (
            <section>
                <Link to="/other">Change Route !</Link>
                <header>
                    Lazy Component Example
                </header>
                {
                    this.props.processing ?
                        <span>{"requests done " + this.props.completedRequests}<button onClick={this.props.onStop}>Stop !</button></span>:
                        <form onSubmit={e => {
                                e.preventDefault();
                                this.props.onStart(parseInt(e.currentTarget.elements.number.value, 10));
                            }}>
                            Nb of posts to fetch<input type="number" name="number" placeholder="0"/>
                            <input type="submit" value="go"/>
                        </form>
                }
            </section>
        );

    }
    componentWillMount () {
        console.log("mounting");
    }
}

非常简单:一个表单,其中包含要执行的请求数量的输入(可以在表组件上复选框...)。

它的道具如下:

  • onStart:fn采用所需的数字
  • onStop:fn没有任何args和我们想停止的信号。可以挂钩到按钮,或者在这种情况下,componentWillUnmout
  • completedRequests:整数,计算已完成的请求,0。
  • 处理:布尔值,表示工作是否正在进行中。

这本身并没有太大作用,所以让我们介绍recompose。其目的是通过HOC 增强组件。我们将在此示例中使用mapPropsStream帮助程序。

注意:在这个答案中,我可以互换使用stream / Observable,但在一般情况下不是这样。流是具有operators的Observable,允许将发射的值转换为新的Observable。

对于React组件,我们可以使用标准api来观察它的道具:在componentWillMount中第一个,然后在componentWillReceiveProps。我们还可以发信号通知componentWillUnmount不再有道具。我们可以构建以下(marble)图表:p1--p2--..--pn--|(管道表示流的完成)。

增强器代码发布在下面,带有注释。

需要理解的是,所有带流的内容都可以像信号一样接近:通过将所有内容建模为流,我们可以确保通过发送适当的信号,我们可以获得所需的行为。

export const enhance = mapPropsStream(prop$ => {

    /*
     * createEventHandler will help us generates the callbacks and their
     * corresponding streams. 
     * Each callback invocation will dispatch a value to their corresponding
     * stream.
     */

    // models the requested number of requests
    const { handler: onStart, stream: requestCount$ } = createEventHandler();
    // models the *stop* signals
    const { handler: onStop, stream: stop$ } = createEventHandler();

    // models the number of completed requests
    const completedRequestCount$ = requestCount$.map( n => {

        // for each request, generate a dummy url list
        const urls = Array.from({ length: n }, (_, i) => `https://jsonplaceholder.typicode.com/posts/${i + 1}` );

        // this is the trick : we want the process to be aware of itself when
        // doing the next operation. This is a circular invocation so we need to
        // use a *proxy*. Note : another way is to use a *subject* but they are
        // not present in __xstream__, plz look at RxJS for a *subject* overview
        // and implementation.
        const requestProxy$ = xs.create();

        const count$ = requestProxy$
        // a *reduce* operation to follow where we are
        // it acts like a cursor.
            .fold(( n ) => n + 5, 0 )
        // this will log the current value
            .debug("nb");

        const request$ = count$.map( n => Promise.all(urls.slice(n, n + 5).map(u => fetch(u))) )
            .map(xs.fromPromise)
            .flatten()
            .endWhen(xs.merge(
        // this stream completes when the stop$ emits
        // it also completes when the count is above the urls array length
        // and when the prop$ has emitted its last value ( when unmounting )
                stop$,
                count$.filter(n => n >= urls.length),
                prop$.last()
            ));



        // this effectively activates the proxy
        requestProxy$.imitate(request$);

        return count$;

    } )
        .flatten();

    // models the processing props,
    // will emit 2 values : false immediately,
    // true when the process starts.
    const processing$ = requestCount$.take(1)
        .mapTo(true)
        .startWith(false);

    // combines each streams to generate the props
    return xs.combine(
        // original props
        prop$,
        // completed requests, 0 at start
        completedRequestCount$.startWith(0),
        // boolean indicating if processing is en route
        processing$
    )
        .map(([ props, completedRequests, processing ]) => {

            return {
                ...props,
                completedRequests,
                processing,
                onStart,
                onStop
            };

        })
    // allows us to catch any error generated in the streams
    // very much equivalent to the new ErrorBoundaries in React
        .replaceError( e => {
            // logs and return an empty stream which will never emit,
            // effectively blocking the component
            console.error(e);
            return xs.empty();
        } );

});

export const Answer = enhance(AnswerView);

我希望这个答案不会(太)令人费解,随时可以提出任何问题。

作为旁注,经过一些研究后你可能会注意到processing布尔值并没有真正用在逻辑中,而只是帮助UI知道发生了什么:这是一个比将一些状态附加到组件的this更清洁。