React Router Dom - v6 - useBlocker
Asked Answered
M

3

14

As https://github.com/remix-run/react-router/issues/8139 is finished and we got useBlocker in v6, did anyone got it to work?

This is what I got so far and pretty much I'm stuck with error I quite don't understand

in App.js I have my BrowserRouter and everything is wrapped inside

Also I used example from implementer's gists (I copy pasted)

import * as React from "react";
import { useBeforeUnload, unstable_useBlocker as useBlocker } from "react-router-dom";

function usePrompt(message, { beforeUnload } = {}) {

let blocker = useBlocker(
  React.useCallback(
     () => (typeof message === "string" ? !window.confirm(message) : false),
  [message]
  )
);
let prevState = React.useRef(blocker.state);

React.useEffect(() => {
    if (blocker.state === "blocked") {
    blocker.reset();
  }
  prevState.current = blocker.state;
}, [blocker]);

useBeforeUnload(
     React.useCallback(
       (event) => {
         if (beforeUnload && typeof message === "string") {
          event.preventDefault();
          event.returnValue = message;
         }
      },
      [message, beforeUnload]
    ),
  { capture: true }
 );
}

function Prompt({ when, message, ...props }) {
    usePrompt(when ? message : false, props);
    return null;
}

And then within my component I called Prompt like this

const MyComponent = (props) => {
    const [showPrompt, setShowPrompt] = useState(false)

    ...

    return (
        ...
        <Prompt when={showPrompt} 
                message="Unsaved changes detected, continue?" 
                beforeUnload={true} 
        />
    )
}

And on page load of MyComponent I keep getting error

Error: useBlocker must be used within a data router.  See 
    https://reactrouter.com/routers/picking-a-router.
     at invariant (history.ts:308:1)
     at useDataRouterContext (hooks.tsx:523:1)
     at useBlocker (hooks.tsx:723:1)
     at usePrompt (routerCustomPrompt.js:8:1)
     at Prompt (routerCustomPrompt.js:37:1)

Did anyone got useBlocker in new version to work?

Misjudge answered 16/1, 2023 at 13:50 Comment(1)
Please revise your post title to ask a clear, specific question. Don't add tags. See How to Ask.Absorbent
T
4

The error message is rather clear. In order to use the useBlocker hook it must be used within a component rendered by a Data router. See Picking a Router.

Example:

const MyComponent = (props) => {
  const [showPrompt, setShowPrompt] = useState(false);

  ...

  return (
    ...
    <Prompt
      when={showPrompt} 
      message="Unsaved changes detected, continue?" 
      beforeUnload={true} 
    />
  );
}
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      {/* ... etc. */}
      <Route path="myComponent" element={<MyComponent />} />
      {/* ... etc. */}
    </Route>
  )
);

const App = () => <RouterProvider router={router} />;
Twiddle answered 16/1, 2023 at 20:28 Comment(0)
F
8

As I have a bit of a struggle with understanding how to get this useBlocker hook and Prompt component working, I will elaborate a bit more on code changes I had to do to get this working with react-router-dom version 6.10.0.

Firstly, refer to the documentation of Using v6.4 Data APIs In v6.4, which suggests using the new data APIs, specifically createBrowserRouter instead of <BrowserRouter>.

As per documentation, the <BrowserRouter> based declaration does not "support the data APIs" which in turn results in Error: useBlocker must be used within a data router.

So in my app, I ended up replacing route declaration:

export default function App() {
  return (
    <YourProviders>
      <BrowserRouter>
        <Routes>
          // your routes
        </Routes>
      </BrowserRouter>
    </YourProviders>
  );
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);

with:

const router = createBrowserRouter(
  createRoutesFromElements(
    <Routes>
      // your routes
    </Routes>
  )
);

export default function App() {
  return (
    <YourProviders>
      <RouterProvider router={router} />
    </YourProviders>
  );
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);

Which in turn finally got the Prompt working as expected.

Formate answered 26/4, 2023 at 9:57 Comment(0)
T
4

The error message is rather clear. In order to use the useBlocker hook it must be used within a component rendered by a Data router. See Picking a Router.

Example:

const MyComponent = (props) => {
  const [showPrompt, setShowPrompt] = useState(false);

  ...

  return (
    ...
    <Prompt
      when={showPrompt} 
      message="Unsaved changes detected, continue?" 
      beforeUnload={true} 
    />
  );
}
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      {/* ... etc. */}
      <Route path="myComponent" element={<MyComponent />} />
      {/* ... etc. */}
    </Route>
  )
);

const App = () => <RouterProvider router={router} />;
Twiddle answered 16/1, 2023 at 20:28 Comment(0)
G
2

Drew Reese's answer https://mcmap.net/q/818273/-react-router-dom-v6-useblocker is accurate but it implies you need to restructure where the routes live in your app. The docs don't make it entirely clear that you don't actually have to change anything about your app and how its organized to switch from v6's <BrowserRouter/> to createBrowserRouter.

nested <Routes> are still fully supported without being declared in the createBrowserRouter function. so there's a couple ways of setting it up but the easiest (imooc) is:

previous main/index:

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

new:

const router = createBrowserRouter(createRoutesFromElements(<Route path='*' element={<App />} />));

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

then in App, remove your <BrowserRouter> tags.

you'll get access to all the new data apis, but you don't have to change anything else.

Gluteal answered 11/6 at 15:31 Comment(2)
This workaround does indeed work - but I'm wondering if there are drawbacks in doing so. Aren't we missing out on some of the benefits of the new createBrowserRouter functionality? Is routing performance not impacted?Bowery
routing performance is not impacted, and you can incrementally adopt whichever features you desire. you can add more into the createBrowserRouter function. I'm not suggesting this is how you should use this library, I'm just answering the question of how you can not restructure your app and still use useBlocker. That being said I don't like the actions api, idk who's webapp is a collection of pages with a single form on each one but mine aren't and if they were I wouldn't need a router.Gluteal

© 2022 - 2024 — McMap. All rights reserved.