24
votes

I'm fetching a list of data with the graphql HOC provided by react apollo. E.g.:

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

I'm displaying the list in a controlled radio button group and I need to select one of the items by default. The id of the selected item is kept in the Redux store.

So, the question is how to update the Redux store (i.e. set the selectedItem) after the query successfully returns?

Some options that came to my mind:

Option 1

Should I listen for APOLLO_QUERY_RESULT actions in my Redux reducer? But that is kind of awkward because then I would need to listen to both APOLLO_QUERY_RESULT and APOLLO_QUERY_RESULT_CLIENT if the query already ran before. And also the operationName prop is only present in the APOLLO_QUERY_RESULT action and not in APOLLO_QUERY_RESULT_CLIENT action. So i would need to dissect every APOLLO_QUERY_RESULT_CLIENT action to know where that came from. Isn't there an easy and straight forward way to identify query result actions?

Option 2

Should I dispatch a separate action like SELECT_LIST_ITEM in componentWillReceiveProps e.g (using 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);
      }
    }
  })
);

Option 3

Should I make use of the Apollo client directly by injecting it with withApollo and then dispatch my action with client.query(...).then(result => { /* some logic */ selectItem(...)}). But then I would loose all the benefits of the react-apollo integration, so not really an option.

Option 4

Should I not update the Redux store at all after the query returns? Because I could also just implement a selector that returns the selectedItem if it is set and if not it tries to derive it by browsing through the apollo part of the store.

None of my options satisfy me. So, how would I do that right?

6
I'm currently having a similar problem, which option did you use in the end ?nepjua
I'm using Option 2 at the moment.TheWebweiser
But will option two always work? I was under the impression that componentWillReceiveProps runs only when props change, and not necessarily on the first render. So if your props don't happen to change, this lifecycle method won't run, and your action won't be dispatched. Perhaps I misunderstand the semantics of that lifecycle method, though.Adam Donahue
@AdamDonahue The React documentation clearly states: Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render. DocsTheWebweiser
@TheWebweiser I think you misunderstood. I'm saying that if your initial set of props never changes, componentWillReceiveProps may not run. That, at least, is my interpretation of the following section of the documentation for this lifecycle method: "React doesn't call componentWillReceiveProps with initial props during mounting. It only calls this method if some of component's props may update." It seems pretty clear, then, that option 2 above is incomplete. Or can be unless you somehow force a prop change.Adam Donahue

6 Answers

1
votes

I would do something similar to Option 2, but put the life cycle methods into the actual Component. This way the business logic in the life cycle will be separated from the props inherited from Container.

So something like this:

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)
1
votes

I would listen to changes in componentDidUpdate and when they happened dispatch an action that will set selectedItem in Redux store

componentDidUpdate(prevProps, prevState) {

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

there should be sufficient to use 'props', sth like:

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,
        }
      },
    }
  ),
);
0
votes

I use hoc which is slightly better version of option 2. I use withLoader Hoc at end of compose.

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

WithLoader hoc render component based on two Props isData and isLoading. If isData true then it renders Wrapped Component else render loader.

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

I set dataList's first item in Component's componentWillMount method. The component doesn't mount untill we get dataList which is ensured by withLoader hoc.

0
votes

In my opinion, the best approach to take is to create a slightly modified and composable version of hoc of Option 2, that will be used similarly to graphql hoc. Here is an example usage that comes to mind:

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)

And the simplest implementation would be something like this:

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)
}

Even thought I haven't tried this pseudocode out, it has no tests and not even finished, the idea behind it is pretty straightforward and easy to grasp. Hope it'll help someone

0
votes

I faced similar issue in past and choose something similar to option 2. If you have both your own redux store and apollo's own internal store, syncing state's between them becomes an issue.

I would suggest to get rid of your own redux store if you are using apollo. If you rely on gql server and some rest servers in the same time, separate data logically and physically.

Once you decide to use apollo as your 'data source', dispatching is just mutation and getting state is just querying. You can also filter, sort etc with queries