24

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?

TheWebweiser
  • 571
  • 4
  • 10
  • I'm currently having a similar problem, which option did you use in the end ? – nepjua May 26 '17 at 23:35
  • I'm using Option 2 at the moment. – TheWebweiser May 29 '17 at 12:25
  • 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 Jul 31 '17 at 21:54
  • @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._ [Docs](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) – TheWebweiser Aug 02 '17 at 10:44
  • @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 Aug 04 '17 at 02:39
  • @TheWebweiser Note that this is an important distinction because being unaware of this behavior could introduce some very subtle bugs. – Adam Donahue Aug 04 '17 at 02:41
  • @AdamDonahue you'd have to check the implementation of the `graphql` HoC, to see if this could be a problem... but I never had any issues with this approach so far. – TheWebweiser Aug 05 '17 at 07:12

6 Answers6

1

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)
C.Lee
  • 7,173
  • 6
  • 27
  • 42
  • That might seem to be just a matter of taste, but actually it is not: I personally like to implement my presentational components as pure functions, whenever possible. And my component doesn't and shouldn't care about which particular item is selected. Also the logic for which item is pre-selected might differ in different parts of the app, where the same presentational component is used for a single select list. – TheWebweiser Mar 28 '17 at 07:16
  • @TheWebweiser I like to have components as pure functions, too. The answer I posted is based on the assumption that the pre-selected logic would be the same across your app. The logic is put in the component because it is part of the component's behavior. But if you are planning to have a customizable logic for it, then I would choose Option 2 if the selectedItem redux state will be used by other components, and Option 4 otherwise. – C.Lee Mar 28 '17 at 18:10
1

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

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,
        }
      },
    }
  ),
);
xadm
  • 6,900
  • 3
  • 8
  • 17
0

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.

shah chaitanya
  • 411
  • 3
  • 8
0

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

streletss
  • 4,305
  • 4
  • 14
  • 34
0

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

Doğancan Arabacı
  • 3,702
  • 2
  • 14
  • 24