Skip to content
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

Closed
nhagen opened this issue Aug 11, 2015 · 69 comments
Closed

Relay and Redux #464

nhagen opened this issue Aug 11, 2015 · 69 comments

Comments

@nhagen
Copy link

nhagen commented Aug 11, 2015

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?

@CooCooCaCha
Copy link

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.

@mindjuice
Copy link
Contributor

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).

@CooCooCaCha
Copy link

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.

@CooCooCaCha
Copy link

Actually, we might be part of the way there already. Relay already allows custom network layers:
http://facebook.github.io/relay/docs/guides-network-layer.html#content

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.

@ide
Copy link

ide commented Aug 12, 2015

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:

  • Redux devtools work, so you could step back in time through your GraphQL mutations
  • Fewer store/dispatcher/actions systems to learn
  • Hot reloading
  • Can use the Redux store outside of Relay code e.g. for storing client state that you don't want to sync to the server or spend time defining a GraphQL schema for
  • The Redux community has put a lot of thought into making Redux extensible -- this would test to see if Redux is extensible enough to support (hypothetical) redux-relay middleware
  • Relay/GraphQL applications could benefit from Redux middleware

Pretty exciting stuff after typing it out...

cc @yungsters @josephsavona

@yungsters
Copy link

@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.

@ForbesLindesay
Copy link
Contributor

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:

  • Redux solves the problem of managing local state and mutations (including managing caches of data from the server). However, Redux makes querying server side data relatively imperative (i.e. send the 'GET_DATA' action and wait for the corresponding 'GOT_DATA' action to update the store).
  • Relay solves the problem of declaratively managing the active queries you have for server data, fetching that data and then keeping the client's cache up to date in the face of mutations.

There are three really difficult and valuable things that Relay accomplishes:

  1. Diff the queries currently executed with the new queries from recently added components in order to query only new data from the server.
  2. Optimistically apply updates on the client
  3. Model both complex data structures (graph data is a hugely powerful model) and complex queries (which reduces need for round trips).

I can see two barriers to integrating relay & redux:

  1. Redux requires all state to be in a single immutable location. Relay appears to store its state in RelayStoreData but in a number of mutable data structures within that. The first step I think would be to pull that out so that this state object could be managed by Redux.
  2. Redux uses this.context to seamlessly support universal/isomorphic applications. The same approach could work well in Relay, but it doesn't look like it's currently supported. It looks like components like RelayRootContainer call into the singleton RelayStore and RelayStoreData. getDefaultInstance. I don't know how much work it would be to pull the relay store from this.context or similar.

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.

@yungsters
Copy link

The same approach could work well in Relay, but it doesn't look like it's currently supported.

This is the (currently unrealized) intent for the RelayStoreData internal module. In theory, we could pass this down through your app via the RootContainer.

@ForbesLindesay
Copy link
Contributor

That would be an excellent first step towards redux integration.

@bsr203
Copy link

bsr203 commented Sep 18, 2015

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

@gaearon
Copy link
Contributor

gaearon commented Sep 18, 2015

Also relevant: https://github.com/gyzerok/adrenaline

@fgarcia
Copy link

fgarcia commented Sep 19, 2015

Am Freitag, 18. September 2015 schrieb Dan Abramov :

Also relevant: https://github.com/gyzerok/adrenaline


Reply to this email directly or view it on GitHub
#464 (comment).

@gyzerok
Copy link

gyzerok commented Sep 22, 2015

@ALL I highly need any feedback about https://github.com/gyzerok/adrenaline. I'm stuck a little bit now about how to move on.

@wmertens
Copy link
Contributor

@gyzerok could you amend your readme with a quick overview of your approach? How are you matching stateless Redux with stateful Relay?

@ForbesLindesay
Copy link
Contributor

@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 👍

@bbirand
Copy link

bbirand commented Sep 23, 2015

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. 💯

@gyzerok
Copy link

gyzerok commented Sep 23, 2015

@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?

@ForbesLindesay
Copy link
Contributor

@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.

@gyzerok
Copy link

gyzerok commented Sep 24, 2015

@ForbesLindesay Do you understand principles behind babel-relay-plugin? If so, can you explain it a little more?

