nodkz/relay-northwind-app

How to do mutations?

cellis opened this issue · 13 comments

I've already talked with @nodkz about this. Using this as a way to keep track. Essentially I want to know how to make a mutation that updates a hasMany, hasOne relationship.

I will help you with pleasure. But let keep our discussion open via github issues and use my existed repos for examples and solving problems:

  1. https://github.com/nodkz/relay-northwind-app for the client side howtos questions (Relay specific).
  2. https://github.com/nodkz/graphql-compose-examples for the server side GraphQL howtos questions.
  3. https://github.com/nodkz/graphql-compose for library API

Before answering to your question, firstly we should fix knowledge about Relay Mutation API:

  • In summer 2016 (maybe earlier) Relay team started to write new Relay 2 facebook/relay#1369 it will have better Mutation API
  • Forget about tracked/fat query ;) Old Mutation API is fatigue and pain. Pain of awful number of Mutation files which has problem of keeping in sync mutations and components. When your component has several mutations (eg. changeStatus, update, remove) then you should to create 3 mutation files and write quite a lot of code in the component itself. So I'll share much better solution how to work with mutations with less typing and keeping your code clear.
  • So take a look on new mutation API https://facebook.github.io/relay/docs/api-reference-relay-graphql-mutation.html#content it is not ideal and may be changed with time, so definitely we should wrap it and make simpler for using it in our apps facebook/relay#1046 (comment)

Khm... before playing with mutations needs to tune relayStore:
So the 1st main step will be creation of own instance of RelayEnvirement (also called as relayStore). Our relayStore will provide helper methods like mutate (for mutations) and reset (when login/logout for cleaning up store from data) Take it here https://gist.github.com/nodkz/03f28aaa1b66195b517ecf06e92487bf
Also we will add to our relayStore fetch method for manual fetching data from server (sometimes you may need data that is difficult to obtain via component fragments, eg. list of admins, or list of cities for autosuggestion component).

So fork my repo https://github.com/nodkz/relay-northwind-app

  • try to put there somehow relayStore in app/clientStores.js
  • add Reset link for flushing Relay cache (in the footer of MainPage)

In further we will refactor the code, introduce mutations and solve tons of other small questions which will occur.

Best,
Pavel.

nodkz commented

Before we coming to mutations, we should complete preparation of our relayStore:

  • Let make our relayStore.mutate() method chainable via Promise. This will allows to us using async/await for complex data flow with mutations.
  • And add relayStore.fetch() method for fetching data from the server without hitting relay-router and component fragments. We will make code refactoring and start using relayStore on full power.

Right now I implement this and provide additional instructions.

nodkz commented

So let practice with relayStore.fetch method. Mutate method has analogous symantics with additional arguments (we will consider it tomorrow, just a little patience).

So lets refactorapp/components/categories/ToggleCategory.js component which use manual data fetching.

  // native Relay API
  toggle() {
    this.setState({
      isOpen: !this.state.isOpen,
    });

    if (!this.state.data) {
      const query = Relay.createQuery(
        Relay.QL`query {
          viewer {
            category(filter: $filter) {
              ${Category.getFragment('category')}
            }
          }
        }`,
        { filter: { categoryID: this.props.id } }
      );
      relayStore.primeCache({ query }, readyState => {
        if (readyState.done) {
          const data = relayStore.readQuery(query)[0];
          this.setState({ data: data.category });
        }
      });
    }
  }

As you can see it is difficult for reading (Relay.createQuery, primeCache, readyState, relayStore.readQuery). Agrrrr.

So let rewrite it with our new relayStore.fetch method

  // our wrapper on relayStore, with simplified API
  toggle() {
    this.setState({ isOpen: !this.state.isOpen });

    if (!this.state.data) {
      relayStore
        .fetch({
          query: Relay.QL`query {
            viewer {
              category(filter: $filter) {
                ${Category.getFragment('category')}
              }
            }
          }`,
          variables: {
            filter: { categoryID: this.props.id },
          },
        })
        .then((res) => {
          // NB: Relay does not return root field, in this case `viewer`
          // so in responce we get object with fields from `viewer` (may be changed in Relay2)
          this.setState({ data: res.category }); 
        });
    }
  }

