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.64k stars 451 forks source link

urql v3 Major Releases: ES2015+ and more! #2621

Closed kitten closed 1 year ago

kitten commented 2 years ago

As part of the latest major releases across all packages we'd like to highlight some of the differences and provide some context and instructions around this release!

Backwards Compatibility

urql has always been architected to be a "stable core" project. This meant that the architecture, as we first set it has seen very little change since the inception of the project.

We still stand by this and would like to highlight that the coming major releases still remain mostly backwards compatible.

We make some exceptions here, and all breaking changes are listed later. However, while Wonka has been updated to v6, all your exchanges and usages of the Client should mostly remain unchanged!

While we're avoiding breaking changes as much as possible (and even more so when it comes to our core architecture and protocols), we're taking this opportunity to introduce some breakage to clean up old behaviour we wanted to adjust and tweak for a while.

First up, we do recommend you to upgrade all packages at once, however, you'll also able to upgrade partially. All exchanges and packages remain intercompatible across versions, and you'd only be duplicating the wonka package.

Client and Exchanges

No internal API contracts have been broken, and exchanges and how they interact with the Client still remain the same.

Wonka and Streams

If you only upgrade some packages you may temporarily have two installations of wonka, namely wonka@4.0.15 and wonka@6.0.0.

Wonka is a streaming library, inspired by the Callbag specification, similar to Observables, but written to be more minimal and small in size. It drives our internal logic in the Client, how you interact with it, and how exchanges work.

Both versions are 100% intercompatible and can coexist in the same app. Its internal data structures and API contracts have also not changed.

Wonka has been rewritten in TypeScript to ensure its future longevity and make it more beginner friendly to new contributors. It was previously written in ReScript/ReasonML, an OCaml derivative and compiled with BuckleScript to JavaScript.

Breaking Changes

We'd like to take a second to highlight the changes that definitely are breaking and that you should take special note of.

Goodbye IE11 / Targeting ES2015

Most importantly, in this batch of releases, we're deprecating our support for Internet Explorer 11 and are instead targeting ES2015+ from now on. This will likely not affect you to much.

If you want to compare what we support to what you support, we're basically sticking to evergreen browsers with some exceptions. In a browserslist rules format this looks like this:

last 3 ios_saf versions
last 3 and_chr major versions
op_mini > 99
and_uc > 99
last 3 samsung major versions
last 3 opera versions
last 3 safari versions
last 10 chrome major versions
last 8 firefox major versions
ie > 99
last 3 edge versions

The tl;dr of the above is that we support ES6 / ES2015 and that you should expect our code to not work in Internet Explorer 11 any longer. Other browsers like Chromium, Firefox, and Safari are therefore unaffected.

Stricter Variable Types

Previously, if you've used type generatords (like GraphQL Code Generator with a React hooks output for TypedDocumentNodes) you were unfortunately able to leave out variables from your GraphQL operations when passing in the query argument.

This was a mistake and we're explicitly removing this behaviour. We're now enforcing client.query(query, variables) to require variables when the query is typed.

Graphcache Optimistic Updates

When writing optimistic updates, you previously had to match your optimistic objects to the field aliases of the mutation that you defined. This was our previous mechanism to ensure that you could support multiple mutations with different selection sets.

This was a poor workaround and instead, we now properly look every field up by its name in your optimistic objects, as you'd expect, since it matches the behaviour of resolvers.

You may still support multiple mutations with different selection sets by passing in nested optimistic functions in your optimistic objects! In short, this is now possible:

cacheExchange({
  optimistic: {
    favoriteTodo(variables, cache, info) {
      return {
        __typename: 'Todo',
        id: variables.id,
        // You can dynamically return values here:
        favorite(args, cache, info) {
          return true;
        },
      },
    },
  },
});

Graphcache's Offline Rehydration

Previously, Graphcache would block all operations while it's rehydrating from its offline storage. Our default storage strategy is based on IndexedDB and your alternative storages (for instance for React Native via @urql/storage-rn) may also be asynchronous.

