-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Relay and Redux #464
Comments
It seems like Redux and Relay are trying to accomplish the same thing, managing your component data. In fact, Redux's @connect decorator seems very similar to Relay's containers. You're basically attaching a data source to your component. In Redux that data source is your reducers and in Relay it's your server and probably a local cache. |
I think they address two very different parts of "managing your component data". Redux is simply about transforming your local state via actions and reducers. Relay is about creating declarative dependency specifications that a hierarchy of components can collect and combine automatically to fetch data from a server without overfetching or underfetching. The traditional problem that Relay was trying to solve was that a component had to know what data its children required, then had to get it from the local state (assuming it's already been retrieved from the server) and then pass it down to its children via props. If a child component's data needs changed, you needed to change the parent object and possibly the parent's parent, depending on how you'd organized things. With Relay, the children declare the dependencies and the parent collects and combines them automatically without needing to be aware of what those dependencies are. Then it asks the server for exactly the data it needs. There's a topic in the reselect project discussing automatic generation of selector dependencies. If you did that, you could probably leverage the data to do something like Relay with Redux, or perhaps feed that data directly to Relay (haven't looked at the Relay code yet, but will soon). |
Right but Relay has mutations which is also a way of transforming your state. I don't think it's a good idea to have two different systems managing your state. For example, with a todo app, you would have to write an addTodo action for Redux and an addTodo mutation for Relay. This doesn't seem like a good design to me. I could see Redux being used as a component state manager and a cache layer for Relay. In the blog post they mention investigating offline support in the future for Relay and I wonder if Redux could plug into that and enable time travel with Relay. |
Actually, we might be part of the way there already. Relay already allows custom network layers: If there was a way to handle GraphQL client-side then we could implement a Redux network layer that would intercept those requests and work its magic. |
Injecting Redux into the Flux Store part of Relay/GraphQL feels directionally right. There are a lot of advantages to using Redux with Relay I can think of:
Pretty exciting stuff after typing it out... |
@CooCooCaCha I think integrating at the network layer might be too late into the pipeline. We are in the early phases (mainly because of the focus on getting Relay open sourced) of exploring ways to expose local data via GraphQL queries and fields. We are definitely inspired by the work on Redux and would like to see how we could fulfill queries for local state from in-memory stores or maybe even native device data sources. |
One nice thing that Redux support really seamlessly is universal apps, so making this conversion work would make working with universal Relay apps much easier. I view Relay and Redux as two halves of the problem:
There are three really difficult and valuable things that Relay accomplishes:
I can see two barriers to integrating relay & redux:
Note that I do not think that the valuable part of Relay is integration into React. What I wonder is, could way separate out the three highly valuable parts of Relay, and make them function on top of an immutable data store. If we could do that, then integration into Redux would be very simple. We could then experiment with alternative ways of structuring the React portion of the application. |
This is the (currently unrealized) intent for the RelayStoreData internal module. In theory, we could pass this down through your app via the RootContainer. |
That would be an excellent first step towards redux integration. |
Hi.. didn't really look into it, but found this repo for an example https://github.com/barbuza/react-redux-relay any progress on relay support |
Also relevant: https://github.com/gyzerok/adrenaline |
Am Freitag, 18. September 2015 schrieb Dan Abramov :
|
@ALL I highly need any feedback about https://github.com/gyzerok/adrenaline. I'm stuck a little bit now about how to move on. |
@gyzerok could you amend your readme with a quick overview of your approach? How are you matching stateless Redux with stateful Relay? |
@wmertens It does it by not using relay at all. It uses the lower level protocol instead. Adrenaline is in fact a GraphQL client, rather than a relay implementation. It's great that people are starting to experiment with alternative ways of building relay-like functionality 👍 |
I agree that this is a very neat approach. The approach here should be similar to how Flux inspired many libraries (including Redux!). From what I understand, very few people use the Flux dispatcher directly, which might be overkill for many problems. I see the Relay ecosystem also turning up the same way. There could be a "redux-relay" project (Adrenaline?) that integrates GraphQL into the workflow in the most appropriate way. I wish one could directly use parts of Relay, instead of writing everything the whole thing from scratch. 💯 |
@wmertens I do not quite understand you. Redux is statefull to, but state live somewhere inside Redux. In Relay state also live somewhere inside. An in Adrenaline state live somewhere inside. Mb I do not get your question right? |
@bbirand A lot of relay's infrastructure is reusable without starting from scratch, it's just mostly undocumented. For example, any efficient implementation of relay-like principles will need to use something like the babel-relay-plugin which is totally reusable for non-relay GraphQL implementations. |
@ForbesLindesay Do you understand principles behind babel-relay-plugin? If so, can you explain it a little more? |
What adrenaline does currently is run a single query on the server side, put it's result in a cache, and then re-run the query against that cache with an identical GraphQL schema but where the Part of the problem with this, is that due to aliasing and arguments, it's impossible to draw a proper correlation between the responses from graphql and the underlying data that should be cached. For example, consider the following from the docs:
You get a response like: {
"user": {
"id": 4,
"name": "Mark Zuckerberg",
"smallPic": "https://cdn.site.io/pic-4-64.jpg",
"bigPic": "https://cdn.site.io/pic-4-1024.jpg"
}
} What you probably end up with is a cache which looks like: {
"user": {
"4": {
"id": 4,
"name": "Mark Zuckerberg",
"smallPic": "https://cdn.site.io/pic-4-64.jpg",
"bigPic": "https://cdn.site.io/pic-4-1024.jpg"
}
}
} Which is enough to reconstruct the answer to that specific query, but only if your client knows that {
"root": {
"user(id:4)": ReferenceTo("User", 4)
},
"User": {
"4": {
"id": 4,
"name": "Mark Zuckerberg",
"profilePic(size:64)": "https://cdn.site.io/pic-4-64.jpg",
"profilePic(size:1024)": "https://cdn.site.io/pic-4-1024.jpg"
}
}
} The point I'm trying to make here is that the structure of your cache depends on the query I ran, whereas my cache's struture depends purely on the structure of the underlying data model. It's possible to run a query against my cache without any knowledge of which queries have already been run, and get correct results. This means that you wouldn't need users to implement client side querying functionality. To create a query that depends on the structure of the underlying data, rather than on the query that was run, you need to be able to read in and restructure the query, then when you want to run the query against the local cache, you need to be able to understand the structure of the query again. What the babel plugin does is something like: var q = Relay.QL`
query {
user(id: 4) {
id
name
smallPic: profilePic(size: 64)
bigPic: profilePic(size: 1024)
}
}
`; into: var q = (function () {
var GraphQL = Relay.QL.__GraphQL;
return new GraphQL.Query("user", new GraphQL.CallValue(4), [new GraphQL.Field("id", null, null, null, null, null, {
parentType: "UserType"
}), new GraphQL.Field("name", null, null, null, null, null, {
parentType: "UserType"
}), new GraphQL.Field("profilePic", null, null, [new GraphQL.Callv("size", new GraphQL.CallValue(64))], "smallPic", null, {
parentType: "UserType"
}), new GraphQL.Field("profilePic", null, null, [new GraphQL.Callv("size", new GraphQL.CallValue(1024))], "bigPic", null, {
parentType: "UserType"
})], null, {
rootArg: "id"
}, "Unknown");
})(); Which evaluates to an object like: { fields:
[ { fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'id',
calls: [],
alias: null,
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } },
{ fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'name',
calls: [],
alias: null,
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } },
{ fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'profilePic',
calls:
[ { kind: 'Call',
value: { kind: 'CallValue', callValue: 64 },
name: 'size',
metadata: {} } ],
alias: 'smallPic',
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } },
{ fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'profilePic',
calls:
[ { kind: 'Call',
value: { kind: 'CallValue', callValue: 1024 },
name: 'size',
metadata: {} } ],
alias: 'bigPic',
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } } ],
fragments: [],
children:
[ { fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'id',
calls: [],
alias: null,
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } },
{ fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'name',
calls: [],
alias: null,
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } },
{ fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'profilePic',
calls:
[ { kind: 'Call',
value: { kind: 'CallValue', callValue: 64 },
name: 'size',
metadata: {} } ],
alias: 'smallPic',
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } },
{ fields: [],
fragments: [],
children: [],
kind: 'Field',
fieldName: 'profilePic',
calls:
[ { kind: 'Call',
value: { kind: 'CallValue', callValue: 1024 },
name: 'size',
metadata: {} } ],
alias: 'bigPic',
condition: null,
__metadata__: { parentType: 'UserType' },
metadata:
{ edgesID: undefined,
inferredRootCallName: undefined,
inferredPrimaryKey: undefined,
isConnection: false,
isFindable: false,
isGenerated: false,
isPlural: false,
isRequisite: false,
isUnionOrInterface: false,
parentType: 'UserType' } } ],
__metadata__: { rootArg: 'id' },
kind: 'Query',
metadata: { rootArg: 'id' },
name: 'Unknown',
fieldName: 'user',
isDeferred: false,
calls:
[ { kind: 'Call',
value: { kind: 'CallValue', callValue: 4 },
name: 'user',
metadata: {} } ] } In there is everything you need to figure out what data is being requested, and where you should store it in the cache. You can build a query, without taking any notice of the "alias" values, so that what you store in your cache is properly normalised, then you can apply the aliases when you read the data out of your cache. I believe this is what relay is doing (and how it's able to query just the data that it doesn't already have). |
@ForbesLindesay impressive research into Relay internals!
It turns out that the raw query descriptor - that output of the |
@acdlite did a great job on https://github.com/acdlite/relay-sink |
@vicentedealencar that looks neat, but I don't think it has anything to do with redux. It still relies on having the entire relay infrastructure in your code and having all the relay state stored in the normal relay store, so it won't let us move that state into redux. |
Can somebody clarify some specific, concrete goals for Relay/Redux coordination/collaboration? Is the goal better debugging? To implement a GraphQL client and store data in Redux (which effectively means reimplementing all of Relay)? Or is it to use Relay to fetch some data that is also needed for Flux stores (for example as default values for things, etc)? Please feel free to file issues over at facebook/relay if there are specific use-cases that we could help support. |
@josephsavona, I for one use Redux outside of a React environment and since GraphQL is also React agnostic (and I hope it stays that way) I'd be very keen for at least some good examples/patterns for Redux as the storage engine in just vanilla Redux and GraphQL! ;) |
@antitoxic I have a half completed solution, PM me if interested. This will be open sourced when it's ready. Goal
Life cycle
|
@jackielii I am interested of course, though I don't know if PM functionality exists in github (email me at: antitoxic at gmail). If you change your mind on open-sourcing your work earlier, I think everyone here will be interested. The flow you described is similar to what I have in mind. I don't know how 13. and 15. would work. They seem the most tricky. |
Hmm, this is an interesting approach - asking for a query, having that set a dirty state in the store, and then using that to compose an efficient query to send to the server. In Apollo Client we're taking a slightly different approach where we start with the query called by the UI, then compare that against the store and send the diffed query without marking dirty state. I think this would be a really exciting thing to try out! One thing that we do have which I think could be very useful, are utilities to read and write queries to a normalized store, see here:
If there were a way to factor those out so that all of these different clients to use them, I think that would be a big win so that we could focus on where stuff is different rather than constantly reimplementing ways to read and write GraphQL data in normalized form. |
@jackielii , @stubailo : just to toss in one more related link, https://github.com/tommikaikkonen/redux-orm is a purely client-side approach to managing relational data in a Redux store. Might be some useful comparison in approaches. |
I think it would be really cool to have the ability to read arbitrary normalized data using a GraphQL query, even if it didn't come from a GraphQL server. We're pretty close to that with Apollo client, and it would be a great way to decouple your UI from the data model by introducing the GraphQL abstraction layer. Since the GraphQL layer is strongly typed, you could then integrate with Typescript checking, validate your queries, etc. and maybe eventually migrate to using a GraphQL server which I think will be the best option. |
Semi-sorta off-topic, but: the thing that's bugging me the most about all this GraphQL talk is the server side. How do you actually use and implement this on the server? How feasible is it to use GraphQL with an existing application? Can you put a GraphQL adapter around an existing data setup? Is all this Node-only? |
You sure can do all of the above, and in many different languages! All of our posts are about JavaScript for now, but you can do the same stuff in any language:
We're actually migrating parts of our existing Node/Mongo/Meteor/React app to Apollo right now, and using it in just a few views of the application is working out really well. Hopefully will have a post about that soon. |
@stubailo : thanks. One more very off-topic question: how does GraphQL deal with continually-refreshed data, if at all? For example, say I've got a service that's tracking some info that's changing over time, and at the moment I'm just doing dumb polling every 15s to get the latest value, and doing diffing on the client. How would a GraphQL setup deal with trying to keep that data current? |
I’ve been feeling this too. |
@markerikson GraphQL doesn't deal with data that is continually changing. Even with subscriptions in graphql there are things you have to setup whether it be through sockets, observables or w/e. Relay handles that for the most part. I built react-reach to test out how to use it with redux. I learned a few things about it. One thing that sucks about |
@kennetpostigo: ah. thanks for the clarification. Yeah, I have two existing apps. First one is basically CRUD, done with an RPC-style approach, and I'm prototyping a React+Redux rewrite for the original GWT client. The other is Backbone, and primarily showing live updates of server data. Just curious how GraphQL might work for that scenario, but sounds like the answer is "not really". |
@gaearon want to talk sometime? I think it would literally just be a couple of tweaks to the internal query stuff of Apollo Client, and then we can publish it as a separate package.
@markerikson this is a fully-supported pattern in apollo client, basically you can just set a
@kennetpostigo Try the Apollo Client normalizer? I'd be happy to work with you to factor that out into a separate module. I think if we work together to make one really great GraphQL normalizer, then we can all benefit from it, rather than implementing a new thing every time which will have its own edge cases and behavior. We're doing our best to make it pluggable, for example by accepting a custom |
@stubailo sounds good, if you can give me a rundown on where the normalization parts in apollo are I'd be glad to help in extracting it out. PM me on the graphql slack. |
Some more new stuff |
I think there are definitely many ways to do this stuff and lots of different approaches are valid, I think the best we can do is share some of the tooling so that we aren't all reinventing the wheel each time! |
@stubailo Following the Redux's middleware approach: action dispatched and middleware intercepts and translates. As for "mark dirty", it's just an plain action with a graphql query, middleware intercepts it and normalise it, then merge into the store, and from here it goes back to Redux's life cycle, any component subscribe to the parts of state that's updated to "dirty"(and you can imagine this achieves the fat query in relay), it updates, and requests for more data if needed(intersect action) I tried to send you an email to the address in you github profile, but got rejected. could you give me you email if want to discuss a bit more? |
Normalise is just a process of executing a graphql query, so basically I re-use the graphql-js's execute to do the nomalising. and the structure of the normalised data is just like a type hashmap:
|
anyone interested, please email jackie.space at gmail |
I personally think loading the schema on the client and using GraphQL-JS is not the right approach, which is why we wrote a much simpler executor that doesn't rely on knowing the schema.
I don't really use email (but I should update that address) but you can find me on the GraphQL or Apollo slack channels: |
@stubailo is there documentation describing to what level the apollo client support diffing? |
@antitoxic here it is: http://docs.apollostack.com/apollo-client/core.html#forceFetch There's an issue open to do fancier diffing, which would be easy to implement but there are some concerns with data consistency - if you diff down to individual fields, then different fields on the same object might get out of sync. apollographql/apollo-client#113 |
I use GraphQL and GraphQL-relay server side. Then I use redux and redux-thunk in my application. The thunks are called by the components and compare their data-requirements against the state before conditionally loading more from graphQl. This setup works excellent and I am happy with the stack. However, when I look at the elegance of relay queries I envy their dynamic nature and expressiveness. It would be great if we could come up with a special action, based on thunks/sagas, or some middleware - that could
I think the translations between relay/graphql and app state will be challenging, which would perhaps require some common structure. But a flexible relay action in redux would definitely be a powerful tool!. |
@tvedtorama this is almost exactly what Apollo Client was designed to do, have you taken a look at that yet? It has all of the custom translation etc you just mentioned. |
@stubailo No, I didn't quite catch its mission from the discussions above. Looks really promising and I love that they use Typescript. I jumped right to the redux-integration which explained things for me: http://dev.apollodata.com/react/redux.html Thanks for this great tip! |
I have integrated Redux and Relay in a way not mentioned in this thread. I have a GraphQL type that defines the representation of the Redux state. That state is provided in my root Relay query. Upon the initial load of the graphql root, this state is also loaded, and initializes the Redux state. Whenever the Redux state is updated on the client, I commit a mutator to push that state up to the server, and use the optimistic update to immediately provide the new state in the Relay fragment. The graphql server then does have chance to respond to the client state and modify it and other graphql fragments accordingly, which may update the client state again. |
@naturalethic does that mean that you essentially don't have any client state any more, and all client states go through the server? I'm having trouble imagining a use-case in which that would be desirable for all client state. |
@helfer This solution is evolving as I play with it, however there is no reason one could not choose to send only a subset of the client state to the server, or none at all. If one were to subclass Relay.DefaultNetworkLayer, one could filter out and ignore queries of this particular mutation, and I suspect the optimistic update would still pass the new state through to the Relay store, though I have not tried that yet. Under such a condition, one could provide, purely on the client, a loop back from redux into relay, where a copy of the redux state is a branch on the relay state tree. |
So now that Relay and GraphQL are out in the open, we should start a conversation about how this relates to Redux. Anyone have thoughts on Relay will effect both the core Redux library, and what ecosystem tools will need to be built out to accommodate it?
The text was updated successfully, but these errors were encountered: