Search code examples
reactjspaginationgraphqlsymfony-3.3react-apollo

React-apollo v2 - Youshido GraphQlBundle - refetch two queries simultaneously


I'm trying to implement apollo to feed my customer's table component.

import CustomersTable from 'components/Customer/CustomersTable';

This table have to be filterable, sortable and paginated. I have 200.000 customers in the MySQL table. That's why filters, sorts and pagination are compute on server side. I need to query separately customers total count for pagination, and the customers list.

import GET_CUSTOMERS_PAGINATED_QUERY from './getCustomersPaginated.graphql';
import GET_CUSTOMERS_PAGINATED_COUNT_QUERY from './getCustomersPaginatedCount.graphql';

Unexpectedly, when the filtersInput are changed the refetch function is called twice. The first time with the correct new variables, and the second with initial variables. So the total count of customers is overwritten.

const initialFilters = {
  filterId: null,
  filterSiren: null,
  filterName: null,
  filterEmail: null,
};

const getCustomersPaginatedCountOptions = {
  name: 'customersPaginatedCount',
  options() {
    return {
      variables: {
        ...initialFilters,
      },
      fetchPolicy: 'network-only',
    };
  },
  props({ customersPaginatedCount }) {
    return {
      customersPaginatedCount: customersPaginatedCount,
    };
  },
};
const getCustomersPaginatedOptions = {
  name: 'customersPaginated',
  options({ offset, limit }) {
    return {
      variables: {
        offset: offset,
        limit: limit,
        ...initialFilters,
      },
      fetchPolicy: 'network-only',
    };
  },
  props({ customersPaginated }) {
    return {
      customersPaginated: customersPaginated,
    };
  },
};

These two queries are composed as advices here (for no errors):

@compose(
  graphql(GET_CUSTOMERS_PAGINATED_QUERY, getCustomersPaginatedOptions),
  graphql(GET_CUSTOMERS_PAGINATED_COUNT_QUERY, getCustomersPaginatedCountOptions),
)
export default class CustomersTableContainer extends React.PureComponent {

  state = {
    offset: this.props.offset,
    limit: this.props.limit,
    pageSize: 10,
    currentPage: 0,
    filters: initialFilters,
    currentOnFilterChangeTimeoutID: null,
  };

  constructor(props) {
    super(props);

    this.onCurrentPageChange = this.onCurrentPageChange.bind(this);
    this.onFiltersChange = this.onFiltersChange.bind(this);
  }

  onCurrentPageChange(newPage) {
    const { customersPaginated } = this.props;
    const { limit, filters } = this.state;

    customersPaginated.refetch({
      offset: newPage * limit,
      ...filters,
    });

    this.setState({ currentPage: newPage });
  }

  onFiltersChange(args) {
    const { customersPaginated, customersPaginatedCount } = this.props;
    const { limit } = this.state;

    const newFilters = Object.assign({}, initialFilters);
    for ( const i in args ) {
      newFilters['filter' + ucfirst(args[i].columnName)] = args[i].value;
    }

    customersPaginated.refetch({
      offset: 0 * limit,
      ...newFilters,
    });

    // --- >> THE REFETCH FUNCTION IS TRIGGERED TWICE HERE ! << ---
    customersPaginatedCount.refetch({
      ...newFilters,
    });

    // here 'test' is displayed once, so onFiltersChange is called once too as expected
    console.log('test');


    this.setState({
      currentPage: 0,
      filters: newFilters,
    });
  }

  render () {
    const { customersPaginated, customersPaginatedCount } = this.props;
    const { currentPage, pageSize } = this.state;

    if (customersPaginated.error) console.error( customersPaginated.error );
    if (customersPaginatedCount.error) console.error( customersPaginatedCount.error );


    return (
      <div>
        {(customersPaginated.error || customersPaginatedCount.error) && (
          <Typography color="error" gutterBottom>
            Une erreur est survenue.
          </Typography>
        )}
        <div>
          <CustomersTable
            customers={customersPaginated.customersPaginated}
            currentPage={currentPage}
            onCurrentPageChange={this.onCurrentPageChange}
            onFiltersChange={this.onFiltersChange}
            pageSize={pageSize}
            totalCount={customersPaginatedCount.customersPaginatedCount || 0}
          />
          {(customersPaginated.loading || customersPaginatedCount.loading) && <Loading />}
        </div>
      </div>
    );
  }

