nodkz / relay-northwind-app

A complex React, Relay, GraphQL demo app. Online demo:
https://nodkz.github.io/relay-northwind/
MIT License
103 stars 18 forks source link

How to do mutations? #1

Open cellis opened 7 years ago

cellis commented 7 years ago

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:

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 7 years ago

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

Right now I implement this and provide additional instructions.

nodkz commented 7 years ago

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 7 years ago

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

cellis commented 7 years ago

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

cellis commented 7 years ago

@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 7 years ago

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 7 years ago

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.

cellis commented 7 years ago

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

nodkz commented 7 years ago

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

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

cellis commented 7 years ago

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

nodkz commented 7 years ago

Some tricks with auth and context discussed here https://github.com/nodkz/graphql-compose-examples/pull/2

st0ffern commented 7 years ago

@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

cellis commented 7 years ago

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