relay icon indicating copy to clipboard operation
relay copied to clipboard

Derived Fields - must specify parents fields in query in order to work

Open michael-haberzettel opened this issue 10 months ago • 5 comments

Hello,

I'm trying the new feature of derived fields. This approach sounds great to replicate the notion of Redux's selectors and have a computed field from server data.

The struggle: There is no example of how to use a "derived field" in a query. My initial assumption was: "when we specify a derived field in a query, the dependent fields in root fragment are indirectly requested" but it's not the case.

Example:

import {readFragment} from 'relay-runtime';

/**
 * @RelayResolver User.fullName: String
 * @rootFragment UserFullNameFragment
 */
export function fullName(key: UserFullNameFragment$key): string {
  const user = readFragment(graphql`
    fragment UserFullNameFragment on User {
        firstName
        lastName
    }
  `, key);
  return `${user.firstName} ${user.lastName}`;
}

// in query
graphql`
    query myQuery {
        viewer {
             # myAccount is of type User
             myAccount {
                   # we also need to specify firstName and lastName in order to work ! Or we need spreading the fragment UserFullNameFragment here.
                  fullName
             }      
        }
    }
`

Am I right on this behavior ? If it's the case, is it possible for relay to inject these fields by itself or maybe to throw an error at compilation ?

It can be error prone to request this derived field and get a null value without any clue on what's going on. I have wasted many hours understanding this subtlety.

michael-haberzettel avatar Apr 04 '25 08:04 michael-haberzettel

Thanks for the report. The compiler is supposed to insert the UserFullNameFragment fragment. Is it possible to put together a minimal example?

Sorry this has been wasting your time. Hopefully we can sort out what's going on here.

captbaritone avatar Apr 05 '25 04:04 captbaritone

Hello,

Here is the minimal reproduction: https://github.com/michael-haberzettel/relay-derived-field-requirements-sample. My example is based on https://github.com/relayjs/relay-examples.

I've changed the project newsfeed and I've updated the corresponding README.md to give the procedure. You can check my commits if you want, I think they are self explanatory.

EDIT: contrary to my initial assumption, you need to spread the fragment, it's not sufficient to request the required fields on the query.

michael-haberzettel avatar Apr 06 '25 19:04 michael-haberzettel

Are you able to reproduce the behavior from my project ?

michael-haberzettel avatar Apr 17 '25 07:04 michael-haberzettel

Can also confirm I'm also having the same (or very similar) issue!

I'm on relay-runtime 20.1.1 and react-relay 20.1.1. If i don't explicitly ask for the fields used in the resolver i get the following error in runtime.

For example if I don't explicitly query for lastName, but using lastName field in resolver, i get the following error:

{
    "kind": "missing_expected_data.log",
    "owner": "UserFullNameResolver",
    "fieldPath": "lastName"
}

My GraphQL query:

query User {
  firstName
  customFullName
}

My resolver:

/**
 * @RelayResolver
 * @onType User
 * @fieldName customFullName
 * @rootFragment UserFullNameResolver
 *
 * A users full name.
 */
import { graphql, readFragment } from 'relay-runtime'
import type { UserFullNameResolver$key } from '@/__generated__/UserFullNameResolver.graphql'

export function customFullName(userKey: UserFullNameResolver$key): string {
  const user = readFragment(
    graphql`
      fragment UserFullNameResolver on User {
        firstName
        lastName
      }
    `,
    userKey
  )
  return [user.firstName, user.lastName].filter(Boolean).join(' ')
}

I've been trying to follow the very basic example here: https://relay.dev/docs/guides/relay-resolvers/derived-fields/, i had to add @onType User or it wont compile, so i guess that's just an oversight from the docs?

danielstocks avatar Aug 25 '25 07:08 danielstocks

Thank you both for the reports and sorry for the excessive delay. I was able to use @michael-haberzettel's repro to identify the issue. We are not correctly processing resolver fragments during normalization when we convert the JSON response into our store format.

The bug is here: https://github.com/facebook/relay/blob/55dff358f4271f6f5aee1c59e3afcc45ac258879/packages/relay-runtime/store/RelayResponseNormalizer.js#L367

This will process the selections within the fragment, but not the fragment itself. That means we fail to capture the detail where we process information about if the concrete type we received actually implements the interface.

So this likely only manifests if you have a resolver with a root fragment on an abstract type, and no other fragment on that abstract type on that parent object.

I'll work on a fix.

captbaritone avatar Aug 25 '25 20:08 captbaritone

@captbaritone Any updates on this?

danielstocks avatar Jan 14 '26 13:01 danielstocks