relay-tools / found-relay

Relay integration for Found
MIT License
276 stars 32 forks source link

Best way to use local client schema in prepareVariables? #526

Closed ersinakinci closed 4 years ago

ersinakinci commented 4 years ago

I'm trying to provide variables inside my prepareVariables function from Relay's store, but I'm not sure how to do this. Here was my first shot:

<Route
  ...
  prepareVariables={async (_params, { context: { relayEnvironment } }) => {
    const { searchQuery: { selectedFacets } } = await fetchQuery(
      relayEnvironment,
      graphql`
        query routes_RootQuery {
          ... on Query {
            __typename
          }
          searchQuery {
            selectedFacets {
              id
            }
          }
        }
      `
    );

    return { selectedFacets };
  }}
/>

(I'm hydrating my Relay store beforehand so that my searchQuery { selectedFacets } query returns something.)

This almost works. The main problem is that, for whatever reason, returning a promise causes prepareVariables to return nothing during SSR. I don't have any problems on my browser (SSR is running on Node 10, so async/await is supported.)

Another problem is that using fetchQuery on a local schema causes Relay compiler to fail. This workaround works, but it causes an annoying error. @sibelius suggested a different workaround using hooks, but that doesn't apply here since prepareVariables isn't a React component.

Lastly, I'm not sure of how to make the QueryRenderer re-render when there's a change to the variables that I'm getting from the store via prepareVariables. I want it such that it will re-run the query if the results of searchQuery { selectedFacets } changes in any way; or even better, if I could specify a comparison function to control exactly when it re-renders.

Has anyone else dealt with this use case? Any suggestions would be greatly appreciated!

ersinakinci commented 4 years ago

Another approach would be relayEnvironment.getStore().getSource().get('client:root:searchQuery'), but that feels brittle. Also, I don't think that it solves the problem of re-rendering QueryRenderer if searchQuery or any of its descendants changes.

taion commented 4 years ago

Related to https://github.com/relay-tools/found-relay/issues/297 – we don't currently support async prepareVariables, in the same way that we don't support async getQuery.

We could do it, but I don't really know of a good way to automatically "subscribe" to variables in the route. We don't really have a great concept of route "effects" that would let you do this in a way that's coupled to the route, rather than to the page component, though – but if I were to approach this, I'd subscribe to the relevant store entry in the component, and call router.replace(...) whenever things updated.

ersinakinci commented 4 years ago

Thanks for the suggestion, @taion.

I realized that because I'm using the pagination container, for my particular use case it makes more sense to rely on refetchConnection rather than trying to reload data at the router level. It's a three step process:

  1. Get the variables for the initial render at the route level. We don't need to subscribe to anything here, we just need to grab some data for the very first render. This approach uses the Relay environment and store directly. (Note the ... on Query workaround to get Relay compiler to be happy):
<Route
  ...
  Component={MyComponent}
  prepareVariables={(_params, { context: { environment } }) => {
    const request = getRequest(graphql`
      query routes_searchQuery_Query {
        ... on Query {
          __typename
        }
        searchQuery {
          query
          selectedFacets {
            id
            type
          }
        }
      }
    `);
    const operation = createOperationDescriptor(request);
    const {
      searchQuery: { query, selectedFacets },
    } = environment.lookup(operation.fragment, operation).data;

    return {
      query,
      selectedSelectors: selectedFacets,
    };
  }}
/>
  1. When creating the pagination container for MyComponent, the query includes the searchQuery field. Including this field subscribes MyComponent to any future changes and adds the field to the results included from the query:
const MyComponent = createPaginationContainer(
  MyComponentInner,
  {
    search: graphql`
      fragment MyComponent_search on Query {
        searchQuery {
          query
          selectedFacets {
            id
          }
        }
        ...
      }
    `
  }
)
  1. Inside MyComponentInner, I refetch the connection any time that searchQuery.query or searchQuery.selectedFacets changes:
const MyComponentInner = ({ relay, search: { query, selectedFacets } }) => {
  // Update search results when the query or filters change
  useEffect(
    () => {
      relay.refetchConnection(20, null, {
        query,
        facets: selectedFacets,
      });
    },
    [query, relay, selectedFacets]
  );
}

One could do something similar using a refetch container, as well, when dealing with non-connection queries.

taion commented 4 years ago

That ... on Query workaround was my workaround https://graphql.slack.com/archives/G95JXAN64/p1588702284062200 :p

But, yeah, if you're using a container that can refetch, your approach there looks good.

ersinakinci commented 4 years ago

Ahaha. props where props are due.

Thanks for the feedback :-)