How to read RTK Query state without hooks in react-router 6.4
Asked Answered
S

2

15

Recently new react-router 6.4 was released and it has the ability to load data before render of component. (https://reactrouter.com/en/main/start/overview#data-loading)

This seems like a cool feature. And I want to use it with RTK Query, since I already use Redux Toolkit.

So I want to to a basic thing, I have the api to load posts. I want to load them, and if request fails - redirect to other page. In react-router 6.4 it all can be done in router(https://reactrouter.com/en/main/start/overview#redirects).

Router is outside of scope of react, so I can not use hooks which are provided by rtk query, so it means that I have to use rtk query without hooks, which according to documentation is totally possible (https://redux-toolkit.js.org/rtk-query/usage/usage-without-react-hooks)

So my problem is, how do I read status of the request IN the react-router loader. I am able to read status in components, using hooks, but it makes components "dirty" and spreads the logic across the app.

import React from "react";
import ReactDOM from "react-dom/client";

import { createBrowserRouter, RouterProvider } from "react-router-dom";

import { Provider } from "react-redux";
import { store } from "./redux/redux";

import { Comments } from "./Comments";
import { Posts } from "./Posts";
import { Root } from "./Root";

import { postsApi } from "./redux/redux";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
  {
    path: "posts",
    element: <Posts />,
    loader: () => {
      store.dispatch(postsApi.endpoints.getPosts.initiate());

      const request = postsApi.endpoints.getPosts.select()(store);

      console.log(request);

      return request;
    },
  },
  {
    path: "/comments",
    element: <Comments />,
  },
]);

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <Provider store={store}>
    <RouterProvider router={router} />
  </Provider>
);

I am trying to use example from the docs, but I always get status "uninitialized" in the console, like the request had never been made, but it was, I can see the data in redux dev tools. And I also get the error "No data found at state.postsApi. Did you forget to add the reducer to the store?"

Slaby answered 18/10, 2022 at 8:31 Comment(0)
R
9

You are on the right track here, but you need to call unwrap() on the promise returned by dispatch, which will either return the result if successful or throw if not. I've modified your snippet to show what I mean.

    loader: () => {
      const p = store.dispatch(postsApi.endpoints.getPosts.initiate());

      try {
        const response = await p.unwrap()
        return response
      } catch (e) {
        // see https://reactrouter.com/en/main/fetch/redirect
        return redirect("/login")
      } finally {
        p.unsubscribe()
      }
    },
Respecting answered 24/12, 2022 at 7:20 Comment(0)
B
5

You would probably do something along the lines

const promise = dispatch(api.endpoints.myEndpoint.initiate(someArgument))
await promise // wait for data to be there
promise.unsubscribe() // remove the subscription. The data will stay in cache for 60 seconds and the component can subscribe to it in that timeframe.

Note that I do not access the data here and you probably shouldn't. While it will be available after that await promise, I would use the loader only for data to be present - and then use the normal useMyEndpointQuery(someArgument) in the component to access that data.

You need to use the hook so the cache knows that your component is actually using that data - otherwise it would be removed from the cache after 60 seconds (or if you never unsubscribed, it would never be removed).

At that point there is no real benefit of passing that data from the loader function into the component - it will already be able to access it through the hook anyways.

So my suggestion: initiate and await the fetch from the loader, but actually access the data as before from the component.

Bijou answered 18/10, 2022 at 9:57 Comment(3)
Thank you for answering. So there is no way to redirect in loader, right? I just find it very inconvinient, that we are trying to first of all display a component, and only then we fetch data and if data loading fails we redirect to other page. What is the point to display a component if there is no data for it?Slaby
So for anybody who's looking. I feel a bit stupid for spending half of a day on this. I solved my problem by doing this: I save query into variable: const request = store.dispatch(postsApi.endpoints.getPosts.initiate()); Then I await for it. And then I have all needed object keys through dot notation request.isError. So now I make request in route loader, if it fails I make redirect, and if everything is ok, I consume recieved data in component using rtk query hooks. Works like a charm!Slaby
@Slaby I'm trying to achieve something similar in my project, any chance you have a code sandbox / JSFiddle showing how you solved the issue? Thanks!Sleight

© 2022 - 2024 — McMap. All rights reserved.