-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Add a defaultContext property on ApolloClient #11275
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
--- | ||
"@apollo/client": patch | ||
--- | ||
|
||
Add a `defaultContext` option and property on `ApolloClient`, e.g. for keeping track of changing auth tokens or dependency injection. | ||
|
||
This can be used e.g. in authentication scenarios, where a new token might be | ||
generated outside of the link chain and should passed into the link chain. | ||
|
||
```js | ||
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; | ||
import { setContext } from '@apollo/client/link/context'; | ||
|
||
const httpLink = createHttpLink({ | ||
uri: '/graphql', | ||
}); | ||
|
||
const authLink = setContext((_, { headers, token }) => { | ||
return { | ||
headers: { | ||
...headers, | ||
authorization: token ? `Bearer ${token}` : "", | ||
} | ||
} | ||
}); | ||
|
||
const client = new ApolloClient({ | ||
link: authLink.concat(httpLink), | ||
cache: new InMemoryCache() | ||
}); | ||
|
||
// somewhere else in your application | ||
function onNewToken(newToken) { | ||
// token can now be changed for future requests without need for a global | ||
// variable, scoped ref or recreating the client | ||
client.defaultContext.token = newToken | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6142,4 +6142,221 @@ describe("QueryManager", () => { | |
} | ||
); | ||
}); | ||
|
||
describe("defaultContext", () => { | ||
let _: any; // trash variable to throw away values when destructuring | ||
_ = _; // omit "'_' is declared but its value is never read." compiler warning | ||
|
||
it("ApolloClient and QueryManager share a `defaultContext` instance (default empty object)", () => { | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: ApolloLink.empty(), | ||
}); | ||
|
||
expect(client.defaultContext).toBe(client["queryManager"].defaultContext); | ||
}); | ||
|
||
it("ApolloClient and QueryManager share a `defaultContext` instance (provided option)", () => { | ||
const defaultContext = {}; | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: ApolloLink.empty(), | ||
defaultContext, | ||
}); | ||
|
||
expect(client.defaultContext).toBe(defaultContext); | ||
expect(client["queryManager"].defaultContext).toBe(defaultContext); | ||
}); | ||
|
||
it("`defaultContext` cannot be reassigned on the user-facing `ApolloClient`", () => { | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: ApolloLink.empty(), | ||
}); | ||
|
||
expect(() => { | ||
// @ts-ignore | ||
client.defaultContext = { query: { fetchPolicy: "cache-only" } }; | ||
}).toThrowError(/Cannot set property defaultContext/); | ||
}); | ||
|
||
it("`defaultContext` will be applied to the context of a query", async () => { | ||
let context: any; | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: new ApolloLink( | ||
(operation) => | ||
new Observable((observer) => { | ||
({ cache: _, ...context } = operation.getContext()); | ||
observer.complete(); | ||
}) | ||
), | ||
defaultContext: { | ||
foo: "bar", | ||
}, | ||
}); | ||
|
||
await client.query({ | ||
query: gql` | ||
query { | ||
foo | ||
} | ||
`, | ||
}); | ||
|
||
expect(context.foo).toBe("bar"); | ||
}); | ||
|
||
it("`ApolloClient.defaultContext` can be modified and changes will show up in future queries", async () => { | ||
let context: any; | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: new ApolloLink( | ||
(operation) => | ||
new Observable((observer) => { | ||
({ cache: _, ...context } = operation.getContext()); | ||
observer.complete(); | ||
}) | ||
), | ||
defaultContext: { | ||
foo: "bar", | ||
}, | ||
}); | ||
|
||
// one query to "warm up" with an old value to make sure the value | ||
// isn't locked in at the first query or something | ||
await client.query({ | ||
query: gql` | ||
query { | ||
foo | ||
} | ||
`, | ||
}); | ||
|
||
expect(context.foo).toBe("bar"); | ||
|
||
client.defaultContext.foo = "changed"; | ||
|
||
await client.query({ | ||
query: gql` | ||
query { | ||
foo | ||
} | ||
`, | ||
}); | ||
|
||
expect(context.foo).toBe("changed"); | ||
}); | ||
|
||
it("`defaultContext` will be shallowly merged with explicit context", async () => { | ||
let context: any; | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: new ApolloLink( | ||
(operation) => | ||
new Observable((observer) => { | ||
({ cache: _, ...context } = operation.getContext()); | ||
observer.complete(); | ||
}) | ||
), | ||
defaultContext: { | ||
foo: { bar: "baz" }, | ||
a: { b: "c" }, | ||
}, | ||
}); | ||
|
||
await client.query({ | ||
query: gql` | ||
query { | ||
foo | ||
} | ||
`, | ||
context: { | ||
a: { x: "y" }, | ||
}, | ||
}); | ||
|
||
expect(context).toEqual( | ||
expect.objectContaining({ | ||
foo: { bar: "baz" }, | ||
a: { b: undefined, x: "y" }, | ||
}) | ||
); | ||
}); | ||
|
||
it("`defaultContext` will be shallowly merged with context from `defaultOptions.query.context", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually this test brings up a good question: What is the value of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for completeness (we talked about this in person) - the |
||
let context: any; | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: new ApolloLink( | ||
(operation) => | ||
new Observable((observer) => { | ||
({ cache: _, ...context } = operation.getContext()); | ||
observer.complete(); | ||
}) | ||
), | ||
defaultContext: { | ||
foo: { bar: "baz" }, | ||
a: { b: "c" }, | ||
}, | ||
defaultOptions: { | ||
query: { | ||
context: { | ||
a: { x: "y" }, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
await client.query({ | ||
query: gql` | ||
query { | ||
foo | ||
} | ||
`, | ||
}); | ||
|
||
expect(context.foo).toStrictEqual({ bar: "baz" }); | ||
expect(context.a).toStrictEqual({ x: "y" }); | ||
}); | ||
|
||
it( | ||
"document existing behavior: `defaultOptions.query.context` will be " + | ||
"completely overwritten by, not merged with, explicit context", | ||
async () => { | ||
let context: any; | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache(), | ||
link: new ApolloLink( | ||
(operation) => | ||
new Observable((observer) => { | ||
({ cache: _, ...context } = operation.getContext()); | ||
observer.complete(); | ||
}) | ||
), | ||
defaultOptions: { | ||
query: { | ||
context: { | ||
foo: { bar: "baz" }, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
await client.query({ | ||
query: gql` | ||
query { | ||
foo | ||
} | ||
`, | ||
context: { | ||
a: { x: "y" }, | ||
}, | ||
}); | ||
|
||
expect(context.a).toStrictEqual({ x: "y" }); | ||
expect(context.foo).toBeUndefined(); | ||
} | ||
); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the
b: undefined
here actually correct? Since its a shallow merge, I'd expecta
to just be{ x: 'y' }
with theb
key completely omitted. Would it be useful to make sure this test checks that? I think it passes right now because technically the value ofb
isundefined
in both cases, but it feels misleading to show it here since this makes it look like it does some kind of deep merge.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just my way of convincing
jest
to make sure thatb
isn't there or has no value.If I tested for
a: { x: "y" },
jest would also accept aa: { b: "still here", x: "y" },
as we are usingobjectContaining
(the real context has a bunch of other values I don't care about and that I don't want to specify in this test)