@ForbesLindesay
Copy link
Contributor

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 resolve implementations read from cache (approximately, correct me if I've massively understood). This makes it very difficult when the query changes to do anything other than re-query anything.

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:

query {
  user(id: 4) {
    id
    name
    smallPic: profilePic(size: 64)
    bigPic: profilePic(size: 1024)
  }
}

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 profilePic(size: 64) will be stored as smallPic in the cache. What you want your cache to look like is something more like:

{
  "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).

@josephsavona
Copy link

@ForbesLindesay impressive research into Relay internals!

In [the raw query object] is everything you need to figure out what data is being requested, and where you should store it in the cache...

It turns out that the raw query descriptor - that output of the babel-relay-plugin - actually isn't sufficient for processing queries. Consider photo(size: $size) - the query description for this will have a placeholder for the size. But what value do we use for $size? Rather than directly operating on the query descriptor, Relay instead uses an abstraction that allows it to process queries and values in parallel. We'll be talking more about how Relay works in some upcoming blog posts, and we're always happy to answer questions at facebook/relay.

@vicentedealencar
Copy link

@acdlite did a great job on https://github.com/acdlite/relay-sink

@ForbesLindesay
Copy link
Contributor

@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.

@josephsavona
Copy link

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.

@clintwood
Copy link
Contributor

@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! ;)

@jackielii
Copy link
Contributor

@antitoxic I have a half completed solution, PM me if interested. This will be open sourced when it's ready.

Goal

  1. think in redux's terms: only use action creators, middlewares, selectors
  2. flexible fragments composition
  3. data to be automatically available to the component that defines a fragment
  4. action creators to easily compose queries, mutations and "markDirty"s
  5. query diffing to fetch only required fields.

Life cycle

  1. connected component start rendering
  2. selector select data from store
  3. selector sees undefined data
  4. selector return dirty state
  5. connected component render with "loading" element
  6. component did mount
  7. subscribe to store and select the state
  8. check if graphql state has dirty entities
  9. if dirty, dispatch a queryDirty action,
  10. store unchanged, component not re-rendered, else rendered
  11. queryDirty action goes through the middleware
  12. graphqlMiddleware intercepts the action
  13. the middleware sends an "optimised" query to the server
  14. server return data
  15. middleware normalises the data
  16. middleware forward the result to the next middleware until the actual dispatch on the redux store gets called
  17. rootReducer gets called
  18. graphql reducer gets called
  19. graphql reducer merges the normalised data into a new state
  20. connect component gets notified of the updated state
  21. goto 7

@antitoxic
Copy link

@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.

@stubailo
Copy link

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.

@markerikson
Copy link
Contributor

@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.

@stubailo
Copy link

stubailo commented May 26, 2016

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.

@markerikson
Copy link
Contributor

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?

@stubailo
Copy link

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:

  1. How one of our contributors put a JavaScript GraphQL server on top of .NET, MySQL, MSSQL, and a PHP API: https://medium.com/apollo-stack/the-business-case-for-graphql-cc7a2b93148d
  2. Tutorial for building a GraphQL server that talks to three backends: https://medium.com/apollo-stack/tutorial-building-a-graphql-server-cddaa023c035?source=latest
  3. A general post about questions people have coming from REST: https://medium.com/apollo-stack/how-do-i-graphql-2fcabfc94a01#.37216p92t

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.

@markerikson
Copy link
Contributor

@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?

@gaearon
Copy link
Contributor

gaearon commented May 26, 2016

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.

I’ve been feeling this too.

@kennetpostigo
Copy link

kennetpostigo commented May 26, 2016

@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 react-reach is that you cant collocate queries/mutations nicely with it. The second iteration that I've been working tries to make collocating queries/mutations nicer, as well as handles batching. I think I'll be done in 1-2 weeks and will deprecate react-reach. I also want to add normalization to the second iteration not sure if to depend on normalizr or make a homegrown solution. If you guys have any ideas for things you would like if you are using graphql with redux lmk.

@markerikson
Copy link
Contributor

@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".

@stubailo
Copy link

stubailo commented May 26, 2016

@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.

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?

@markerikson this is a fully-supported pattern in apollo client, basically you can just set a pollInterval on your query, and it will integrate that result into the store for you. No custom diffing or normalization logic required.

I also want to add normalization to the second iteration not sure if to depend on normalizr or make a homegrown solution.

@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 dataIdFromObject function which lets you specify an arbitrary function to get the normalization ID from the object.

@kennetpostigo
Copy link

@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.

@gaearon
Copy link
Contributor

gaearon commented May 26, 2016

Some more new stuff

https://medium.com/@matt.krick/2990c81aa807

@stubailo
Copy link

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!

@jackielii
Copy link
Contributor

jackielii commented May 27, 2016

@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?

@jackielii
Copy link
Contributor

jackielii commented May 27, 2016

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:

Map<typeName: string, entityMap: Map<key: mixed, entity: Map<fieldNameWithArgs: string, value: mixed>>>

@jackielii
Copy link
Contributor

anyone interested, please email jackie.space at gmail

@stubailo
Copy link

I re-use the graphql-js's execute to do the nomalising

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.

could you give me you email if want to discuss a bit more?

I don't really use email (but I should update that address) but you can find me on the GraphQL or Apollo slack channels:

@antitoxic
Copy link

@stubailo is there documentation describing to what level the apollo client support diffing?

@stubailo
Copy link

stubailo commented Jun 2, 2016

@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

@tvedtorama
Copy link

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

  1. Parse relay data requests from components, using schema
  2. Use a custom translation to compare requirement against redux state
  3. Fetch data, if needed
  4. Use a custom translation in the reducer to put the result into state

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!.

@stubailo
Copy link

@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.

@tvedtorama
Copy link

@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!

@naturalethic
Copy link

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.

@helfer
Copy link

helfer commented Nov 29, 2016

@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.

@naturalethic
Copy link

@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.

@reduxjs reduxjs deleted a comment from halil199409 Mar 5, 2021
@reduxjs reduxjs locked as resolved and limited conversation to collaborators Mar 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests