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

Toaster.create() uses ReactDOM.render which is being deprecated in React 18 #5212

Closed
switz opened this issue Mar 31, 2022 · 8 comments
Closed

Comments

@switz
Copy link
Contributor

switz commented Mar 31, 2022

Environment

  • Package version(s): 4.0.3
  • Operating System: Mac stable
  • Browser name and version: Firefox stable

https://github.com/palantir/blueprint/blob/develop/packages/core/src/components/toast/toaster.tsx#L130

        const toaster = ReactDOM.render<IToasterProps>(
            <Toaster {...props} usePortal={false} />,
            containerElement,
        ) as Toaster;

In console:

Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

Actual behavior

React warns us against using ReactDOM.render

Expected behavior

Blueprint should not use ReactDOM.render for a toast

Possible solution

Relevant thread:

chakra-ui/chakra-ui#5795 (comment)

The recommendation is to use a Portal. It is not recommended to have multiple roots for things like toasts etc.

@adidahiya
Copy link
Contributor

Can you work around this by using one of the alternative Toaster APIs?

@adidahiya adidahiya changed the title Toaster uses ReactDOM.render which is being deprecated in React 18 Toaster.create() uses ReactDOM.render which is being deprecated in React 18 Mar 31, 2022
@aikoven
Copy link

aikoven commented May 12, 2022

I've been able to work this around by replacing this:

export const toaster = Toaster.create();

with this:

export let toaster: Toaster;

createRoot(document.getElementById('toaster')!).render(
  <Toaster
    ref={instance => {
      toaster = instance!;
    }}
  />,
);

and manually adding DOM element to the body:

<body>
  <div id="root"></div>
  <div id="toaster"></div> <!-- <--- added this -->
</body>

@alberto-f
Copy link

Is there any date for getting a fix to this one ?

Im thinking of ways to get around this but I don't come with any solution. We were using Toaster.create to get an IToaster instance when importing a module. This instance would be the only one we would use for showing toasts. It attaches itself to the document.body if no container is passed. This part is really convenient as your tests do not need to care about the container to which they attach. They are using document.body as the default one.

https://github.com/palantir/blueprint/blob/develop/packages/core/src/components/toast/toaster.tsx#L127

I can use one of the alternative methods to get a reference to a Toaster but that means that Im coupling the Toaster to a fixed component in my app.

<Router>
    <App />
    // Couple Toaster to this very specific component.
    // I cannot test that toaster is shown unless I bring this whole component
    <Toaster
    className="main-toaster"
    ref={mainToasterRef}
  />
  </Router>,

The issue with doing this is that I cannot test toasts are shown in integration tests unless I bring this very specific component.

Is there any solution that Im missing ?

@adidahiya adidahiya added this to Needs triage in DX paper cuts via automation Jul 25, 2022
@adidahiya adidahiya added this to the 5.0.0 milestone Jul 25, 2022
@adidahiya adidahiya self-assigned this Jul 25, 2022
@adidahiya adidahiya removed this from the 5.0.0 milestone Aug 22, 2022
@xtecox
Copy link

xtecox commented Oct 27, 2022

Any updates on this?

@adidahiya
Copy link
Contributor

@xtecox updates on what, exactly? There are some alternative APIs mentioned above that should allow you to work around the ReactDOM.render() in React 18. We're planning to remove ReactDOM.render() from Toaster in Blueprint v6.0.

@adidahiya adidahiya added this to the 6.0.0 milestone Oct 28, 2022
@adidahiya adidahiya removed their assignment Oct 28, 2022
@lmk123
Copy link
Contributor

lmk123 commented Nov 17, 2022

I have created a small module, hope it helps. You can install it from @hcfy/create-toaster, or use the following code in your project

import { IToasterProps, Toaster } from '@blueprintjs/core'
import { createRoot } from 'react-dom/client'

export default function createToaster(
  props?: IToasterProps,
  container = document.body
) {
  const containerElement = document.createElement('div')
  container.appendChild(containerElement)
  const root = createRoot(containerElement)
  return new Promise<Toaster>((resolve, reject) => {
    root.render(
      <Toaster
        {...props}
        usePortal={false}
        ref={(instance) => {
          if (!instance) {
            reject(new Error('[Blueprint] Unable to create toaster.'))
          } else {
            resolve(instance)
          }
        }}
      />
    )
  })
}

