Как обновить хранилище Redux после запроса Apollo GraphQL

Я собираю список данных с graphql HOC, предоставляемый реагировать на apollo. Например:.

const fetchList = graphql(
  dataListQuery, {
    options: ({ listId }) => ({
      variables: {
        listId,
      },
    }),
    props: ({ data: { loading, dataList } }) => {
      return {
        loading,
        list: dataList,
      };
    }
  }
);

Я показываю список в управляемой группе переключателей, и мне нужно выбрать один из элементов по умолчанию. Элемент id выбранного элемента хранится в хранилище Redux.

Итак, вопрос в том, как обновить хранилище Redux (т.е. установить selectedItem) после успешного завершения запроса?

Некоторые варианты, которые пришли мне на ум:

Вариант 1

Должен ли я слушать действия APOLLO_QUERY_RESULT в моем редукторе Redux? Но это неловко, потому что тогда мне нужно будет слушать как APOLLO_QUERY_RESULT, так и APOLLO_QUERY_RESULT_CLIENT, если запрос уже выполнялся раньше. А также operationName prop присутствует только в действии APOLLO_QUERY_RESULT, а не в действии APOLLO_QUERY_RESULT_CLIENT. Поэтому мне нужно было бы проанализировать каждое действие APOLLO_QUERY_RESULT_CLIENT, чтобы знать, откуда оно взялось. Не существует ли простой и прямой способ идентифицировать действия с результатами запроса?

Вариант 2

Мне нужно отправить отдельное действие вроде SELECT_LIST_ITEM в componentWillReceiveProps например (используя recompose):

const enhance = compose(
  connect(
    function mapStateToProps(state) {
      return {
        selectedItem: getSelectedItem(state),
      };
    }, {
      selectItem, // action creator
    }
  ),
  graphql(
    dataListQuery, {
      options: ({ listId }) => ({
        variables: {
          listId,
        },
      }),
      props: ({ data: { loading, dataList } }) => ({
        loading,
        items: dataList,
      }),
    }
  ),
  lifecycle({
    componentWillReceiveProps(nextProps) {
      const {
        loading,
        items,
        selectedItem,
        selectItem,
      } = nextProps;
      if (!selectedItem && !loading && items && items.length) {
        selectItem(items[items.length - 1].id);
      }
    }
  })
);

Вариант 3

Должен ли я использовать клиента Apollo напрямую, введя его с помощью withApollo, а затем отправьте мое действие с помощью client.query(...).then(result => { /* some logic */ selectItem(...)}). Но тогда я потеряю все преимущества интеграции "реакция-аполлон", поэтому на самом деле не вариант.

Вариант 4

Должен ли я вообще не обновлять хранилище Redux после возвращения запроса? Потому что я мог бы просто реализовать селектор, который возвращает selectedItem, если он установлен, и если он не пытается получить его, просмотрев часть apollo в хранилище.

Ни один из моих вариантов не удовлетворяет меня. Итак, как бы я сделал это правильно?

Ответ 1

Я бы сделал что-то похожее на Вариант 2, но поместил методы жизненного цикла в фактический компонент. Таким образом, бизнес-логика жизненного цикла будет отделена от реквизита, унаследованного от Container.

Так что-то вроде этого:

class yourComponent extends Component{
    componentWillReceiveProps(nextProps) {
      const {
        loading,
        items,
        selectedItem,
        selectItem,
      } = nextProps;
      if (!selectedItem && !loading && items && items.length) {
        selectItem(items[items.length - 1].id);
      }
    }
  render(){...}
}

// Connect redux and graphQL to the Component
const yourComponentWithGraphQL = graphql(...)(yourComponent);
export default connect(mapStateToProps, mapDispatchToProps)(yourComponentWithGraphQL)

Ответ 2

должно быть достаточно использовать "реквизит", например:

const enhance = compose(
  connect(
    function mapStateToProps(state) {
      return {
        selectedItem: getSelectedItem(state),
      };
    }, {
      selectItem, // action creator
    }
  ),
  graphql(
    dataListQuery, {
      options: ({ listId }) => ({
        variables: {
          listId,
        },
      }),
      props: ({ data: { loading, dataList } }) => {
        if (!loading && dataList && dataList.length) {
          selectItem(dataList[dataList.length - 1].id);
        }
        return {
          loading,
          items: dataList,
        }
      },
    }
  ),
);

Ответ 3

Я бы прослушал изменения в componentDidUpdate и, когда они произошли, отправил действие, которое установит selectedItem в хранилище Redux

componentDidUpdate(prevProps, prevState) {

    if (this.props.data !== prevProps.data) {
        dispatch some action that set whatever you need to set
    }
}

Ответ 4

Я использую hoc, который является немного лучшей версией варианта 2. Я использую withLoader Hoc в конце compose.

const enhance = compose(
    connect(),
    graphql(dataListQuery, {
      options: ({ listId }) => ({
        variables: {
          listId,
        },
      }),
      props: ({ data: { loading, dataList } }) => ({
        isLoading:loading,
        isData:!!dataList,
        dataList
       }),
    }
  ),
withLoader
)(Component)

WithLoader hoc компонент рендеринга, основанный на двух свойствах isData и isLoading. Если isData истина, тогда это рендерит Wrapped Component, иначе рендеринг загрузчик.

    function withLoader(WrappedComponent) {
        class comp extends React.PureComponent {
           render(){
              return this.props.isData?<WrappedComponent {...this.props}/>:<Loading/>
           }
        }
    }

Я устанавливаю первый элемент dataList в методе Component componentWillMount. Компонент не монтируется, пока мы не получим dataList, который обеспечивается withLoader hoc.

Ответ 5

На мой взгляд, лучший подход - это создать слегка модифицированную и компонуемую версию hoc варианта 2, которая будет использоваться аналогично graphql hoc. Вот пример использования, который приходит на ум:

export default compose(
  connect(
    state => ({ /* ... */ }),
    dispatch => ({ 
      someReduxAction: (payload) => dispatch({ /* ... */ }),
      anotherReduxAction: (payload) => dispatch({ /* ... */ }),
    }),
  ),
  graphqlWithDone(someQuery, {
    name: 'someQuery',
    options: props => ({ /* ... */ }),
    props: props => ({ /* ... */ }),
    makeDone: props => dataFromQuery => props.someReduxAction(dataFromQuery)
  }),
  graphqlWithDone(anotherQuery, {
    name: 'anotherQuery',
    options: props => ({ /* ... */ }),
    props: props => ({ /* ... */ }),
    makeDone: props => dataFromQuery => props.anotherReduxAction(dataFromQuery)
  })
)(SomeComponent)

И самая простая реализация будет выглядеть примерно так:

const graphqlWithDone = (query, queryConfig) => (Wrapped) => {

  const enhance = graphql(query, {
    ...queryConfig,
    props: (props) => ({
      queryData: { ...( props[queryConfig.name] || props.data ) },
      queryProps: queryConfig.props(props),
    })
  })

  class GraphQLWithDone extends Component {
    state = {
      isDataHandled: false
    }

    get wrappedProps () {
      const resultProps = { ...this.props };
      delete resultProps.queryData;
      delete resultProps.queryProps;
      return {  ...resultProps, ...this.props.queryProps }
    }

    get shouldHandleLoadedData () {
      return (
        !this.props.queryData.error &&
        !this.props.queryData.loading &&
        !this.state.isDataHandled
      )
    }

    componentDidUpdate() {
      this.shouldHandleLoadedData &&
      this.handleLoadedData(this.props.queryData);
    }

    handleLoadedData = (data) => {
      if (!makeDone || !isFunction(makeDone)) return;
      const done = makeDone(this.wrappedProps)
      this.setState({ isDataHandled: true }, () => { done(data) })
    }

    render() {
      return <Wrapped {...this.wrappedProps}  />
    }
  }

  return enhance(GraphQLWithDone)
}

Даже при том, что я не пробовал этот псевдокод, у него нет тестов и даже не закончен, идея этого довольно проста и легка для понимания. Надеюсь, это кому-нибудь поможет

Ответ 6

В прошлом я сталкивался с подобной проблемой и выбирал что-то похожее на вариант 2. Если у вас есть и собственное хранилище с избыточностью, и собственное внутреннее хранилище apollo, состояние синхронизации между ними становится проблемой.

Я бы посоветовал избавиться от собственного магазина редуксов, если вы используете apollo. Если вы используете сервер gql и несколько остальных серверов одновременно, разделите данные логически и физически.

Как только вы решите использовать apollo в качестве "источника данных", диспетчеризация - это просто мутация, а получение состояния - просто запрос. Вы также можете фильтровать, сортировать и т.д. С запросами