Relay: Hydrate scalar fields on client

Created on 17 Aug 2015  Â·  25Comments  Â·  Source: facebook/relay

E.g. a DateTime field could be automatically turned into a Moment.js object.

Is this possible already and I just missed it in the docs?

enhancement help wanted legacy-core-api

Most helpful comment

@wincent I notice this issue is still closed. Does that mean there are no plans to work on this? Custom scalars are a pretty awesome feature of GraphQL and are currently super type-unsafe in Relay since they can't be typechecked by Flow due to #2162.

All 25 comments

This isn't currently supported, and you'd have to manually convert complex types e.g. in your components given the plain data from Relay. This is definitely a use-case worth considering.

I'll leave this open to gather feedback about the idea and examples of data types people may need.

The most common use case that comes to mind is indeed DateTime (that was the first custom scalar type we added in our own GraphQL server). Another example that would be useful in apps that deal with location data could be a geo point, for example hydrated to google.maps.LatLng.

Because these scalar types and their client-side representation are application specific, if this is going to be added to Relay it should probably be fully extendable, e.g. you could define a mapping from your GraphQL types to your JavaScript object builders. Transit-js might be a good place to look for an example on how to provide extension points for this.

I think this is could be an interesting idea. While it can also be implemented separately in the transport layer, e.g. with Transit, the GraphQL schema already has the necessary type information for hydrating types, meaning no extra metadata about the object type would need to be passed in the responses. This might save lots of bytes in apps that handle large amounts of data.

On the other hand this could add complexity to the framework, so maybe it would be worth it to consider if this could be done as a separate library outside Relay? A custom RelayNetworkLayer gets access to the runtime representation of each query in sendQueries and sendMutation. Maybe if the Babel plugin would store enough information about the types in the tree, the hydration of responses and serialization of requests could happen in the network layer using that information about the query?

Yeah, Transit-js was what I was thinking of when I wrote this issue.

This would be useful for having fields that represent plain JSON objects as well.

The transport layer is probably the wrong place to introduce this as it couples too much of the system together; in particular, the transpiler (to annotate types), the query representation (to expose this metadata), and any network implementation. It also increases the responsibility of a network layer, putting a burden on all implementors.

The best way to approach this is to change babel-relay-plugin to annotate the types of leaf fields, and add support in the payload processing phase (RelayQueryWriter) for deserializing "complex" values. For each leaf field, check if it has a non-scalar (Int, String, Boolean) type and if so, determine if there is an injected handler for that type. Pseudo code:

// In `RelayQueryWriter`
visitField(field, value) {
  var type = field.getType();
  if (type && !isScalarType(type)) {
    var handler = Relay.getInjectedTypeHandler(type);
    value = handler.deserialize(value);
  }
}

// define handler with:
Relay.injectTypeHandler("DateTime", {
  deserialize: (value) => ...,
  serialize: (value) => ...,
});

cc @yungsters

Edit: summarizing on offline discussion with @yungsters - given the complexity of the "ideal" solution I described above, we're inclined to _not_ add support for deserializing complex values right now. Doing so could conflict with other planned improvements such as querying local/device data and representing the cache as an immutable object.

We'd be curious to see how you approach this in a custom network layer and see about integrating that work down the road.

For me not super high as a) it'll be a month or so before I start
implementing relay and b) it's pretty easy to just hydrate in views for now.
On Tue, Aug 18, 2015 at 12:56 PM Joseph Savona [email protected]
wrote:

How would you rate the priority of this?

—
Reply to this email directly or view it on GitHub
https://github.com/facebook/relay/issues/91#issuecomment-132332431.

It's of very low importance to me as well - it's easy enough to handle this in my own code for now.

Thanks for the feedback. I'll close this for now but feel free to reopen later!

On using this pattern a bit, I find that there's a bit of a complication in that naively dehydrating data from Relay on-the-fly causes issues with breaking reference equality, which breaks using shallow equality for pure components.

I've taken to doing something like https://gist.github.com/taion/df126b9252825927b037 to avoid e.g. breaking referential equality on blob1 when e.g. blob2 changes because arg changes.

Someone mentioned seeing related performance issues with repeatedly instantiating Moment.js objects on the component.

Would it be possible to re-open this issue? I understand that this is unlikely to be a priority, especially given adequate user-side workarounds, but this is something that needs to be handled... somewhere.

Here's an additional problem.

Suppose I have a field that deserializes into a non-scalar type. If I use this field in multiple nested components, I have a choice of either deserializing this in each component, or doing this once at top-level and passing through the deserialized value.

Neither is ideal. If I deserialize at top-level, the non-scalar prop defeats the Relay container shouldComponentUpdate check, and I get excessive re-renders. If I deserialize in each component, then I'm doing work multiple times for no good reason.

I've talked this over with some people and have written a draft specification. We're not pursuing this internally, but if folks are motivated to get this working, we'd be happy to take a look at something similar to this proposed spec:

