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

[Modern] Client schema docs and examples #1656

Closed
ivosabev opened this issue Apr 20, 2017 · 51 comments
Closed

[Modern] Client schema docs and examples #1656

ivosabev opened this issue Apr 20, 2017 · 51 comments
Assignees
Labels

Comments

@ivosabev
Copy link

Any idea when docs and examples would be available?

Client Schema Extensions
The Relay Modern Core adds support for client schema extensions. These allow Relay to conveniently store some extra information with data fetched from the server and be rendered like any other field fetched from the server. This should be able to replace some use cases that previously required a Flux/Redux store on the side.

https://facebook.github.io/relay/docs/new-in-relay-modern.html#client-schema-extensions

@leethree
Copy link

👍 I was trying to find any reference to how to do "client schema extensions" but failed.

@mjmahone
Copy link
Contributor

My suspicion is it'll happen in an example in RelayExamples before hitting official documentation here. I don't have a timeline, but that's your best bet for taking advantage of this feature. It already works (at least, it works on my machine :)), and you can find references to it via things like the RelayFilterDirectivesTransform, but getting this to work outside the FB environment hasn't yet happened.

@abhishiv
Copy link

abhishiv commented Apr 29, 2017

I would like to know more about this to as well.

This is what I figured out till now

  1. extendSchema function to extend schema. Also it's interesting to note that transformers are using this function.

  2. From the tests it looks like lookup, and commitPayload can be used to read and save data.

Am I missing anything else?

@josephsavona
Copy link
Contributor

I tried documenting this the other day and realized that there is an issue with the OSS compiler that prevents schema extensions from being defined. Once this is fixed we can document this feature.

Cc @kassens

@josephsavona
Copy link
Contributor

josephsavona commented May 30, 2017

@unirey The idea is that you can do something like this:

// client-schema.graphql
extend type SomeType {
  client: String
}

// compile script:
relay-compiler --schema schema.graphql --client-schema client-schema.graphql ...

// container fragment
graphql`
  fragment on SomeType {
    client
  }
`

// mutation updater
function updater(store) {
  const someValue = store.get(idOfValueOfSomeType);
  someValue.setValue("Foo", "client");  // value, field name
}

The missing part is that the oss compiler binary doesn't expose a way to tell the compiler about the client schema - all the primitives exist in the compiler to support client fields, and the runtime supports them as regular fields.

The client schema can also define new types: you can do

extend type Foo { field: NewType} 
type NewType { ... }

@fsenart
Copy link

fsenart commented Jun 30, 2017

Hi there
Please, do you have any news and/or roadmap about this compiler feature?

@chadfurman
Copy link

FYI anyone new to react / relay looking for a temporary workaround, a "dirty" solution as suggested in #1787 is to use context at the application root

@chadfurman
Copy link

@josephsavona In terms of changes, we're looking at :

https://github.com/facebook/relay/blob/master/packages/relay-compiler/generic/core/RelayCompiler.js#L52

class RelayCompiler {
  _context: RelayCompilerContext;
  _schema: GraphQLSchema;
  _transformedQueryContext: ?RelayCompilerContext;
  _transforms: CompilerTransforms;

  // The context passed in must already have any Relay-specific schema extensions
  constructor(
    schema: GraphQLSchema,
    context: RelayCompilerContext,
    transforms: CompilerTransforms,
  ) {
    this._context = context;
    // some transforms depend on this being the original schema,
    // not the transformed schema/context's schema
    this._schema = schema;
    this._transforms = transforms;
  }

We currently only have schema and context so we'll have to add in clientSchema and clientContext yes? Then manage these separately? Or do we have to have to merge the two schemas?

@josephsavona
Copy link
Contributor

@chadfurman The compiler is configured to treat this._schema as the canonical server schema, while this._context has its own copy of the schema that may include client extensions. So I think what we probably want is to add an optional clientSchema: string constructor argument. Each time the context is rebuilt, do a final pass to parse the client schema and extend the this._context's schema. Something like these lines to parse the client schema text into definitions and add them to context. Maybe do this here (or similar), ie constructing a fresh context just before actually running it through all the transforms.

@adjourn
Copy link

adjourn commented Aug 22, 2017

@josephsavona Im trying to follow the discussion. Are the steps you mentioned only ones left?

The missing part is that the oss compiler binary doesn't expose a way to tell the compiler about the client schema [...]

This is still missing? I don't even know what oss compiler binary exactly stands for or where to look.

@chadfurman
Copy link

@austinamorusotraceme
Copy link

This would be a super helpful feature.

@ryanpardieck
Copy link

Does the oss compiler support client schema extensions yet?

@chadfurman
Copy link

@chadfurman
Copy link

chadfurman commented Oct 29, 2017

Anyone who didn't notice this thread, also, it has some useful details w.r.t. client schema: #1787

I've been storing lots of client-side data either directly in component state, or as user preferences in the DB if it needs to be shared with other components

@sibelius
Copy link
Contributor

@jstejada any docs about this part?

@jstejada
Copy link
Contributor

Hey @sibelius, unfortunately we haven't made any docs about this yet. The limitation mentioned earlier in this thread still stands. We'll keep you posted with any updates :)

