urql-graphql / urql

The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
https://urql.dev/goto/docs
MIT License
8.66k stars 454 forks source link

Announcement: urql v4 Major Releases #3114

Open kitten opened 1 year ago

kitten commented 1 year ago

It’s only been seven months since we’ve launched our urql v3 batch of major releases, but today, we’re even more excited to talk about our next batch of urql v4 releases!

What happened in the last episode of urql?
Expand for a quick summary of changes made in the prior urql v3 releases.
### General changes - We dropped IE11 support and modernised our build output - We introduced stricter `Variables` generics typings across our bindings - Granular `graphql` imports have been removed since they caused build issues in rare cases ### `@urql/core` - Not all `ClientOptions` are now reflects as properties on the `Client` - The `createOperationContext` method was removed from the `Client` - `ClientOptions.url` must now always be a string and defined ### `@urql/exchange-graphcache` - Easier `optimistic` functions - The `optimistic` functions now may return objects that contain more, nested `optimistic` functions - We now use field names rather than aliases, so it’s easier to define reusable `optimistic` functions - Offline Mode Non-Blocking - The offline rehydration is now non-blocking and a network request would always be attempted first


Back in our last batch of major releases, little changed and little broke, relatively speaking. We chose to save up larger refactors and changes for when we'd be able to ship some big improvements along with them; and today's the day!

Symbol Legend: We have a lot of changes to talk about, therefore, we’re not necessarily grouping everything by whether it’s breaking or not, and will instead annotate headings of sections according to whether they’re major, minor, or patch changes.

  • 💥: A major, breaking change, or an addition that may affect how urql used to work
  • ✨: A minor, feature change, which adds a new feature without breaking old code
  • 🦋: A patch, which often only improve what’s going on in the background

Table of Contents

After upgrading with your favourite package manager you may have duplicates of several packages, which you can fix by trying out the following command:

# for npm
npm dedupe
# for pnpm
pnpm dedupe
# for yarn
npx yarn-deduplicate

For the full release notes and changelogs, check the Release PR.


Sponsoring and Discord

In case you've missed it or don't come by the repository often, Hiya 👋 Welcome back!

We're also here to say that we now accept sponsorships on GitHub: https://github.com/sponsors/urql-graphql Sponsorships are important to us of course, but they will take more of a central role in how this project sustains itself moving forward. We have more moving parts now than ever and want to dedicate as much time to the urql community as possible! 💖

We also now have an easier way to ask help and chat with us and other contributors, by joining our Discord: https://urql.dev/discord Building a community on Discord allows us to also talk about and support you with our related projects and other libraries, like wonka and graphql-web-lite!

TSDocs... TSDocs everywhere ()

We’re replacing our API docs pages with TSDocs! 📚

Every one of our public API will now come with inline comments, called TSDocs, which give you a summary of what the API does, and they're in our opinion a big step forward from clunky, old API docs:

We hope that this will ease the learning curve of having to learn urql's APIs, and reduces common misconceptions and questions from popping up early on, when you're starting to adopt urql.

We're preparing to launch a web app that allows you to browse the APIs of our packages as well. More news on this soon...

No more defaultExchanges (💥)

In prior versions we didn't need you to explicitly pass exchanges to the client we would automatically make these to be [dedupExchange, cacheExchange, fetchExchange].

We have now removed this and need you to explicitly pass us the exchanges, in doing so we have reduced the default budndle size for folks not using these exchanges. You can fix the missing exchanges by doing the following:

import { createClient, cacheExchange, fetchExchange } from '@urql/core'

const client = createClient({
  url: '',
  exchanges: [cacheExchange, fetchExchange],
})

A rewrite of our default fetch/HTTP transport ()

urql's default fetchExchange used to come with a humble but extensible set of features. In @urql/core@^3.0.0, we're moving and adding more features into the core package:

With multipart/mixed and text/event-stream, we’re now supporting more protocol options for APIs, which enable APIs to use more transport methods for @defer, @stream, @live, and subscriptions and give API authors more options without having to add custom exchanges to urql.

Persisted Query Support is changing ()

As for, (Automatic) Persisted Queries support, this used to be implemented using the @urql/exchange-persisted-fetch library, which didn’t work for the subscriptionExchange, as it simply made a fetch/HTTP request on its own.

Instead, we’re deprecating the package and replacing it with @urql/exchange-persisted.

The API of this package will match the old one exactly, but it will only annotate query (and optionally, mutation) operations to have persisted query extensions on them.

Source Code

File Upload Support goes built-in ()

Multipart File Uploads are now supported in @urql/core and are activated when your variables contain a File or a Blob.

You won't have to install @urql/exchange-multipart-fetch anymore and this package has been deprecated.

hasNext, stale, and multiple results (💥)

@urql/core contains two subtle yet important changes to how OperationResults are defined and delivered in response to Operations.

In this version we're tightening our guarantees around results that are marked as hasNext: true and when results are marked as stale: true.

Any API transport that delivers multiple results may mark its ExecutionResults with hasNext: true. This for instance happens when:

When the Client sees hasNext, it nows consistently knows to expect updated results.

Furthermore, stale: true, as before, indicates that the Client expects an updated API result for a given query soon. For instance, this may mean that an operation is being sent and will replace an existing result with a new one as soon as the API responds.

Knock-on changes to the subscriptionExchange (💥)

The subscriptionExchange had to be changed a little to accommodate the new @urql/exchange-persisted support above and to fix some issues we were seeing in how it's being used.

Internally, @urql/core/internal contains a makeFetchBody function (and other fetch utilities that are reusable) which constructs the JSON data that will be sent to the GraphQL API. This function is aware of how to handle persisted queries.

Unfortunately, our subscriptionExchange's forwardSubscription function was instead accepting an Operation-like structure. That's why this has changed to now accept a FetchBody as the first argument.

This helps transports like graphql-ws to function correctly, as they pass this input on 1:1 without filtering it, expecting it to basically be like our FetchBody structure, ready to be accepted by GraphQL APIs.

If you were previously relying on this first argument having a context property — the OperationContext — we're now instead passing the entire Operation as a second argument to forwardSubscription

code>@urql/core</code ditches the graphql package (💥)

@urql/core doesn’t rely on a peer dependency on graphql anymore.

When you installed @urql/core@^3.2.2, you will at least add 17.3kB (gzip) to your bundlesize, since it not only includes its own code but also parts of graphql (and wonka, our stream utilities.) We wouldn't consider this bad, but we can do better.

In @urql/core@^4.0.0, we’re replacing the parts of graphql we used to use (which includes parse, print, and GraphQLError). Instead, we’re now relying on a homegrown, spec-compliant implementation of the GraphQL query language, the @0no-co/graphql.web package.

This small change means @urql/core@^4.0.0 will only add at most 9.9kB (gzip) to your bundlesize!

How does this affect TypeScript and the standard graphql library?

Any output from @0no-co/graphql.web is tested (with 100% test coverage; if this puts you at ease) to be compatible with the reference output from the graphql library. This means all ASTs will remain compatible.

When you have the standard graphql library installed, all @urql/core APIs will also automatically accept types from graphql (e.g. import('graphql').DocumentNode) and @0no-co/graphql.web will additionally switch to using graphql’s AST types as well.

How do I reduce my bundlesize if I depend on graphql myself?

If any of the rest of your app uses graphql already, we do have another trick up our sleeves, if you want to avoid a slight increase in bundlesize of 2kB (gzip).

You can use the graphql-web-lite package and alias graphql imports to it. The graphql-web-lite repostory contains instructions using which you can alias the graphql package in just a few minutes! ⏲️ We hope, even faster than it’d take you to make a coffee.

The graphql-web-lite package is built to slim down the default build of graphql, and is built against the graphql library, but also uses @0no-co/graphql.web internally. Once you’ve aliased graphql to graphql-web-lite, you’ll continue to be able to use its API, but keep size to a minimum.

