This is a bit of a usage question (and might be on the wrong repo), but I think it might be interesting enough to warrant further discussion.
I'm looking to fetch data from my GraphQL server based on dynamic remote data.
For a simple example, imagine I'm rendering a list of todos, but the fields I want to render are different based on whether the todo in question is active or completed. Imagine a React component like:
function Todo({ todo }) {
return (
<div>
{todo.text}
{todo.status === 'completed' ?
<CompletedTodoDetails todo={todo} /> :
<ActiveTodoDetails todo={todo} />}
</div>
);
}
I can think of a couple of ways to define my GraphQL schema to allow this.
The most straightforward way is to have something like:
interface Todo { ... }
type ActiveTodo implements Todo { ... }
type CompletedTodo implements Todo { ... }
And then just define fragments on ActiveTodo or CompletedTodo as needed.
However, this is fairly limited – it's very awkward for defining conditionals on more than one field, requires the conditional to be like an enum, and is frankly confusing in the context of Relay, if I want to have this type be a node, but somehow have the concrete type of the node change under me.
Another option seems to be to do something like:
type Todo {
self(status: String!): Todo
...
}
Then for the above example, I could write a fragment like
fragment on Todo {
name
self(status: "completed") {
${CompletedTodoDetails.getFragment('todo')}
}
self(status: "active") {
${ActiveTodoDetails.getFragment('todo')}
}
}
This gives me a lot more flexibility over how to do the conditional. It still seems a bit awkward, though.
It seems like it'd be nice to do this with some sort of custom directive, but handling the custom directive myself seems to not admit a straightforward syntax, especially if I wanted to define that custom directive on a fragment.
Am I missing anything here?
To clarify why I feel the type system is not quite the right solution – imagine I had a Widget type where I wanted to do things conditionally based on both size and color. I'd then need to declare e.g. BigBlueWidget, SmallBlueWidget, BigRedWidget, SmallRedWidget, &c. If I had a fragment that I only wanted to fetch conditional on the widget being blue, I'd have to do something like
fragment on BigBlueWidget { ... }
fragment on SmallBlueWidget { ... }
With something like a recursive self field (or maybe something directive-based?), I could instead do
self(color: "blue") { ... }
I'm not sure I follow the concern. I would suggest:
type Todo {
text: String
status: TodoStatusEnum
}
enum TodoStatusEnum {
open
completed
}
or if status is truly a boolean:
type Todo {
text: String
isCompleted: Boolean
}
Suppose instead of text, we had veryLongDescription. Further suppose I fetch a list of all todos, but only want to show (and thus fetch) veryLongDescription for todos that are completed.
If I query for
todos {
status
veryLongDescription
}
I end up fetching veryLongDescription even for todos that are not in the completed state. Instead right now I'm doing something like:
todos {
ifCompleted: if($status: COMPLETED) {
veryLongDescription
}
}
Ah, I understand. There isn't a capability for this right now
Well, there is! I started using that pattern (if(status: Status): Todo) and it's been working quite well so far.
Does this seem "right", though? Or should there be a better way (I guess at the GraphQL language level?) to do this?
Yeah, I think that's a pretty cool "hack" to get the behavior you're looking for! The only real caveat that I'm sure you're aware of is that you actually have to define an if field on Todo and include the arguments you want. I'm glad that's working well for you.
There have been some rough ideas tossed around about more general ways to do this via additions to the language, but none have passed muster yet. If you have concrete proposals on this front, that would be great to see.
It's just tricky, because conditions like these really have to be defined in user space.
My first thought here was that it'd be nice to be able to do this with decorators, like:
fragment on Todo {
name
veryLongDescription @if(status: "completed")
}
and somehow expose that via e.g. the info arg to the resolver.
The issue, though, is that the natural place to attach something like this (especially if you're building your UI Relay-style) is it's really fragments that are conditional, not fields, in which case this is just clunky/unworkable.
I'll update this issue if I run into problems with my current approach that could be addressed well with language-level support.
I had a similar situation, and took a different approach by adding new schema fields at a higher level of abstraction on top of the data.
Context: our application has configurable/dynamic data structure, and there is a lot of meta-programming. Most objects have common "core" fields, and customer-specific configurable fields. At the start of working with GraphQL, the mandatory typing system was frustrating because we didn't want to fall back to having a generic "attributes: JSON" field. The "aha" moment was when we started treating GraphQL as a strongly typed _API_ to the data, instead of a direct _proxy_ to the data. So we started generating dynamic strongly-typed schemas which translate all meta-level magic into concrete fields our opinionated front-end can use.
I try to make the front-end as unaware, logic-less, and mundane as possible. So I push all the thinking and decisions to the server by creating higher-level _API_ fields within the schema. Using the simplified TODO example above, the schema would be -
type Todo {
name: String
status: TodoStatusEnum
lastModified: String
veryLongDescription: String
openDetails {
lastReviewed: String
}
completedDetails {
veryLongDescription: String
completedDate: String
someExpensiveField: String
}
}
Then during the resolve phase, I’d ensure to send completedDetails _only_ if status is completed, otherwise it’s empty. A few observations on this method -
completedDetails is there, render it" not "should completedDetails be there?”.veryLongDescription regardless of the status. In those cases, those specific data fields are exposed on the object itself so we can query them directly (as is in the example schema above). As I mentioned, the new completedDetails for is a higher-level abstraction - it exists _in addition_ to normal fields, it doesn’t replace them. Most of the time it just aggregates/proxies data from the main fields, just first it goes through the decision gate.completedDetails over self(status: "completed") { because the former feels more self-documenting, and the latter seems to require more cognitive load about what I should do with that nested call. The latter provides more flexibility, but IMO increases ambiguity and fragility, so I treat that as an implementation details and hide behind a higher-level abstraction.Ironically, I love GraphQL for the flexibility it provides, and at the same time try to remove it/abstract it away from the userspace as much as possible. I don’t want the client code to make many decisions, and ideally no complex/business decisions. I do this by creating domain-specific building blocks in the schema, and using GraphQL as more like an API call and less like a data proxy.
I should note that, as I'm working with Relay, it's very convenient to use the if-pattern, because I want the child fragments to have the same type. The way things look is that I have components that will be rendered in certain cases, but whether they are rendered is controlled by their parents.
I also have a few places where I need to select on e.g. a combination of statuses, so it's been convenient for me to have the additional control there.
Closing sins like there are no new input in this issue for the last 2 years.
Most helpful comment
I had a similar situation, and took a different approach by adding new schema fields at a higher level of abstraction on top of the data.
Context: our application has configurable/dynamic data structure, and there is a lot of meta-programming. Most objects have common "core" fields, and customer-specific configurable fields. At the start of working with GraphQL, the mandatory typing system was frustrating because we didn't want to fall back to having a generic "attributes: JSON" field. The "aha" moment was when we started treating GraphQL as a strongly typed _API_ to the data, instead of a direct _proxy_ to the data. So we started generating dynamic strongly-typed schemas which translate all meta-level magic into concrete fields our opinionated front-end can use.
I try to make the front-end as unaware, logic-less, and mundane as possible. So I push all the thinking and decisions to the server by creating higher-level _API_ fields within the schema. Using the simplified TODO example above, the schema would be -
Then during the
resolvephase, I’d ensure to sendcompletedDetails_only_ if status iscompleted, otherwise it’s empty. A few observations on this method -completedDetailsis there, render it" not "shouldcompletedDetailsbe there?”.veryLongDescriptionregardless of thestatus. In those cases, those specific data fields are exposed on the object itself so we can query them directly (as is in the example schema above). As I mentioned, the newcompletedDetailsfor is a higher-level abstraction - it exists _in addition_ to normal fields, it doesn’t replace them. Most of the time it just aggregates/proxies data from the main fields, just first it goes through the decision gate.completedDetailsoverself(status: "completed") {because the former feels more self-documenting, and the latter seems to require more cognitive load about what I should do with that nested call. The latter provides more flexibility, but IMO increases ambiguity and fragility, so I treat that as an implementation details and hide behind a higher-level abstraction.Ironically, I love GraphQL for the flexibility it provides, and at the same time try to remove it/abstract it away from the userspace as much as possible. I don’t want the client code to make many decisions, and ideally no complex/business decisions. I do this by creating domain-specific building blocks in the schema, and using GraphQL as more like an API call and less like a data proxy.