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
馃憤 I was trying to find any reference to how to do "client schema extensions" but failed.
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.
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?
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
@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 { ... }
Hi there
Please, do you have any news and/or roadmap about this compiler feature?
FYI anyone new to react / relay looking for a temporary workaround, a "dirty" solution as suggested in https://github.com/facebook/relay/issues/1787 is to use context at the application root
@josephsavona In terms of changes, we're looking at :
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?
@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.
@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.
@adjourn I'd guess this is the OSS compiler: https://github.com/facebook/relay/tree/master/packages/relay-compiler
This would be a super helpful feature.
Does the oss compiler support client schema extensions yet?
Anyone who didn't notice this thread, also, it has some useful details w.r.t. client schema: https://github.com/facebook/relay/issues/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
@jstejada any docs about this part?
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 :)
This is roughly what needs to happen in the compiler: https://github.com/kassens/relay/commit/a51f9f22d83c79b9b08100c866dbb84da210eac1
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).
@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
@sibelius just mixing the client schema into the server schema won鈥檛 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 鈥攃lient-schema) and use that appropriately.
Check the network layer in the example, it filters the local stuff. There is probably a better way, but it works.
I opened a PR with some slight changes to what @kassens did. https://github.com/facebook/relay/pull/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.
My example has been updated to use @edvinerikson's changes in #2264 if anyone is here looking for an example.
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
I'm confused, isn't there still a limitation where the compiler needs to be altered to allow for client schema? Or was that solved in #2264? Is the only step now to get things documented?
notwithstanding the limitation pointed out above, it is solved.
@tslater That鈥檚 correct, it was solved in #2264 and the only thing needed now is documentation.
@alloy Thanks for clarifying. I just noticed this in the release notes:
Extensions only work on existing types, and does not currently support adding client-only types to the schema.
Does that mean one needs to put any types they're using client side in their server/other schemas?
I believe so, yes. Maybe @edvinerikson can better help you with the details.
I haven't tested adding new types, but I don't think there is any limitation. The compiler just takes all the extra documents and merges them into a complete schema (together with server schema). To also check if a type is from the server, they also track the server schema separately as well.
https://github.com/facebook/relay/blob/master/packages/relay-compiler/codegen/RelayFileWriter.js#L120
https://github.com/facebook/relay/blob/master/packages/relay-compiler/graphql-compiler/transforms/SkipClientFieldTransform.js#L115
https://github.com/facebook/relay/blob/master/packages/relay-compiler/graphql-compiler/core/GraphQLCompilerContext.js#L44
should we open another issue to track adding new types to client schema extentions?
@edvinerikson the main limitation right now is that records of those types will always get garbage collected, I /think/
@sibelius yeah I think that's a good idea
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
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.
It supports client schemas well, we use this feature in development.
Could anyone provide some meaningful examples, since docs are lacking?
.graphql extension for example local.graphql# 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
}
}
}
`;
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());
};
});
}
this.disposableMap[err.id]() on err removalWow, thanks @istarkov , you are the * Boss!
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
@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?
@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.
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.
This post on medium is really helpful too https://medium.com/@matt.krick/replacing-redux-with-relay-47ed085bfafe
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 (https://github.com/facebook/relay/pull/2482) but it's been in PR purgatory for a few months :cry:
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.
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馃棥
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
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
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?
@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
}
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
Most helpful comment
.graphqlextension for examplelocal.graphqlrun 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
this.disposableMap[err.id]()on err removal