Easier core-usage (🦋)

Sources now have .then() set which means that you can leverage await by default on sources returned by urql, this enable you to perform await client.mutation().

Sources also have .subscribe() now which accepts a callback that allows you to see all values passed back from the exchanges.

const { unsubscribe } = client.query().subscribe(operationResult => {
  console.log(operationResult)
})

No more need for the dedupExchange (🦋)

The dedupExchange has long been needed in @urql/core as a way to avoid sending multiple requests for operations subscribed to from multiple locations. This has now been added as default behavior to the client.

This means you can drop the dedupExchange all together, the exchange will be a noop in this major.

Integrations

Fixing code>@urql/svelte</code's missing subscription handler (💥)

When refactoring our Svelte bindings in a prior revision, we accidentally misplaced the handleSubscription support and it wasn't possible to merge subscription events like on our other bindings.

This has now been fixed and the subscriptionStore accepts it again as a second argument.

import { subscriptionStore, gql, getContextClient } from '@urql/svelte';

const todos = subscriptionStore({
  client: getContextClient(),
  query: gql`
    subscription {
      newNotification { id, text }
    }
  `,
}, function combineNotifications(notifications = [], data) {
  return [...notifications, data.newNotification];
});

urql and code>@urql/preact</code no longer create a default Client (💥)

In prior versions, urql and @urql/preact would create a default Client when no Provider was used in the tree. This by default would send GraphQL requests to /graphql. This wasn't really useful as you would need to add a Provider anyway when you went to production. We have removed this in this major and instead throw an error when we miss a Provider.

negezor commented 1 year ago

File upload seems to be broken as I found the key {"__key":"<random>"} instead of null in operations. https://github.com/jaydenseric/graphql-multipart-request-spec#multipart-form-field-structure

kitten commented 1 year ago

@negezor: I just checked this against our example for file uploads and it works just fine 😅 (although noting that the example API currently is down for uploads). You can check the resulting request here: https://user-images.githubusercontent.com/2041385/229220689-6126358b-8bb8-4d74-bdc0-b27190d7eed2.png

The behaviour of variables serialisation hasn't changed, so File, Blob, and other non-serializable objects are still replaced by random (but identity stable) strings, i.e. {__key:"[string]"}, as you're seeing.

However, as long as a File and Blob is inside the request itself the actual GraphQL request will switch over to using a multipart request (as per the screenshot), which then specifies where in the variables the file should be inserted at, as per the spec.

(btw, If you'd like help, please open an issue, discussion or feel free to chat on Discord ✌️)

Edit: Ah, you know what; I didn't realise this but I believe the input file map should be a record of string: [string]. I have no idea why and I'm getting a little sick and tired of the spec being very handwavy, but I'll check whether that's the issue

Edit 2: Fix is out. I think it's pretty trivial, so I'll just yeet it out quickly 😄

negezor commented 1 year ago

@kitten Specifically, in my case, this does not work, I get a response from the server: Invalid files map: invalid type: string "variables.input.avatar", expected a sequence at line 1 column 29

If you look at the specification, null is expected there

Payload

Using the old multipartFetchExchange solves the problem. However, it has already been deprecated.

kitten commented 1 year ago

Again, please do stick to issues please for bug reports 😅 That said, while I didn't bother creating an issue for bug tracking for this, @urql/core@4.0.1 does fix this. This also really doesn't have anything to do with the non-null value in variables, but with the FormData's map value

hawkeye64 commented 1 year ago

My issue #3144 was closed and redirected here. I still don't understand what needs to be done to resolve the issue I am having. I don't think the docs have been updated. I'll be forced to roll back until I have a clear view of what needs to be done.

In the meantime, if anyone else has a similar issue (and using yarn), I resolved it with this in package.json

  "resolutions": {
    "@urql/core": "3.2.2"
  },

Be sure to remove node_modules and yarn.lock and then rerun yarn.

kitten commented 1 year ago

@hawkeye64 your issue was not closed, just fyi, as stated there it was converted to a Q&A discussion: https://github.com/urql-graphql/urql/discussions/3145

The section there I've linked you is the "No More Default Exchanges" section on this page. The section up here will tell you what to do, namely to specify an explicit exchanges array option when creating the client, as the default has been removed for better treeshaking when the defaults aren't in use.

So, pass exchanges: [cacheExchange, fetchExchange] and that replaces the old default. More details in the original document in this issue. Generally these things are caugh]t by TypeScript, if it's used, which is why we only document these changes in migration docs. However, all of our docs on the docs site should be updated to reflect this change already. Do let me know though if you found one that doesn't :v:

A resolution is not good practice in this case at least, and while it should work it will force the bindings to work with a core package that is out of their explicitly set range. Furthermore, even if your resolution is in range, you'll from then on often miss out on updates and many tools will fail to notify you about them or override the resolution. (Also for clarity's sake, this is Yarn's syntax; other package managers may have other solutions for resolutions, so careful if someone here copies this)

To also repeat this here, since it has come up in other issues. Each package manager supports upgrading and updating. When updating a bindings package (e.g. for React, Vue, or urql) with the "upgrade" command or "add/install" commands, @urql/core will usually also be updated automatically.

If not, all package managers have an "update" command, to update a transitive dependency like @urql/core. Alternatively, an explicit "add/install" will also work.

Afterwards, you may accidentally have old versions of @urql/core still stuck around, especially if you haven't upgraded all other exchanges, or are using Yarn v1. In any case, duplicates, e.g. of @urql/core, can be deduplicated using one of the following commands, depending on the package manager used:

# for npm
npm dedupe
# for pnpm
pnpm dedupe
# for yarn
npx yarn-deduplicate

This is always in the migration guides and we add these commands when we bump out of range and duplicates become more likely.

Hope this clarifies some FAQs 😸

hawkeye64 commented 1 year ago

@kitten Fair enough on converting it to a discussion. I guess GitHub closes it and then recreates it as a discussion.

image

Thanks for the info. I'll have to read up more on the exchanges.

TroyKomodo commented 1 year ago

Hi, previously I was doing something like this.

/// This is a bit hard to understand, but it's basically a custom exchange that
// will send all operations to the websocket if it's open, and to the HTTP
// endpoint if it's not. Websockets take some time to connect
// so we dont want to delay the user.

const operationExchange: Exchange = (input) => {
    const wsExchange = wsSink(input);
    const httpExchange = fetchExchange(input);

    return (ops$) => {
        // We have to share the operations here because if the kind is teardown we forward it to both streams.
        // Teardowns are used to cancel requests, so we need to forward them to both streams.
        const sharedOps$ = pipe(ops$, share);

        // Here we filter if the websocket is open or if the kind is subscription or teardown.
        // We want to use the websocket as much as possible so we send all operations if it is open.
        // We also need to forward all teardowns and subscriptions regardless of the websocket state.
        const wsPipe$ = pipe(
            sharedOps$,
            filter((op) => get(websocketOpen) || op.kind === "subscription" || op.kind === "teardown"),
            wsExchange,
        );

        // Here we use it as a fallback if the websocket is not open, however we still need to forward teardowns even if the websocket is open.
        const httpPipe$ = pipe(
            sharedOps$,
            filter((op) => !get(websocketOpen) || op.kind === "teardown"),
            httpExchange,
        );

        // At the end we need to merge both streams together and return a single result stream.
        return merge([wsPipe$, httpPipe$]);
    };
};

On the new release I get

app.js:20 Error: forward() must only be called once in each Exchange.

Is there a way to achieve similar functionality with the new release?

kitten commented 1 year ago

I see that it's basically like a "terminating" exchange, but from urql's perspective, no exchange ever has to handle all operations, so it's expected they'll always pass on what they don't handle.

You can either call forward(never), with wonka's never export, or what you can actually create a pass-through case.

What I mean is, for instance, even the subscriptionExchange has a passthrough case: https://github.com/urql-graphql/urql/blob/76ad6191dd193a6e9433acc7ccb1b4a1ac6461b3/packages/core/src/exchanges/subscription.ts#L219-L227 So you could split the fetchExchange out again and leave it as a separate exchange.

However, if you need a quick fix forward(never) is definitely the quickest, and essentially the same as:

return merge([
  wsPipe$,
  httpPipe$,
  forward(pipe(operations, filter(() => false)),
]);

You could also do:

forward(
  pipe(operations,
  filter((operation) => operation.kind === 'teardown'),
)

That's of course unnecessary in your case, but typically, the built-in exchanges will always pass those on, in case there's more exchanges in the chain receiving future operations — however unlikely that may be

TroyKomodo commented 1 year ago

Well, not exactly a terminating exchange. Essentially I was conditionally toggling the fetch exchange based on if the websocket was open and ready.

exchanges after this exchange would still be called.

kitten commented 1 year ago

Ah, I misread the part where you're instantiating the exchanges. That's indeed where forward is used twice. So, the only way to stop that is to wrap the forward function you're receiving, call it yourself once and share it, then return it in a new function that those exchanges receive

Edit: i.e.

const result$ = share(forward(never));
const wsExchange = wsSink({ ...input, forward: () => result$ });
const httpExchange = fetchExchange({ ...input, forward: () => result$ });

I suppose, we didn't yet look at whether we need to add a splitExchange helper for this, but we didn't really deem it important enough, since it's still possible

TroyKomodo commented 1 year ago
const wsExchange: Exchange = ({ client, dispatchDebug, forward}) => {
    const wsExchange = wsSink({
        client,
        dispatchDebug,
        forward: () => never, 
    });

    return (ops$) => {
        // Here we filter if the websocket is open or if the kind is subscription or teardown.
        // We want to use the websocket as much as possible so we send all operations if it is open.
        // We also need to forward all teardowns and subscriptions regardless of the websocket state.
        const wsPipe$ = pipe(
            ops$,
            filter((op) => get(websocketOpen) || op.kind === "subscription" || op.kind === "teardown"),
            wsExchange,
        );

        // Here we use it as a fallback if the websocket is not open, however we still need to forward teardowns even if the websocket is open.
        const forward$ = pipe(
            ops$,
            filter((op) => (!get(websocketOpen) || op.kind === "teardown") && op.kind !== 'subscription'),
            forward,
        );

        // At the end we need to merge both streams together and return a single result stream.
        return merge([wsPipe$, forward$]);
    };
};

ended up with this thanks!

samavati commented 1 year ago

Subject: Missing "__typename" in query results after migrating from urql v3 to v4.0.5

Description: I recently updated the urql package from version 3 to the latest version (v4.0.5) as per the migration steps mentioned in the documentation. However, I'm facing an issue where the "__typename" field is no longer present in the query results. This missing field is causing problems in my application, as I rely on it for certain functionalities.

Steps to Reproduce:

Updated urql package from v3 to v4.0.5 using the provided migration steps. Executed the GraphQL queries that previously returned the "typename" field. Expected Behavior: I expected the "typename" field to be included in the query results, just like it was in urql v3.

Actual Behavior: The "__typename" field is missing from the query results in urql v4.0.5.

Additional Information:

OS: MacOS Node.js version: v20.5.5 urql version: 4.0.5 I have verified that the issue is not due to any changes in my GraphQL schema or queries since it worked correctly in urql v3. The "__typename" field is crucial for my application's functionality, and its absence is causing unexpected behavior.

Please let me know if there's any additional information I can provide or if there's a workaround to bring back the "__typename" field in urql v4.0.5.

Thank you for your help!

JoviDeCroock commented 1 year ago

@samavati Are you querying the __typename field in the selections where you need them? If you are would you mind creating a codesandbox where this is issue is reproduced, if you are not it can be fixed by querying the __typename field as before we had a few bugs where this was erroneously returned when not queried.

samavati commented 1 year ago

@JoviDeCroock Thank you :) This resolved my problem 🎉