  static propTypes = {
    customersPaginated: PropTypes.object.isRequired,
    customersPaginatedCount: PropTypes.object.isRequired,
    offset: PropTypes.number.isRequired,
    limit: PropTypes.number.isRequired,
  };
}

My console logs on component load in an expected behavior :

{variables: {filterId: null, filterSiren: null, filterName: null, filterEmail: null}, operationName: "getCustomersPaginatedCount"
{variables: {filterId: null, filterSiren: null, filterName: null, filterEmail: null}, operationName: "getCustomersPaginated"

My console logs on a filter input change in an unexpected behavior :

{variables: {filterId: null, filterSiren: null, filterName: "example of customer name", filterEmail: null}, operationName: "getCustomersPaginated"
{variables: {filterId: null, filterSiren: null, filterName: "example of customer name", filterEmail: null}, operationName: "getCustomersPaginatedCount"
{variables: {filterId: null, filterSiren: null, filterName: null, filterEmail: null}, operationName: "getCustomersPaginatedCount"

getCustomersPaginated.graphql :

query getCustomersPaginated(
    $filterId: Int,
    $filterSiren: String,
    $filterName: String,
    $filterEmail: String,
    $offset: Int,
    $limit: Int
  ) {
    customersPaginated(
      filterId: $filterId,
      filterSiren: $filterSiren,
      filterName: $filterName,
      filterEmail: $filterEmail,
      offset: $offset,
      limit: $limit
    ) {
    id
    name
    siren
    email
    activity {
      id
      name
      shortName
      code
    }
    salesFollower {
      id
      username
      firstname
      lastname
      email
      initials
      enabled
    }
    customerGroup {
      id
      name
      code
      enabled
    }
    coreBusiness {
      id
      name
      codeApe
      codeNaf
    }
  }
}

getCustomersPaginatedCount.graphql :

query getCustomersPaginatedCount(
  $filterId: Int,
  $filterSiren: String,
  $filterName: String,
  $filterEmail: String
) {
  customersPaginatedCount(
    filterId: $filterId,
    filterSiren: $filterSiren,
    filterName: $filterName,
    filterEmail: $filterEmail,
  )
}

My environnement :

Front : reactjs with react-apollo

Back : PHP 7 with Symfony3 and Youshido\GraphQLBundle

I begun react this year and apollo this month. Maybe I'm not using refetch like I should, maybe there is a better way, or maybe there is a bug (I updated apollo-client-preset from 1.0.2 to 1.0.3 without seeing any changes). Maybe there's a solution on Youshido's side to be able to fetch the customers's list and customers's count in one query.

Thanks for your help.


Solution

  • In some situations the refetch function is not necessary. Thanks to @stelmakh for his help on this issue !!

    My new code : Child :

    import React from 'react';
    import PropTypes from 'prop-types';
    import { compose, graphql } from 'react-apollo';
    import { ucfirst } from 'utils/string';
    
    import CustomersTable from 'components/Customer/CustomersTable';
    import Typography from 'material-ui/Typography';
    
    import Loading from 'components/Loading/Loading';
    
    import GET_CUSTOMERS_PAGINATED_QUERY from './getCustomersPaginated.graphql';
    import GET_CUSTOMERS_PAGINATED_COUNT_QUERY from './getCustomersPaginatedCount.graphql';
    
    
    const getCustomersPaginatedCountOptions = {
      name: 'customersPaginatedCount',
      options({ variables }) {
        return {
          variables: variables,
          fetchPolicy: 'network-only',
        };
      },
      props({ customersPaginatedCount }) {
        return { customersPaginatedCount: customersPaginatedCount };
      },
    };
    const getCustomersPaginatedOptions = {
      name: 'customersPaginated',
      options({ variables }) {
        return {
          variables: variables,
          fetchPolicy: 'network-only',
        };
      },
      props({ customersPaginated }) {
        return { customersPaginated: customersPaginated };
      },
    };
    
    @compose(
      graphql(GET_CUSTOMERS_PAGINATED_QUERY, getCustomersPaginatedOptions),
      graphql(GET_CUSTOMERS_PAGINATED_COUNT_QUERY, getCustomersPaginatedCountOptions),
    )
    export default class CustomersTableContainer extends React.PureComponent {
    
      state = {
        currentOnFilterChangeTimeoutID: null,
      };
    
      constructor(props) {
        super(props);
    
        this.onCurrentPageChange = this.onCurrentPageChange.bind(this);
        this.onSortingChange = this.onSortingChange.bind(this);
        this.onFiltersChange = this.onFiltersChange.bind(this);
      }
    
      onCurrentPageChange(newPage) {
        const { onChange, variables } = this.props;
    
        onChange({
          currentPage: newPage,
          'offset': newPage * variables.limit,
        });
      }
    
      onFiltersChange(args) {
        clearTimeout(this.state.currentOnFilterChangeTimeoutID);
    
        const newCurrentOnFilterChangeTimeoutID = setTimeout(() => {
          const { onChange, variables } = this.props;
    
          const newVariables = Object.assign({}, variables);
    
          if (args.length > 0) {
            for ( const i in args ) {
              newVariables['filter' + ucfirst(args[i].columnName)] = args[i].value;
            }
          } else {
            for ( const i in newVariables ) {
              if (i.substr(0, 6) === 'filter') newVariables[i] = null;
            }
          }
    
          onChange({
            ...newVariables,
            'currentPage': 0,
            'offset': 0 * variables.limit,
          });
        }, 1000);
    
        this.setState({ currentOnFilterChangeTimeoutID: newCurrentOnFilterChangeTimeoutID });
      }
    
      render () {
        const { variables, customersPaginated, customersPaginatedCount } = this.props;
    
        if (customersPaginated.error) console.error( customersPaginated.error );
        if (customersPaginatedCount.error) console.error( customersPaginatedCount.error );
    
    
        return (
          <div>
            {(customersPaginated.error || customersPaginatedCount.error) && (
              <Typography color="error" gutterBottom>
                Une erreur est survenue.
              </Typography>
            )}
            <div>
              <CustomersTable
                customers={customersPaginated.customersPaginated}
                currentPage={variables.currentPage}
                onCurrentPageChange={this.onCurrentPageChange}
                onSortingChange={this.onSortingChange}
                onFiltersChange={this.onFiltersChange}
                pageSize={variables.pageSize}
                totalCount={customersPaginatedCount.customersPaginatedCount || 0}
              />
              {(customersPaginated.loading || customersPaginatedCount.loading) && <Loading />}
            </div>
          </div>
        );
      }
    
      static propTypes = {
        customersPaginated: PropTypes.object.isRequired,
        customersPaginatedCount: PropTypes.object.isRequired,
        variables: PropTypes.object.isRequired,
        onChange: PropTypes.func.isRequired,
      };
    }
    

    Parent :

    import React from 'react';
    
    import Typography from 'material-ui/Typography';
    import Button from 'material-ui/Button';
    import AddIcon from 'material-ui-icons/Add';
    
    import CustomersTableContainer from 'containers/Customer/CustomersTableContainer';
    
    export default class CustomersPage extends React.PureComponent {
    
      constructor(props) {
        super(props);
    
        this.state = {
          customersTableVariables: {
            filterId: null,
            filterSiren: null,
            filterName: null,
            filterEmail: null,
            pageSize: 10,
            currentPage: 0,
            offset: 0,
            limit: 10,
          },
        };
    
        this.onCustomersChange = this.onCustomersChange.bind(this);
      }
    
      onCustomersChange (newVariables) {
        this.setState({
          customersTableVariables: Object.assign({}, this.state.customersTableVariables, newVariables)
        });
      }
    
      render () {
        const { customersTableVariables } = this.state;
        return (
          <div>
            <Typography align="right">
              <Button fab color="primary" aria-label="add" href="/customer/new">
                <AddIcon />
              </Button>
            </Typography>
            <Typography type="title" gutterBottom>
              Clients/Prospects
            </Typography>
            <CustomersTableContainer variables={customersTableVariables} onChange={this.onCustomersChange} />
          </div>
        );
      }
    }