To deal with this we used to queue up operations and would only allow execution to continue once rehydration was complete. However, this made server-side rendering rehydration impossible, as it'd interfere with the first render.

We now instead eagerly execute operations, even sending network requests as necessary and "race" the offline storage against network requests.

Slimming down the Client

@urql/core's Client, the centre of all GraphQL operations, previously had a lot of properties and methods it didn't need anymore, which were leftovers of its first implementation, believe it or not.

Since, they weren't used a lot and are just sitting there, waiting to be monkey-patched in unintended ways, and are just adding to our bundle-size, we've removed them. They were already not documented before, but we still waited for a major release to fully get rid of them.

The properties that are now gone are:

Accept for the first one here, as you can see, all of these were ClientOptions that were copied over to the Client instance and could be modified on the fly. This wasn't an intentional feature, and we now recommend you to instead use the new @urql/context's contextExchange instead, if you reply on updating any of the OperationContext options dynamically.

ClientOptions.url is assumed to be a URL

In previous versions of urql we refrained from doing anything fancy to the url option that was passed in. It'd simplt end up being what fetch(url) would be called with.

However, when we in the past introduced preferGetMethod and @urql/exchange-persisted-fetch this meant that GET methods were becoming part of @urql/core. This in turn meant that URL manipulation became necessary.

In this major release of @urql/core we've switched over to using the Web Standard built-ins URL and URLSearchParams. This is less error prone. (Previously we didn't support preferGetMethod being enabled while search parameters were passed to ClientOptions.url, for instance)

As the name implies, ClientOptions.url was always meant to be a qualified URL, but '/graphql' (in fact, not a full URL) made its way into several parts of the documentation. We do in fact recommend to always construct a full URL string before passing it to urql.

At the very least, passing '/graphql' can be replaced (when no server-side use is involved) with new URL('/graphql', document.location.origin).toString().

Edit: We forgot to mention this breaking change on our list currently. But you'll only run into it if you're using GET methods with GraphQL

Removing Granular Imports

Previously, we used to resolve imports from graphql in our output bundles to granular imports. This meant that our bundles could contain imports like:

import { visit } from "graphql/language/visitor.mjs";
import { Kind } from "graphql/language/kinds.mjs";
import { print } from "graphql/language/printer.mjs";

While this was great for bundlesize, at least for bundlers that failed at correctly treeshaking graphql, it wasn't great for our Node.js support.

That's why we're removing this, and if you rely on treeshaking to work correctly, please make sure that your bundler is able to only leave around the parts of graphql that you're actually using.

Alternatively, you could alias graphql in production to graphql-web-lite.

onionhammer commented 2 years ago

Stricter Variable Types Previously, if you've used type generatords (like GraphQL Code Generator with a React hooks output for TypedDocumentNodes) you were unfortunately able to leave out variables from your GraphQL operations when passing in the query argument.

Was this release coordinated with graphql-codegen? Because it seems to be borked now. I guess this will eventually trickle down to them and gradually people will be able to use this release?

Is the typing here right? 'variables' has a question mark (allowing undefined) on line 9 image

also

image

JoviDeCroock commented 2 years ago

@onionhammer I just checked in one of our bigger applications and the integration is still working correctly, could you reproduce the breakage?

onionhammer commented 2 years ago

@JoviDeCroock graphql-codegen's urql plugin (with the 'withHooks' option) will generate hooks with a signature of

export function useQueryProfileContextQuery(options?: Omit<Urql.UseQueryArgs<QueryProfileContextQueryVariables>, 'query'>)

Where that '?' after the 'options' is no longer valid, since 'variables' must be assigned.

kitten commented 2 years ago

@onionhammer Could you open an issue please if anything looks off? I can jump on that later today even if it blocks migrations. As far as I'm aware though, it should continue working with GCG just fine πŸ€” so I'm not quite sure where to look first

onionhammer commented 2 years ago

@kitten I will create a sample schema & codegen to repro

onionhammer commented 2 years ago

@kitten @JoviDeCroock

To reproduce:

