react-relay-network-modern icon indicating copy to clipboard operation
react-relay-network-modern copied to clipboard

How can environment.retain() be used with the network layer to disable relay garbage collection?

Open a-tokyo opened this issue 5 years ago • 4 comments

Description

How can we achieve using relay's environment.retain() to retain the data of a certain query and disable garbage collection?

Reasoning:

  • "If no component is rendering the local data and you want to manually retain it, you can do so by calling environment.retain()" - Relay docs

Example from relay documentation:

import {createOperationDescriptor, getRequest} from 'relay-runtime';

// Create a query that references that record
const localDataQuery = graphql`
  query LocalDataQuery {
    viewer {
      notes {
        __typename
      }
    }
  }
`;

// Create an operation descriptor for the query
const request = getRequest(localDataQuery);
const operation = createOperationDescriptor(request, {} /* variables */);


// Tell Relay to retain this operation so any data referenced by it isn't garbage collected
// In this case, all the notes linked to the `viewer` will be retained
const disposable = environment.retain(operation);


// Whenever you don't need that data anymore and it's okay for Relay to garbage collect it,
// you can dispose of the retain
disposable.dispose();

a-tokyo avatar Aug 20 '20 21:08 a-tokyo

I don't know if handling this on the network layer is a good idea.

How we'd know which operation we should retain or dispose? I mean, we can't retain all queries forever.

felippepuhle avatar Aug 21 '20 13:08 felippepuhle

@felippepuhle

We can implement it as a feature on top of the QueryRenderer or fetchWithMiddleware function (relay's fetch func) (eg: a ttl). The query is then retained for the ttl period. If the ttl provided to the QueryRenderer is larger than the TTL for the cache we'd never face the issues were the store was updated and the network cache overwrote these updates.

How we'd know which operation we should retain or dispose?

  • We only retain queries that have ttl -- we dispose the Observables after the ttl expires

Assume this scenario:

  • We have a connection of messages that we render via a query renderer
  • We update the connection of messages via subscriptions
  • The Messages Query renderer is unmounted AND mounted again
  • The Messages Query renderer now uses the old query data that is store in the network layer's cache and doesn't render the data updated by the subscriptions. Since relay modern garbage collected the connection along with it's updates.

Note: react-relay-offline provides good example use cases

a-tokyo avatar Aug 21 '20 17:08 a-tokyo

Oh, understood! Thanks for the explanation @A-Tokyo! It seems a really cool idea.

felippepuhle avatar Aug 21 '20 17:08 felippepuhle

@felippepuhle Awesome!! (:

I was checking the codebase to see how I can implement it but faced a couple of issues that I hope I can overcome soon:

  • How to pass data from the Query Renderer (the ttl option)
  • How to get an environment instance to call retain on

example of how the retain code should look like:

/* @flow */
import { createOperationDescriptor, getRequest } from 'relay-runtime';

/**
 * Retains data in the relay store to avoid garbage collection
 *
 * useful in case of subscriptions
 *
 * https://stackoverflow.com/questions/58022925/disable-relayjs-garbage-collection
 */
const retainInStore = ({
  getEnvironment,
  query,
  variables,
}: {
  getEnvironment: () => Environment,
  query: string,
  variables?: Object,
}): Disposable => {
  // Create a relay request from the query
  const request = getRequest(query);
  // Create an operation descriptor for the query
  const operation = createOperationDescriptor(request, variables);
  // Tell Relay to retain this operation so any data referenced by it isn't garbage collected and return the disposable
  return getEnvironment().retain(operation);
};

export default retainInStore;
if (opts.ttl) {
  const { dispose } = retainInStore({ getEnvironment, query, variables });
  const timeout = setTimeout(() => { dispose() }, ttl);
  // we can call clearTimeout(timeout); when we want to cleanup
}

a-tokyo avatar Aug 21 '20 19:08 a-tokyo