A method for serializing/deserializing scalar fields on the client

Given a scalar value sent from the server, like ‘1980-03-27’, you might want to interact with it as a JavaScript Date object on the client (eg. by creating a Date instance with new Date('1980-03-27')). What follows is a proposal to implement custom, runtime serializers/deserializers on the client.

Custom scalar types as deserializable

The idea is to consider all fields of type GraphQLScalarType as in need of a serializer/deserializer to be used with Relay. Given a field with this .graphql definition:

scalar ISODate

…which was, for instance, generated with this graphql-js schema:

const ISODate = new GraphQLScalarType({
  name: 'ISODate',
  serialize: date => date.toISOString().toJSON(),
  parseValue: isoString => new Date(isoString),
  parseLiteral: ast => new Date(ast.value),
});

…Relay should look up a custom serializer/deserializer at runtime to use to deal with values of this type.

Specification

  1. Map typenames to their serializer/deserializer using the following API:

    relayEnvironmentInstance.registerTypeHandler('ISODate', {deserialize, serialize});
    
  2. Check to see if you have a registered deserializer when you go to read a scalar value (see readRelayQueryData#_readScalar())

    • if no deserializer is registered, return the raw value

    • if a deserializer is registered use it to produce the value

  3. Memoize the deserialized values and clean up the memo when those fields are no longer used (see, for instance _updateGarbageCollectorSubscriptionCount in GraphQLStoreQueryResolver)
  4. When printing mutation queries, serialize the values for transport over the wire. See printArgument of printRelayOSSQuery.

Going to close this as Relay 2 will have support for arbitrary "handle" fields, for which you can register a handler in a way similar to what @steveluscher has suggested. As we're unlikely to undertake the work required to implement something like that on Relay 1, I'm going to mark this one as closed for now.

Thanks to everybody for their input!

So I tried to use this to implement a JSON scalar. I managed to hook up a handler like this:

fragment {
   config @__clientField(handle: "json"),
  ...
}

And pass handlerProvider to the Relay Environment, which looks like this:

import {ConnectionHandler} from 'relay-runtime';

function updateJSON(proxy, payload) {
  const record = proxy.get(payload.dataID);
  if (!record) {
    return;
  }
  const parsed = JSON.parse(record.getValue(payload.fieldKey));

  // Whatever the value you set for payload.handleKey is what the React component will see.
  record.setValue(parsed, payload.handleKey);
}

function handlerProvider(handle) {
  if (handle === 'json')
    return {update: updateJSON};

  if (handle === 'connection')
    return ConnectionHandler;
}

But this doesn't work, because Record.setValue() doesn't allow objects:

 RelayRecordProxy#setValue(): Expected a scalar value, got ...

This means we can also not parse Dates.

Should this issue be reopened? As @miracle2k mentioned, it seems it's not currently possible to handle the cases discussed in this thread (e.g. Date, JSON) via handlerProvider because of the scalar values restriction.

JSON "just works" even with Relay Modern w/graphql-type-json, BTW.

@taion I use the same setup as graphql-type-json, and it largely seemed to work at first, but if you try to update such a JSON field that is already in the store you'll get errors (for example, through a second query, or by reading a mutation payload). There is code in Relay Modern that tries to, I think, merge the new objects into the old ones to keep as much as possible their identify if they didn't change, and it recurses into the JSON structures.

I don't have the exact file/line saved unfortunately.

I'll reopen this as it might be interesting to explore how we could for example hydrate a custom URL scalar into an URL instance or a timestamp into a Date on the client.

I'm not sure about all the implications of this on Mutations (we should still validate that people don't set a deep structure on what should be a linked record) or other places this would have.

Thanks for the input here. We're currently going through old issues that appear to have gone stale (ie. not updated in about the last 3 months) because the volume of material in the issue tracker is becoming hard to manage. If this is still important to you please comment and we'll re-open this.

Thanks once again!

@wincent can you reopen ticket?
In our project we needed scalar type hydration. And as we understand the only solution is to allow to save in store JS objects, not only JSON types (strings, numbers, arrays, objects). Thanks!

@wincent I notice this issue is still closed. Does that mean there are no plans to work on this? Custom scalars are a pretty awesome feature of GraphQL and are currently super type-unsafe in Relay since they can't be typechecked by Flow due to #2162.

@wincent it would be great to have an update on this topic when you get the chance. Thanks.

I'm not working on Relay anymore.

So is this issue dead? This is still pretty much the only discussion I can find on this topic through Google. I have a bunch of "date" leafs in my GraphQL schema and it's a real pain to repeatedly have to convert them to Date objects in the views they are needed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bondanherumurti picture bondanherumurti  Â·  3Comments

MartinDawson picture MartinDawson  Â·  3Comments

fedbalves picture fedbalves  Â·  3Comments

sgwilym picture sgwilym  Â·  4Comments

HsuTing picture HsuTing  Â·  3Comments