Package.json

    "@graphql-codegen/cli": "^2.11.6",
    "@graphql-codegen/typescript": "^2.7.3",
    "@graphql-codegen/typescript-operations": "^2.5.3",
    "@graphql-codegen/typescript-urql": "^3.6.4",

GraphQL codegen:

schema: schema.graphql
documents: "./src/test.graphql"
generates:
  ./src/queries.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-urql
    config:
      withHooks: true
      exportFragmentSpreadSubTypes: true
      mergeFragmentTypes: true
      scalars:
        Decimal: number
        DateTime: string
        UUID: string

schema.graphql


type Environment {
  "Retrieve application version"
  version: String
}

type Query {
  "Gets current system environment"
  environment: Environment!
}

test.graphql

query env {
  environment {
    version
  }
}

This generates typescript with the following error:

image

const EnvDocument: DocumentNode Argument of type '{ requestPolicy?: Urql.RequestPolicy | undefined; context?: Partial | undefined; pause?: boolean | undefined; variables?: Exact<{ [key: string]: never; }> | undefined; query: DocumentNode; }' is not assignable to parameter of type 'UseQueryArgs<Exact<{ [key: string]: never; }>, EnvQuery>'. Type '{ requestPolicy?: Urql.RequestPolicy | undefined; context?: Partial | undefined; pause?: boolean | undefined; variables?: Exact<{ [key: string]: never; }> | undefined; query: DocumentNode; }' is not assignable to type '{ variables: Exact<{ [key: string]: never; }>; }'. Types of property 'variables' are incompatible. Type 'Exact<{ [key: string]: never; }> | undefined' is not assignable to type 'Exact<{ [key: string]: never; }>'. Type 'undefined' is not assignable to type 'Exact<{ [key: string]: never; }>'.ts(2345)

kitten commented 2 years ago

@onionhammer In lieu of a separate issue, I'll mark this as off -topic for now, but it looks like GCG adds undefined for the full variables as an option potentially which wouldn't be correct anymore πŸ€” not sure what's causing this though. In general, I'd recommend their Typed Document Node plugin

onionhammer commented 2 years ago

@kitten If you want to track it here you can just move my posts to a new issue, otherwise I'll just leave it be and stay on urql 2.x until this is addressed. The bug reporting on the graphql-codegen is pretty oppressively templated, so I'll just leave this here:

https://github.com/dotansimha/graphql-code-generator/blob/master/packages/plugins/typescript/urql/src/visitor.ts#L132

kitten commented 2 years ago

@onionhammer No, I can't move comments, hence I asked you to please open an issue, either on this repo or on GCG, depending

onionhammer commented 2 years ago

@kitten Ahh, my mistake. I guess you don't have this feature, since you marked it off topic that option disappeared - I will copy/paste it if you want.

image

kitten commented 2 years ago

Edit: We previously left this issue unlocked for quick feedback, but realised, it's likely better to lock it, for notices like the below πŸ˜… Please open an issue if you find a regression or issue.

Patch Notes

We had a couple of reports of regressions, which have been addressed in the last few days over patch releases (Thanks for the reports!). I'm listing them here just to provide a helpful update on what we've been tweaking, and what you can expect.

urql@3.0.1 / @urql/vue@1.0.1 / @urql/svelte@3.0.1 / @urql/preact@3.0.1

We found a small regression with GraphQL Code Generator, which showed that requiring variable types actually contained a small mistake.

What we failed to take into account was that multiple variables could be defined that will all be optional, which GCG then uses to mark the entire variables object as optional. We've fixed the typings to account for this.

@urql/vue@1.0.2

We discovered an issue that caused promise-based usage of the useQuery Vue function to fail, due to a small mistake in how we defined the immediate unsubscribe action when the Client returned a result.

Ultimately, this was caused because we stopped transpiling const/let to var, but we fixed the mistake in @urql/vie directly but also start transpiling const/let again to prevent this from happening proactively.

@urql/core@3.0.1

We previously changed how the Client applies the default request policy and passed in request policies, but didn't realise that useQuery in react-urql relies on the old behaviour and would hence cause requestPolicy to be set to undefined.

This has been fixed and we now correctly use defaults as passed to the Client or cache-first.