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

feat: React 18 + automatic JSX runtime + build --dev #8961

Merged
merged 33 commits into from
Jun 8, 2023
Merged

Conversation

slorber
Copy link
Collaborator

@slorber slorber commented May 5, 2023

Breaking changes

  • React 18 comes with a few breaking changes documented on the [upgrade guide](How to Upgrade to React 18). Most of them are handled for you by Docusaurus, but if you use third-party plugins or customize your site, you might need to update some code, in particular:
    • Automatic batching: the setState batching behavior has changed and it might lead to a slightly different behavior (example fix)
    • Hydration issues: React 18 is much stricter than before regarding client/server hydration mismatches, and you may get unexpected console errors. You should avoid using things like typeof window === "undefined" (or ExecutionEnvironment. canUseDOM) during the render phase (including useMemo, useCallback, useState initial value...), and prefer using our dedicated hook useIsBrowser). Read The Perils of Hydration to better understand the problem. You can get better error messages when building with the new --dev option.
    • Third-party plugins: when using third-party plugins, those plugins might not be compatible with that new version or React. In this case, you should report bugs to the plugins authors directly.
  • We now use the "automatic" JSX runtime and it's not needed to import React to use JSX
  • For TypeScript users, the new automatic JSX runtime will require updating your TS extended base config to our new package @docusaurus/tsconfig (see feat: create official TypeScript base config @docusaurus/tsconfig #9050)
  • Use startTransition for React hydration (in PR perf(core): use React 18 startTransition for hydration #9051)

Caution - Suspense and new features.

This PR upgrades React to v18 as a dependency. Please let us some time to experiment with the new React 18 features before introducing them to your codebase. It is possible that those features will not work as you expect:

  • <Suspense>
  • React.lazy()
  • useTransition() + startTransition()

We will let you know later once it's safe to use these new APIs inside Docusaurus.

Motivation

Main changes:

  • Upgrade Docusaurus to React 18
  • Use the "automatic" JSX runtime
  • Add a new npm run build --dev option to turn on full React error messages, useful for debugging hydration issues

Useful links:

Other notable changes in this PR:

  • Upgrade app entry point to new React 18 APIs
  • Fully refactor BaseUrlIssueBanner to avoid hydration errors
  • Fork and fix legacy IdealImage library to avoid setState batching and hydration issues: slorber/docusaurus-react-ideal-image
  • Fix all the Docusaurus website hydration errors in general
  • Extract useIsomorphicLayoutEffect in core (undocumented, temporary until React 19?)

Related issues:

Test Plan

tests + preview + argos + react-18 test page + dogfood

Test links

https://deploy-preview-8961--docusaurus-2.netlify.app/tests/pages/react-18

Related issues/PRs

Previous React 18 PRs:

Related links:

Useful docs links:

Inspirations to transform renderToPipeableStream to something awaitable for SSG:

@slorber slorber added pr: new feature This PR adds a new API or behavior. pr: breaking change Existing sites may not build successfully in the new version. Description contains more details. labels May 5, 2023
@facebook-github-bot facebook-github-bot added the CLA Signed Signed Facebook CLA label May 5, 2023
@netlify
Copy link

netlify bot commented May 5, 2023

[V2]

Name Link
🔨 Latest commit 66e0aa1
🔍 Latest deploy log https://app.netlify.com/sites/docusaurus-2/deploys/64820edd99af4100085c90ce
😎 Deploy Preview https://deploy-preview-8961--docusaurus-2.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@github-actions
Copy link

github-actions bot commented May 5, 2023

⚡️ Lighthouse report for the deploy preview of this PR

URL Performance Accessibility Best Practices SEO PWA Report
/ 🟠 68 🟢 97 🟢 92 🟢 100 🟠 89 Report
/docs/installation 🟠 76 🟢 100 🟢 92 🟢 100 🟠 89 Report

@slorber slorber marked this pull request as draft May 5, 2023 17:33
@github-actions
Copy link

github-actions bot commented May 5, 2023

Size Change: +31.7 kB (+3%)

Total Size: 1.04 MB

Filename Size Change
website/build/assets/js/main.********.js 784 kB +31.8 kB (+4%)
website/build/index.html 40.9 kB -127 B (0%)
ℹ️ View Unchanged
Filename Size
website/.docusaurus/globalData.json 101 kB
website/build/assets/css/styles.********.css 113 kB

compressed-size-action

@NMinhNguyen
Copy link

NMinhNguyen commented May 9, 2023

Would it also be possible to set the React runtime to automatic in

require.resolve('@babel/preset-react'),
?

It turns out that if you specify ['@babel/preset-react', { runtime: 'automatic' }] alongside Docusaurus' preset in babel.config.js, both React presets will be present (instead of one overriding the other). I think this is because the other React preset is embedded inside Docusaurus' one, and Babel doesn't attempt to dedupe such cases. I found this out when I included a /** @jsxImportSource @emotion/react */ comment and encountered an importSource cannot be set when runtime is classic. error.

Even if we were to ignore the issue above, with React 18 I'm not sure why you'd ever want to use the classic runtime instead of automatic anyway.

@luhc228
Copy link

luhc228 commented May 25, 2023

When will react 18 be supported?

@slorber slorber marked this pull request as ready for review June 8, 2023 16:34
@slorber
Copy link
Collaborator Author

slorber commented Jun 8, 2023

@NMinhNguyen I turned on the automatic JSX runtime as part of this PR

@luhc228 it will be supported very soon in canary releases, this PR is ready and will be merged very soon

@slorber slorber changed the title feat: React 18 + automatic JSX runtime feat: React 18 + automatic JSX runtime + build --dev Jun 8, 2023
@NMinhNguyen
Copy link

@NMinhNguyen I turned on the automatic JSX runtime as part of this PR

Great, thank you!

@slorber slorber merged commit 187e5aa into main Jun 8, 2023
@slorber slorber deleted the slorber/react-18 branch June 8, 2023 17:40
@ntucker
Copy link
Contributor

ntucker commented Jun 9, 2023

Anyone get

ERROR in ../docs/core/concepts/normalization.md
Module build failed (from ./node_modules/@docusaurus/mdx-loader/lib/index.js):
Error: MDX compilation failed for file "/home/ntucker/src/rest-hooks/docs/core/concepts/normalization.md"
Cause: chunks[startIndex].slice is not a function
Details:
{}
    at Object.mdxLoader (/home/ntucker/src/rest-hooks/website/node_modules/@docusaurus/mdx-loader/lib/loader.js:111:25)
normalization.md

title: Entity and Data Normalization
sidebar_label: Data Normalization

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import LanguageTabs from '@site/src/components/LanguageTabs';

Entities have a primary key. This enables easy access via a lookup table.
This makes it easy to find, update, create, or delete the same data - no matter what
endpoint it was used in.

<Tabs
defaultValue="State"
values={[
{ label: 'State', value: 'State' },
{ label: 'Response', value: 'Response' },
{ label: 'Endpoint', value: 'Endpoint' },
{ label: 'Entity', value: 'Entity' },
{ label: 'React', value: 'React' },
]}>

Entities cache

[
  { "id": 1, "title": "this is an entity" },
  { "id": 2, "title": "this is the second entity" }
]
const PresentationList = new Endpoint(
  () => fetch(`/presentations`).then(res => res.json()),
  { schema: [PresentationEntity] },
);
class PresentationEntity extends Entity {
  readonly id: string = '';
  readonly title: string = '';

  pk() {
    return this.id;
  }
}
export function PresentationsPage() {
  const presentation = useSuspense(PresentationList, {});
  return presentation.map(presentation => (
    <div key={presentation.pk()}>{presentation.title}</div>
  ));
}

Extracting entities from a response is known as normalization. Accessing a response reverses
the process via denormalization.

:::info Global Referential Equality

Using entities expands Rest Hooks' global referential equality guarantee beyond the granularity of
an entire endpoint response.

:::

Mutations and Dynamic Data

When an endpoint changes data, this is known as a side effect. Marking an endpoint with sideEffect: true
tells Rest Hooks that this endpoint is not idempotent, and thus should not be allowed in hooks
that may call the endpoint an arbitrary number of times like useSuspense() or useFetch()

By including the changed data in the endpoint's response, Rest Hooks is able to able to update
any entities it extracts by specifying the schema.

<Tabs
defaultValue="Create"
values={[
{ label: 'Create', value: 'Create' },
{ label: 'Update', value: 'Update' },
{ label: 'Delete', value: 'Delete' },
]}>

import { RestEndpoint } from '@rest-hooks/rest';

const todoCreate = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos',
  method: 'POST',
  schema: Todo,
});
Example Usage
import { useController } from '@rest-hooks/react';

export default function NewTodoForm() {
  const ctrl = useController();
  return (
    <Form onSubmit={e => ctrl.fetch(todoCreate, new FormData(e.target))}>
      <FormField name="title" />
    </Form>
  );
}
import { RestEndpoint } from '@rest-hooks/rest';

const todoUpdate = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  method: 'PUT',
  schema: Todo,
});
Example Usage
import { useController } from '@rest-hooks/react';

export default function UpdateTodoForm({ id }: { id: number }) {
  const todo = useSuspense(todoDetail, { id });
  const ctrl = useController();
  return (
    <Form
      onSubmit={e => ctrl.fetch(todoUpdate, { id }, new FormData(e.target))}
      initialValues={todo}
    >
      <FormField name="title" />
    </Form>
  );
}
import { schema, RestEndpoint } from '@rest-hooks/rest';

const todoDelete = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  method: 'DELETE',
  schema: new schema.Delete(Todo),
});
Example Usage
import { useController } from '@rest-hooks/react';

export default function TodoWithDelete({ todo }: { todo: Todo }) {
  const ctrl = useController();
  return (
    <div>
      {todo.title}
      <button onClick={() => ctrl.fetch(todoDelete, { id: todo.id })}>
        Delete
      </button>
    </div>
  );
}

:::info

Mutations automatically update the normalized cache, resulting in consistent and fresh data.

:::

Schema

Schemas are a declarative definition of how to process responses

import { RestEndpoint } from '@rest-hooks/rest';

const getTodoList = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos',
  // highlight-next-line
  schema: [Todo],
});

Placing our Entity Todo in an array, tells Rest Hooks to expect
an array of Todos.

Aside from array, there are a few more 'schemas' provided for various patterns. The first two (Object and Array)
have shorthands of using object and array literals.

  • Object: maps with known keys
  • Array: variably sized arrays
  • Union: select from many different types
  • Values: maps with any keys - variably sized
  • Invalidate: remove an entity

Learn more

Nesting

Additionally, Entities themselves can specify nested schemas
by specifying a static schema member.

<Tabs
defaultValue="Entity"
values={[
{ label: 'Entity', value: 'Entity' },
{ label: 'Response', value: 'Response' },
]}>

import { Entity } from '@rest-hooks/endpoint';

class Todo extends Entity {
  readonly id: number = 0;
  readonly user: User = User.fromJS({});
  readonly title: string = '';
  readonly completed: boolean = false;

  pk() {
    return `${this.id}`;
  }

  // highlight-start
  static schema = {
    user: User,
  };
  // highlight-end
}

class User extends Entity {
  readonly id: number = 0;
  readonly username: string = '';

  pk() {
    return `${this.id}`;
  }
}
{
  "id": 5,
  "user": {
    "id": 10,
    "username": "bob"
  },
  "title": "Write some Entities",
  "completed": false
}

Learn more

Data Representations

Additionally, any newable class that has a toJSON() method, can be used as a schema. This will simply construct the object during denormalization.
This might be useful with representations like bignumber

import { Entity } from '@rest-hooks/endpoint';

class Todo extends Entity {
  readonly id: number = 0;
  readonly user: User = User.fromJS({});
  readonly title: string = '';
  readonly completed: boolean = false;
  // highlight-next-line
  readonly dueDate: Date = new Date(0);

  pk() {
    return `${this.id}`;
  }

  static schema = {
    user: User,
    // highlight-next-line
    dueDate: Date,
  };
}

:::info

Due to the global referential equality guarantee - construction of members only occurs once
per update.

:::

with the canary build?

@slorber
Copy link
Collaborator Author

slorber commented Jun 9, 2023

@ntucker that does not seem related to React 18, but more likely related to MDX 2

This looks related to this bug: #9084

For any MDX v2 upgrade support request, please use this discussion instead: #9053

This was referenced Oct 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Signed Facebook CLA pr: breaking change Existing sites may not build successfully in the new version. Description contains more details. pr: new feature This PR adds a new API or behavior.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

testing-library - Dependency issue with react-types Migrate to React 18
5 participants