Before:

const toaster = Toaster.create(props, container)

After:

const toaster = await createToaster(props, container)

@ra50
Copy link

ra50 commented Aug 11, 2023

Adding a Toaster component somewhere on the page like this

<OverlayToaster ref={(ref: OverlayToaster) => (toaster = ref)} />
with let toaster: OverlayToaster somewhere at the top was the easiest workaround for me.

@gluxon
Copy link
Contributor

gluxon commented Jan 8, 2024

Hi folks — We've merged a new OverlayToaster.createAsync method that will be available in the next @blueprintjs/core release. #6599

The next version isn't published yet. As a preview for when it is published, the new createAsync method can be customized to use React 18's createRoot instead of the older deprecated ReactDOM.render. It works similarly to @lmk123's example above.

import { OverlayToaster } from "@blueprintjs/core";
import { createRoot } from "react-dom/client";

const toaster = await OverlayToaster.createAsync(/* props */ {}, {
  domRenderer: (toaster, containerElement) => createRoot(containerElement).render(toaster),
});

toaster.show({ message: "Hello React 18!" })

At some point in the future, Blueprint will require React 18 and drop support for React 16 and 17. When this happens, the createAsync method will use React 18 by default and the domRenderer option will no longer need to be customized.


Details

To account for changes in React 18, the new OverlayToaster.createAsync API works a bit differently than OverlayToaster.create. Copying a few question + answers from the PR.

Why is the new API asynchronous?

The change to Blueprint's API reflects a change in React's API. The new createRoot function from react-dom/client no longer renders components synchronously.

import * as React from "react";
import { createRoot } from "react-dom/client";

const toaster = React.createRef<OverlayToaster>();

createRoot(containerElement)
  .render(<OverlayToaster {...props} ref={toaster} usePortal={false} />)
  
// OverlayToaster render function is not yet executed.

// ‼️ This is null ‼️
ref.current

setTimeout(() => {
  // Okay, now the OverlayToaster render() function has ran.
  ref.current // now available
}, 0)

This is different than ReactDOM.render, which does synchronously populate the ref.

import * as React from "react";
import * as ReactDOM from "react-dom";

const toaster = React.createRef<OverlayToaster>();

ReactDOM.render(
  <OverlayToaster {...props} ref={toaster} usePortal={false} />,
  containerElement
);

// OverlayToaster render function has ran by this point.

ref.current // instance of <OverlayToaster />

This seems to be an intentional change in React 18. See the “What about the render callback?” section under Replacing render with createRoot.

Why do I need to pass in a custom domRenderer value for React 18?

The createRoot function is an import on react-dom/client. Blueprint is an NPM library that needs to support multiple bundlers (e.g. Webpack, Vite). Most bundlers will fail if it encounters a non-existent submodule import. Since the current major version of Blueprint needs to support React 16 to 18, imports into 18-specific code paths can't be made directly in Blueprint.

As a workaround, consumers of Blueprint can provide the createRoot function to Blueprint. This is an application of dependency injection.

When Blueprint drops support for React 16 in a future major version, the domRenderer option will change its current default from ReactDOM.render to a function using the new createRoot API. This breaking change will make OverlayToaster.createAsync easier to use in the future.

How do I migrate?

The most one-to-one conversion would be from:

OverlayToaster.create().show({ message: "Hello!" });

to:

OverlayToaster.createAsync().then(toaster => toaster.show({ message: "Hello!" }));

You may notice:

  1. We're swallowing errors that happen when rendering the component. There's no rejection handler on the promise object.
  2. The OverlayToaster component is never unmounted or removed from the DOM after the message is dismissed.

Both of these are true of the original synchronous OverlayToaster.create() API. The new API makes these existing problems more apparent. We recommend mounting the toaster directly to your React application tree if the first problem is a concern, and sharing the Toaster to avoid the second problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

8 participants