@kassens
Copy link
Member

kassens commented Dec 23, 2017

This is roughly what needs to happen in the compiler: kassens@a51f9f2

That would start reading *.graphql files from somewhere where one might define schema extensions of the form:

extend type User {
  isTyping: boolean
}

This extra field can then be queried as usual using fragment containers and updated using commitLocalUpdate (permanent change to the store as opposed to optimistic updates that can be rolled back), approximately like this:

require('react-relay').commitLocalUpdate(environment, store => {
   store.get('some-user-id').setValue(true, 'isTyping');
});

(I'm typing this from memory and haven't actually tested this, but given there's a bunch of interest and this has been too long on the back burner for us, I figured someone might be interested to take a look).

@sibelius
Copy link
Contributor

sibelius commented Jan 1, 2018

@kassens @brysgo did this to make client extensions works: https://github.com/brysgo/create-react-app/blob/master/packages/react-scripts/scripts/relay.js#L75

does this look reasonable?

here a sample example of it https://github.com/brysgo/relay-example

@josephsavona
Copy link
Contributor

@sibelius just mixing the client schema into the server schema won’t work: the compiler needs to know which fields/types are from which schema in order to strip out client-only portions of queries that it sends to the server. Th compiler binary needs to change to accept a second argument (eg —client-schema) and use that appropriately.

@brysgo
Copy link

brysgo commented Jan 2, 2018

Check the network layer in the example, it filters the local stuff. There is probably a better way, but it works.

@edvinerikson
Copy link
Contributor

I opened a PR with some slight changes to what @kassens did. #2264. I tested it on the Relay Todo app in the relayjs-examples repo. I can define extensions and see them being added to the concrete fragments but removed for the final query so seems to work.

@brysgo
Copy link

brysgo commented Feb 27, 2018

My example has been updated to use @edvinerikson's changes in #2264 if anyone is here looking for an example.

@jstejada
Copy link
Contributor

jstejada commented Mar 4, 2018

any PR's to add docs for this would be accepted! The only important thing to note would be that client schema extensions only currently support additions to existing types and not adding entirely new types

@istarkov
Copy link

istarkov commented Apr 11, 2018

What is the good way to prevent records to be collected from store?

i.e.

  commitLocalUpdate(environment, s => {
    const err = s.create('4', 'Error');
    err.setValue('4', 'id');
    err.setValue('Error message', 'message');
    root.setLinkedRecords([err], 'errors');
  });

errors array after gc contains undefined.

But If I add code below

    store.retain({
      dataID: '4',
      node: { selections: [] },
      variables: {},
    });

this prevents records deletion

@lszomoru
Copy link

lszomoru commented Apr 17, 2018

As I am reading through this issue it is not clear to me whether relay-compiler version 1.5.0 which was updated 2 months ago on npm supports client schema or not. The release notes for version 1.5.0 mention client schema but running relay-compiler does not display the actual flag.

@istarkov
Copy link

It supports client schemas well, we use this feature in development.

@ivosabev
Copy link
Author

Could anyone provide some meaningful examples, since docs are lacking?

@istarkov
Copy link

istarkov commented Apr 18, 2018

  • place anywhere inside src folder your local schema, give it .graphql extension for example local.graphql
  • add inside local schema your types and extend server types
# local.graphql
type Trace {
  id: ID!
  file: String
  line: Float
  what: String
  addr: String
}

type Error {
  id: ID!
  message: String
  prettyMessage: String
  operationName: String
  path: [String]
  trace: [Trace]
}

extend type Query {
  errors: [Error]
}
  • run relay compiler as usual relay-compiler --src ./src --schema {PATH_TO_SERVER_SCHEMA}

  • since now you can use local definitions above in your fragments or query

const query = graphql`
  query AppQuery {
    ...serverStuff_root
    ...otherServerStuff_root
    errors {
      id
      message
      prettyMessage
      operationName
      path
      trace {
        id
        file
        line
        what
        addr
      }
    }
  }
`;
  • now you need to add data in your relay store
import { commitLocalUpdate, ROOT_ID } from 'relay-runtime';

const ROOT_TYPE = '__Root';
const ERR_TYPE = 'Error';
const TRACE_TYPE = 'Trace';

function addErrs(environment, errs) {

    commitLocalUpdate(this.environment, s => {
      let root = s.get(ROOT_ID);
      if (!root) {
        root = s.create(ROOT_ID, ROOT_TYPE);
      }

      const errRecords = errs.map(err => {
        const errRecord = s.create(err.id, ERR_TYPE);
        errRecord.setValue(err.id, 'id');
        errRecord.setValue(err.message, 'message');
        errRecord.setValue(err.prettyMessage, 'prettyMessage');
        errRecord.setValue(err.operationName, 'operationName');
        errRecord.setValue(err.path, 'path');

        /*
        file: String
        line: Float
        what: String
        addr: String
        */
        const traceRecords = err.trace.map(traceLine => {
          const traceLineRecord = s.create(traceLine.id, TRACE_TYPE);
          traceLineRecord.setValue(traceLine.id, 'id');
          traceLineRecord.setValue(traceLine.file, 'file');
          traceLineRecord.setValue(traceLine.line, 'line');
          traceLineRecord.setValue(traceLine.what, 'what');
          traceLineRecord.setValue(traceLine.addr, 'addr');
          return traceLineRecord;
        });

        errRecord.setLinkedRecords(traceRecords, 'trace');
        return errRecord;
      });

      const existingErrRecords = root.getLinkedRecords('errors') || [];

      root.setLinkedRecords([...errRecords, ...existingErrRecords], 'errors');
    });


    // Hack to prevent records being disposed on GC
    // https://github.com/facebook/relay/issues/1656#issuecomment-380519761
    errs.forEach(err => {
      const dErr = this.store.retain({
        dataID: err.id,
        node: { selections: [] },
        variables: {},
      });

      const dTraces = err.trace.map(tLine =>
        this.store.retain({
          dataID: tLine.id,
          node: { selections: [] },
          variables: {},
        }),
      );

      this.disposableMap[err.id] = () => {
        dErr.dispose();
        dTraces.forEach(dT => dT.dispose());
      };
    });
}
  • don't forget to call dispose i.e this.disposableMap[err.id]() on err removal
  • You win, you don't need redux etc ;-) you can use relay for all state management

@kikoseijo
Copy link

Wow, thanks @istarkov , you are the ***** Boss!

@istarkov
Copy link

Also we use local type extensions, to add local properties to existing objects like isNew, then setting isNew in mutation updater, we can show that object just created, (updated) etc so you can use local schemes even without commitLocalUpdate

@MrSaints
Copy link

@istarkov Thanks for that, though, arguably not as straightforward as say, the way Apollo GraphQL does it. And from the looks of it, you can't do things like mutations?

@istarkov
Copy link

istarkov commented Apr 20, 2018

@MrSaints I haven't tested the ability to have a fully compatible mutation interface. BTW if you can extend mutations (I don't know can you or not (not at computer right now)) it will be not a problem at all, just intercept them at network level and do any stuff with store.
Also IMO it's not a big deal to write more user friendly interface over the store for example like immerjs do with proxies https://github.com/mweststrate/immer/blob/master/src/proxy.js so all that store stuff can be hided via usual js operations.

@hisapy
Copy link

hisapy commented Jul 2, 2018

I found @istarkov findings super useful but didn't know where this.store comes from ... So I did a little research and found that in Relay 1.6.0 retain method is available directly from environment.

const dErr = environment.retain({
    dataID: err.id,
    node: { selections: [] },
    variables: {},
});

I also found that you can access store from environment, useful if you want to test changes like the following

describe("handleChange(field, value)", () => {
  test("updates userForm.user in local Relay store", () => {
    const firstName = "Roberto";
    const container = shallow(<UserForm relay={{ environment }} />).instance();
    container.handleChange("firstName", firstName);

    expect(
      environment
        .getStore()
        .getSource()
        .get("userForm:user").firstName
    ).toEqual(firstName);
  });
});

Just for the record relay-mock-network-layer is really useful for this kind of tests.

@JCMais
Copy link

JCMais commented Sep 17, 2018

This post on medium is really helpful too https://medium.com/@matt.krick/replacing-redux-with-relay-47ed085bfafe

@mattkrick
Copy link

thanks for the shoutout @JCMais! just a heads up, the client schema is not safe to use for fields that are mutated by server & optimistic updates. I wrote a fix for it here (#2482) but it's been in PR purgatory for a few months 😢

@timjacobi
Copy link
Member

Since @istarkov's comment is a bit dated now I was wondering if there was an easier way to set the initial values for the client state.

@MartinN3
Copy link

As @mattkrick and @josephsavona are working tough on #2482 - as we might see some improvements soon, it might be good time to start documenting those experimental features on that ### pull request🗡

@sibelius
Copy link
Contributor

can we have a fetchFnClient in network layer that handle custom resolvers on client schema extensions?

fetchFnClient would receive resolvers that are not in server schema

@sibelius
Copy link
Contributor

sibelius commented Jul 8, 2019

you can use this helper

export const setLocal = (query: GraphQLTaggedNode, localData: object) => {
  const request = getRequest(query);
  const operation = createOperationDescriptor(request, {});

  env.commitPayload(operation, localData);
  env.retain(operation.root);  // <== here @en_js magic :wink:
};

To make sure relay does not garbage collect same graphql operation

@sibelius
Copy link
Contributor

more blog posts here

https://babangsund.com/relay_local_state_management/
https://babangsund.com/relay_local_state_management_2/

anybody want to improve the docs with all this info?

@mattkrick
Copy link

mattkrick commented Jul 26, 2019

@sibelius thanks for keeping this alive!

Using hooks for local data is pretty easy, too. That way you don't need a clunky QueryRenderer.

const data = useLocalQuery<SnackbarQuery>(query)
const useLocalQuery = <TQuery extends {response: any; variables: any}>(
  environment: Environment,
  query: any,
  inVariables: TQuery['variables'] = {}
): TQuery['response'] | null => {
  const variables = useDeepEqual(inVariables)
  const [dataRef, setData] = useRefState<SelectorData | null>(null)
  const disposablesRef = useRef<Disposable[]>([])
  useEffect(() => {
    const {getRequest, createOperationDescriptor} = environment.unstable_internal
    const request = getRequest(query)
    const operation = createOperationDescriptor(request, variables)
    const res = environment.lookup(operation.fragment, operation)
    setData(res.data || null)
    disposablesRef.current.push(environment.retain(operation.root))
    disposablesRef.current.push(
      environment.subscribe(res, (newSnapshot) => {
        setData(newSnapshot.data || null)
      })
    )
    const disposables = disposablesRef.current
    return () => {
      disposables.forEach((disposable) => disposable.dispose())
    }
  }, [environment, setData, query, variables])
  return dataRef.current
}

@sibelius
Copy link
Contributor

we now have official docs about how to use client schema extensions:

https://relay.dev/docs/en/next/local-state-management

thanks to @babangsund for the great work exploring and documenting this

milieu added a commit to milieu/relay that referenced this issue Oct 5, 2020
The "New in Relay Modern" post indicates this support is experimental. This changes the title of the guide to match the announcement.
https://relay.dev/docs/en/new-in-relay-modern#client-schema-extensions-experimental

As of this writing this feature still seems to be experimental.
facebook#2471 (open)
facebook#1656 (closed, related)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests