relay: "Uncaught TypeError: Cannot read property 'identifier' of undefined" when calling `retain()` in Relay 8.0.0

This is in an example 1:1 from the official documentation https://relay.dev/docs/en/local-state-management

commitLocalUpdate(modernEnvironment, store => {
  const dataID = 'client:store';
  const record = store.create(dataID, 'Store');

  modernEnvironment.retain({
    dataID,
    variables: {},
    node: {selections: []},
  });
});

that causes the following error:

Uncaught TypeError: Cannot read property 'identifier' of undefined
    at RelayModernStore.retain (RelayModernStore.js:158)
    at RelayModernEnvironment.retain (RelayModernEnvironment.js:223)

I did a bare bones setup based on relay-examples todo app. Nothing fancy.

Not sure if related: https://github.com/facebook/relay/issues/2971

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 2
  • Comments: 21 (14 by maintainers)

Most upvoted comments

Thank you @jstejada! This works for me for now:

  /**
   * Retain a record with `id`.
   * This is not the recommended way of using the api!
   * See https://github.com/facebook/relay/issues/2997
   */
  const query = {
    kind: "Request",
    operation: {
      kind: "Operation",
      argumentDefinitions: [],
      selections: [],
    },
    params: {
      id,
      operationKind: "query",
    },
  };
  const operation = createOperationDescriptor(getRequest(query), {}, id));
  environment.retain(operation);

hey @cvle, unfortunately yes. The previous api was also only meant to be used on operations and this formalized it a bit more. I /think/ (but could be wrong) that because we use Flow internally that function wasn’t being type checked externally, and it’s an internal-ish api that isn’t well documented, so people figured out how to handcraft an input that would allow retaining a single record.

That being said, technically you could also handcraft an operationdescriptor to pass to retain without actually creating a query if you really need to, but that would not be the recommended way to use the api.

In any case, i think retaining individual records is probably a valid use case for managing local data, so we’ll consider adding a variant of this api for that use case.

thanks!

Could you update the docs to reflect how to use it to initialize a local store as the docs are very unclear on what to do. Thanks!

@jstejada thanks for the clarifications. I’ll try to describe the issue a bit better.

I have a selected, client extended, boolean field on the Document type. This field indicates if a given document is selected in a list view.

This is all great, until you navigate away from the list view and all React imposed retentions are released because elements (hooks/components) referencing the selected field are unmounted.

To avoid this loss of state, I want to retain the selected field on specific selected documents. This is exactly what I am trying in the code examples.

  • When selecting a document, I ask Relay to retain the selected field of that exact document and add the retention lock to the retainedDocuments map
  • When deselecting a document, I check the retainedDocuments map and dispose of the retention lock because I don’t care about it anymore

I hope this clarifies stuff from my side.

I am back with revelations. Thanks to help from @josephsavona, I’ve figured out a generic way to solve my problem in the above comment.

Relay offers missingFieldHandlers which allows it to populate/inject fields which are seemingly missing from the record store.

Empowering this feature, we can write type-safe retention requests for records which don’t exist on the root.

Following the Relay GraphQL Server Specification, we know for a fact that when the user requests something like this:

query {
  node(id: "globallyUniqueID") {
    ... on Document {
      title
    }
  }
}

we want a node/record with the globally unique ID behind the id argument. The following missing field handler allows such queries to get resolved:

// environment.js

import { Environment, ROOT_TYPE } from 'relay-runtime';
export const environment = new Environment({
  store,
  network,
  missingFieldHandlers: [
    {
      kind: 'linked',
      handle: function handle(field, record, variables) {
        if (
          // originates from root
          record?.__typename === ROOT_TYPE &&
          // name of the field equals to `node`
          field.name === 'node' &&
          // requests the record by ID
          field.args[0]?.name === 'id'
        ) {
          const arg = field.args[0];
          if (arg.kind === 'Literal') {
            return arg.value;
          }
          return variables[arg.variableName];
        }
        return undefined;
      },
    },
  ],
});

Refactoring the original document.js from the above comment like this:

// document.js

import { graphql, createOperationDescriptor, getRequest } from 'relay-runtime';
function selectDocument(id, selected) {
  let documentExists = false;
  environment.commitUpdate((store) => {
    const record = store.get(id);
    if (record) {
      documentExists = true;
      record.setValue(selected, 'selected');
    }
  });

  // retain selected documents so that the `selected` state is preserved throughout the app
  if (documentExists && selected && !retainedDocuments[id]) {
    const operation = createOperationDescriptor(
      getRequest(graphql`
        query documentRetainSelectedQuery($id: ID!) {
          node(id: $id) {
            ... on Document {
              selected
            }
          }
        }
      `),
      { id },
    );
    // checking the operation triggers the `missingFieldHandlers` and updates the store if necessary
    environment.check(operation);
    retainedDocuments[id] = environment.retain(operation);
  } else if (!selected && retainedDocuments[id]) {
    retainedDocuments[id].dispose();
    delete retainedDocuments[id];
  }
}

cleanly implements the wanted behaviour with all Relay benefits!

This

modernEnvironment.retain({
    dataID,
    variables: {},
    node: {selections: []},
  });

to this?

modernEnvironment.retain({
    request: {identifier: 0}, // HACK
    root: {
        dataID,
        variables: {},
        node: {selections: []},
    }
});

I’ve figured adding request: {identifier: 0} as a sibling of root would make it work, but this looks really hacky. What is the proper way of dealing with this and why are the docs not updated 😞

can you just change like this

environment.retain(operation.root) to evironment.retain(operation)