This is much much better.


@cellis please refactor all other Toggle* components (pull fresh version and create new branch).

When it will be completed let move to the server side. Clone https://github.com/nodkz/graphql-compose-examples and try to add mutations createOneProduct and removeProduct to the schema.

nodkz commented

@cellis also I want you to pay attention to fragment inclusion Category.getFragment('category') to the query. This is a replacement for fatQuery in mutations. All data required explicitly (no more complex operation on mobile devices for determining additional fields for query). So when you provide fragment in such way, data automatically updates in the Relay Store, and store touch all components that subscribed on changed data for rerendering.

And keep in mind that mutations are the regular queries. The difference only in naming (pointing that this request will change data on server) and in serial execution if several mutations in the request (several queries executes in parallel). Say it again, cause it is important, mutations in GraphQL almost similar to the queries. When we complete with queries, it will be very easy to understand and construct complex mutations with deep data fetching in the payload.

@nodkz thanks for this. Will work on this refactor tomorrow after work.

@nodkz I'm looking into refactoring the Toggles and it dawned upon me, why use the toggle() method at all? I will refactor anyways, but it seems like the act of toggling should instead be showing() (perhaps by setState({ toggled: true })) yet another sub container in relay with it's own route/"root" query. Anyways I will make it work like this, just wondering what the reasoning was. I think it could save time, it's just not documented on the relay website.

nodkz commented

Called toggle cause on pressing a button it should show/hide sub-component and change button text.

toggle() {
    this.setState({ isOpen: !this.state.isOpen });
    // ...
}

render() {
  const  { isOpen } = this.state;

  return (
     // ....
     <Button onClick={this.toggle}>
       {isOpen ? 'close' : 'open'}
     </Button>
   );
}

We may move out fetching data to another method for clearence:

toggle() {
  const isOpen = !this.state.isOpen;
  this.setState({ isOpen });
  if (isOpen) this.fetch();  
}

fetch() {
  relayStore
        .fetch({
          query: Relay.QL`query {
            viewer {
              category(filter: $filter) {
                ${Category.getFragment('category')}
              }
            }
          }`,
          variables: {
            filter: { categoryID: this.props.id },
          },
        })
        .then((res) => {
          this.setState({ data: res.category });
        });
}

Also this code makes one fix, it always will refresh component state from RelayStore on opening. Right now it fetches data only once, and when we introduce mutations and run them we will see stale data from component store, not from relayStore.

BTW relayStore.fetch() method should make http-request only first time for populating its cache. After that (when we hide data and show it again) Relay will run a new query but fulfill it from its cache without network request.

FYI you can use relayStore.fetch({ ..., force: true }) for force retrieving data from a server without clearing the cache.

nodkz commented

Anyway, current toggle implementation it is some kind of hack. Cause we keep data for child in the component state. Maybe in future when will be introduced react-router@v4 and this hack will be somehow removed. Cause RRv4 allows deep nested routes.

But for now, for demo purposes of component approach power is the good solution.

@nodkz RRv4 doesn't work with isomorphic rendering ( relay-tools/react-router-relay#193 ). So maybe we have to keep doing it this way :/

nodkz commented

I do not use react-router-relay. With its queries aggregation, it brings too many restrictions for my app. Most important:

  • can not use in sub-routes variables with the same name (eg. filter in parent component and filter in child component)

It was discussed here relay-tools/react-router-relay#213
But with my solution, you may get a waterfall of queries. But for my app it is not a problem.

Ok, refactor if Toggle* complete. Will move to other repo later tonight to createProduct mutation.

nodkz commented

Some tricks with auth and context discussed here graphql-compose/graphql-compose-examples#2

@cellis if you want to use react-router-relay you can use my isomorphic router middleware for koa@2
https://github.com/stoffern/koa-universal-relay-router

@Stoffern I have been using it in my personal project. But right now react-router-relay is hardcoded to relay 0.9.3 which means it doesn't support the new Relay GraphQLMutation stuff.