Как добавить индикатор загрузки на каждый сетевой запрос Relay.js

В настоящее время я создаю веб-приложение Relay/React. Это было удивительно просто, за одним исключением. Я не смог понять, как я могу получить глобальное уведомление, когда какой-либо из моих компонентов делает сетевые запросы. Я планирую добавить счетчик в верхнюю часть своего приложения, когда когда-либо возникает сетевая активность, потому что некоторые из моих мутаций загружаются долго.

Это была моя попытка решить эту проблему, но она работает только при загрузке новой страницы маршрута.

function renderer(info)
{
    let {props, error, element} = info;
    if (error) {
        return (<ServerError errors={error}/>);
    } else {
        if (props) {
            return React.cloneElement(element, props);
        } else {
            return (<Loading />);
        }
    }
}


ReactDOM.render(
    <Router
        history={browserHistory}
        render={applyRouterMiddleware(useRelay)}
        environment={Relay.Store}
    >
        <Route
            path="/"
            queries={ViewerQuery}
            component={App}
        >
            <IndexRoute
                queries={ViewerQuery}
                component={Libraries}
                render={renderer}
            />
        <Route path="*" component={Error}/>

    </Route>
</Router>

В идеале я мог бы получить некоторый обратный вызов, который я мог бы передать своему компоненту приложения, который отображает верхние и нижние колонтитулы всех моих страниц. Любая помощь в этом будет принята с благодарностью. Я некоторое время бродил по Интернету, пытаясь найти хорошее решение для этого.


person AlexW.H.B.    schedule 12.10.2016    source источник


Ответы (3)


Вы также можете попробовать добавить собственный сетевой уровень ретрансляции, отображает компонент индикатора загрузки при каждом запросе. Я думаю, что основная проблема, которую следует учитывать для «глобального» индикатора загрузки, связана не только с дизайном индикатора, но и с его глобальным влиянием на пользовательский интерфейс. Будет ли он блокировать пользовательский интерфейс? Только одну его часть? Сместит ли он остальные элементы вверх/вниз? И т. д.

Тем временем вы можете сделать что-то вроде:

  handleSubmit(event) {
    event.preventDefault();
    this.setState({isLoading: true});

    this.props.relay.commitUpdate(
      new LoginMutation({
        email: this.refs.email.value,
        password: this.refs.password.value
      }), {
        onSuccess: response => {
          Auth.login(response.login.accessToken);
          const { location, router } = this.props;

          if (location.state && location.state.nextPathname) {
            router.replace(location.state.nextPathname)
          } else {
            router.replace('/')
          }
        },
        onFailure: transaction => {
          this.setState({hasError: true});
          this.setState({isLoading: false});
        }
      }
    );
  }

Приведенный выше фрагмент взят из здесь. Вы можете увидеть остальную логику в этом репо.

person hisa_py    schedule 13.10.2016

Вы можете создать собственный компонент счетчика в реакции, и в зависимости от состояния загрузки данных вы можете либо показать, либо скрыть счетчик.

Примером этого может быть -

Ваш компонент Spinner может быть таким:

    let SpinMe
    = (
        <div className="spinner-container">
            <div className="loader">
                <svg className="circular">
                    <circle className     = "path"
                        cx                = "50"
                        cy                = "50"
                        r                 = "20"
                        fill              = "none"
                        strokeWidth      = "3"
                        strokeMiterLimit = "10"
                    />
                </svg>
            </div>
        </div>
    );

Этот компонент счетчика должен иметь несколько более высокий z-index, чем другой компонент, чтобы во время загрузки пользователь не мог щелкать другие компоненты или взаимодействовать с другими компонентами.

Также в стилизации покажите прозрачный темный фон спиннера.

e.g

.spinner-container {

    position         : absolute;
    background-color : rgba(12, 12, 12, 0.61);
    width            : 100%;
    min-height       : 100%;
    background-size  : 100% 100%;
    text-align       : center;
    z-index          : 3;
    top              : 0;
    left             : 0;
}

Теперь ваш другой компонент, в котором вы хотите использовать счетчик, и в этом компоненте вы хотите сделать сетевой запрос.

import React from 'react';

class SpinTestComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        isLoading:false
    };
  }

  sendNetworkRequest(URL, dataToSend) {
      return $.ajax({
            type        : 'POST',
            url         : URL,
            data        : JSON.stringify(dataToSend),
            dataType    : 'json'
            });
  }

  componentDidMount() {
        const URL = "test url";
        const dataToSend = { param1:"val", param2:"val" };

        this.setState({isLoading:true});
        this.sendNetworkRequest(dataToSend)
            .then(
            () => {
                // success response now remove spinner
                this.setState({isLoading:false});
            },
            () => {

                // error response again remove spinner and 
                // show error message to end user
                this.setState({isLoading:false});
            });
    }

    render() {
        return (
                <div>
                { this.state.isLoading ? <SpinMe/> : null }
                    <div>
                        <h1>
                            Remaining Component structure 
                            or Jsx
                        </h1>
                        <p>
                            To be show after loading is done.
                        </p>
                    </div>
                </div>
               );
    }
}

export default SpinTestComponent;
person WitVault    schedule 12.10.2016

В итоге я расширил сетевой уровень Relay по умолчанию, чтобы я мог создавать некоторые события Flux, которые прослушивает мой основной корневой компонент. Это позволяет мне иметь всю загрузку и обработку сообщений об ошибках в одном месте, если я того пожелаю.

Вот мое окончательное решение. Не уверен, что это самое чистое, что может быть, но работает довольно хорошо.

import Relay from "react-relay";
import RelayMutationRequest from "react-relay/lib/RelayMutationRequest";
import AppDispatcher from "./AppDispatcher";

export default class CustomNetworkLayer extends Relay.DefaultNetworkLayer {

    constructor(uri, init)
    {
        super(uri, init);
    }

    networkLoadingEvent()
    {
        AppDispatcher.dispatch({
            actionType: 'LOADING'
        });
    }

    networkDoneLoadingEvent()
    {
        AppDispatcher.dispatch({
            actionType: 'DONE_LOADING'
        });
    }

    networkErrorEvent(error)
    {
        AppDispatcher.dispatch({
            actionType: 'ERROR',
            payload: error
        });
    }

    sendQueries(requests)
    {
        this.networkLoadingEvent();
        return Promise.all(requests.map(request => (
            this._sendQuery(request).then(
                result => result.json()
            ).then(payload =>
            {
                if (payload.hasOwnProperty('errors')) {
                    const error = CustomNetworkLayer.createRequestError(request, '200', payload);
                    this.networkErrorEvent(payload.errors[0].message);
                    request.reject(error);
                } else {
                    if (!payload.hasOwnProperty('data')) {
                        const error = new Error(
                            'Server response was missing for query ' +
                            `\`${request.getDebugName()}\`.`
                        );
                        this.networkErrorEvent(error);
                        request.reject(error);
                    } else {
                        this.networkDoneLoadingEvent();
                        request.resolve({response: payload.data});
                    }
                }
            }).catch(
                error =>
                {
                    this.networkErrorEvent(error);
                    request.reject(error)
                }
            )
        )));
    }

    sendMutation(request)
    {
        this.networkLoadingEvent();
        return this._sendMutation(request).then(
            result => result.json()
        ).then(payload =>
        {
            if (payload.hasOwnProperty('errors')) {
                const error = CustomNetworkLayer.createRequestError(request, '200', payload);
                this.networkErrorEvent(payload.errors[0].message);
                request.reject(error);
            } else {
                this.networkDoneLoadingEvent();
                request.resolve({response: payload.data});
            }
        }).catch(
            error =>
            {
                this.networkErrorEvent(error);
                request.reject(error)
            }
        );
    }


    /**
     * Formats an error response from GraphQL server request.
     */
    static formatRequestErrors(request, errors)
    {
        const CONTEXT_BEFORE = 20;
        const CONTEXT_LENGTH = 60;

        const queryLines = request.getQueryString().split('\n');
        return errors.map(({locations, message}, ii) =>
        {
            const prefix = (ii + 1) + '. ';
            const indent = ' '.repeat(prefix.length);

            //custom errors thrown in graphql-server may not have locations
            const locationMessage = locations ?
                ('\n' + locations.map(({column, line}) =>
                {
                    const queryLine = queryLines[line - 1];
                    const offset = Math.min(column - 1, CONTEXT_BEFORE);
                    return [
                        queryLine.substr(column - 1 - offset, CONTEXT_LENGTH),
                        ' '.repeat(Math.max(0, offset)) + '^^^',
                    ].map(messageLine => indent + messageLine).join('\n');
                }).join('\n')) :
                '';

            return prefix + message + locationMessage;

        }).join('\n');
    }

    static createRequestError(request, responseStatus, payload)
    {
        const requestType =
            request instanceof RelayMutationRequest ? 'mutation' : 'query';
        const errorReason = typeof payload === 'object' ?
            CustomNetworkLayer.formatRequestErrors(request, payload.errors) :
            `Server response had an error status: ${responseStatus}`;
        const error = new Error(
            `Server request for ${requestType} \`${request.getDebugName()}\` ` +
            `failed for the following reasons:\n\n${errorReason}`
        );
        error.source = payload;
        error.status = responseStatus;
        return error;
    }
}

затем в моем файле index.js я делаю это:

Relay.injectNetworkLayer(new CustomNetworkLayer("/graphql",
    {
        fetchTimeout: 35000, // timeout after 35 seconds
        retryDelays: [2000] // retry after 2 seconds
    }));

Краткое примечание: AppDispatcher — это просто диспетчер потока js, и я прослушиваю эти события в своем основном компоненте-оболочке.

надеюсь, что это поможет кому-то еще. Я, конечно, потратил слишком много времени на это.

Также спасибо всем, кто помог мне прийти к этому решению.

person AlexW.H.B.    schedule 13.10.2016