DEV Community

Facundo Siracusa
Facundo Siracusa

Posted on

Apollo, React Adopt and Redux

Post originally posted in Medium:

https://medium.com/@facusiracusa/how-to-use-react-adopt-and-redux-connect-f410a17d2899?sk=98053d07ca94b447bee1eb10952fa28d

The other day I was in the need to rewrite an Apollo container, render props are nice, but you can also be lost in a callbacks hell if you have several mutations and query components to mix up. Talking with a work mate, he suggested me react-adopt, but even if he gave me an example I needed to go further and mix that with redux function connect.

So since I spent time trying different approaches to do that, surfing the web without much success, and trying to understand react-adopt examples, I thought to write this article and maybe speed up others work with a concrete real life example.
Consider this container with 2 mutations and 2 queries, the container needs to use one query or another depending on a url parameter, and also be connected to a redux store:

render() {
    const { match } = this.props;
    const isSellRequest = match.params.isSellRequest == 'true';
    return (
      <Mutation
        mutation={updateSellRequestStatus}
      >
        {(updateSellRequestStatus, { loading, ...mutation2Props }) => {
          const loadingMut2 = loading;
          return (
            <Mutation
              mutation={createSell}
              refetchQueries={[
                {
                  query: getSellsQuery,
                  variables: {
                    page: 0,
                    limit: SELLS_PAGE_LIMIT,
                    filter: ''
                  }
                }
              ]}
              awaitRefetchQueries
            >
              {(createSell, { loading, ...mutationProps }) => {
                const loadingMut = loading;
                const Comp = ({ data, loadingQ, other }) => (
                  <WrappedComponent
                    createSell={createSell}
                    updateSellRequestStatus=    {updateSellRequestStatus}
                    request={get(data, 'node', null) || null}
                    {...mutationProps}
                    {...this.props}
                    {...other}
                    loading={loadingQ || loadingMut || loadingMut2}
                    isSellRequest={isSellRequest}
                  />
                );
                if (isSellRequest) {
                  return (
                    <Query
                      query={sellRequestQuery}
                      variables={{
                        id: match && match.params.id
                      }}
                    >
                      {({ data, loading, ...other }) => {
                        return (
                          <Comp 
                             data={data}
                             other={other}
                             loadingQ={loading} 
                          />;
                        )
                      }}
                    </Query>
                  );
                } else {
                  return (
                    <Query
                      query={quoteRequestQuery}
                      variables={{
                        id: match && match.params.id
                      }}
                    >
                      {({ data, loading, ...other }) => {
                        return (
                          <Comp 
                            data={data} 
                            other={other} 
                            loadingQ={loading} 
                          />;
                        )
                      }}
                    </Query>
                  );
                }
              }}
            </Mutation>
          );
        }}
      </Mutation>
    );
export default connect(mapStateToProps)(CreateSellFromReqContainer);

Yes, I know, impossible to understand and debug it!! So, let's talk first about react-adopt.
Taking the description from its page, React Adopt is a simple method that composes multiple render prop components, combining each prop result from your mapper. For example you can use it like this example:

import { adopt } from 'react-adopt'

import { User, Cart, ShippingRate } from 'my-containers'

const Composed = adopt({
  cart: <Cart />,
  user: <User />,
  shippingRates: ({ user, cart, render }) => (
    <ShippingRate zipcode={user.zipcode} items={cart.items}>
      {render}
    </ShippingRate>
  )
})

<Composed>
  {({ cart, user, shippingRates }) => /* ... */ }
</Composed>

To see more examples, you can check its own github page https://github.com/pedronauck/react-adopt.

Ok, so first we will rewrite each part of the container individually to be used by adopt, let's see how we achieve this:

const UpdateRequestMutation = ({ render }) => (
  <Mutation mutation={updateSellRequestStatus}>
    {(updateSellRequestStatus, { loading, ...mutationProps }) =>
      render({ updateSellRequestStatus, loadingUpdate: loading, ...mutationProps })
    }
  </Mutation>
);

const CreateSellMutation = ({ render }) => (
  <Mutation
    mutation={createSell}
    refetchQueries={[
      {
        query: getSellsQuery,
        variables: {
          page: 0,
          limit: SELLS_PAGE_LIMIT,
          filter: ''
        }
      }
    ]}
    awaitRefetchQueries
  >
    {(createSell, { loading, ...mutation2Props }) =>
      render({ createSell, loadingCreate: loading, ...mutation2Props })
    }
  </Mutation>
);

const SellRequestQuery = ({ render, match }) => {
  const isSellRequest = match.params.isSellRequest == 'true';
  return (
    <Query
      query={sellRequestQuery}
      variables={{
        id: match && match.params.id
      }}
      skip={!isSellRequest}
    >
      {({ data, loading }) => render({ sellRequest: get(data, 'node', null) || null, loading })}
    </Query>
  );
};

SellRequestQuery.propTypes = {
  match: object,
  render: func
};

const QuoteRequestQuery = ({ render, match }) => {
  const isSellRequest = match.params.isSellRequest == 'true';
  return (
    <Query
      query={quoteRequestQuery}
      variables={{
        id: match && match.params.id
      }}
      skip={isSellRequest}
    >
      {({ data, loading }) =>
        render({ quoteRequest: get(data, 'node', null) || null, loadingQR: loading })
      }
    </Query>
  );
};

As we can see, we extracted our two queries and mutations separately, you can choose what to return from each component inside its return method. For example the SellRequestQuery will return these props, sellRequest and loading:

{({ data, loading }) => render({ 
  sellRequest: get(data, 'node', null) || null, 
  loading 
})}

that will be accessible in our composed component. Putting it all together with react-adopt results in a component like this:

const Composed = adopt({
   CreateSellMutation,
   UpdateRequestMutation,
   QuoteRequestQuery,
   SellRequestQuery
});

Now we have to mix all the properties and return our composed component in our container:

const CreateFromSellRequestContainer = props => {
  const { match } = props;
  const isSellRequest = match.params.isSellRequest == 'true';
  return (
    <Composed match={match}>
      {({
        CreateSellMutation: { createSell, loadingCreate },
        SellRequestQuery: { loading, sellRequest },
        QuoteRequestQuery: { loadingQR, quoteRequest },
        UpdateRequestMutation: { updateSellRequestStatus, loadingUpdate }
      }) => {
        return (
          <WrappedComponent
            isSellRequest={isSellRequest}
            loading={loading || loadingCreate || loadingUpdate || loadingQR}
            createSell={createSell}
            updateSellRequestStatus={updateSellRequestStatus}
            request={sellRequest || quoteRequest}
            {...props}
          />
        );
      }}
    </Composed>
  );
};

As we can see, the composed component receives four root properties, one for each item used in the adopt function that englobes the properties returned by each component in its return statement:

CreateSellMutation: { createSell, loadingCreate },
SellRequestQuery: { loading, sellRequest },
QuoteRequestQuery: { loadingQR, quoteRequest },
UpdateRequestMutation: { updateSellRequestStatus, loadingUpdate }

So basically, in our container we are taking all these properties and we are re formatting and passing them to the View component as we need it.

The last step is to return it and connect the container with the redux store:

export default connect(mapStateToProps(CreateFromSellRequestContainer);

I know this can be improved with the new hooks fashion, but it is not the scope of this article, it is just intended to help others to understand how react-adopt works and how it can be used to improve the readability and extensibility of a container created using apollo render props components.

Top comments (0)