如何将 React Query v4 SSR Hydration 方法与 Next JS 13 结合使用

Idr*_*ris 5 javascript reactjs server-side-rendering next.js react-query

Next JS 13 于上个月刚刚发布,完全改变了数据的获取方式,同时还通过使用根布局.js 提供了 _app.js 和 _document.js 的替代方案。以前在 Next JS 12 及更低版本中,要使用 Hydration 方法使用 React Query SSR 功能,您需要像这样设置 _app.js 文件:

import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import queryClientConfig from '../queryClientConfig';

export default function MyApp({ Component, pageProps }) {

  const queryClient = useRef(new QueryClient(queryClientConfig));
  const [mounted, setMounted] = useState(false);

  const getLayout = Component.getLayout || ((page) => page);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return (
    <ErrorBoundary FallbackComponent={ErrorFallbackComponent}>
      <QueryClientProvider client={queryClient.current}>
        <Hydrate state={pageProps.dehydratedState}>
          <AppProvider>
            {getLayout(<Component {...pageProps} />)}
          </AppProvider>
        </Hydrate>
      </QueryClientProvider>
    </ErrorBoundary>
  );
}
Run Code Online (Sandbox Code Playgroud)

要在 Next JS 的页面中使用 React Query SSR getServerSideProps,如下所示:

// Packages
import Head from 'next/head';
import { dehydrate, QueryClient } from '@tanstack/react-query';

// Layout
import getDashboardLayout from '../../layouts/dashboard';


// Parse Cookies
import parseCookies from '../../libs/parseCookies';

// Hooks
import { useFetchUserProfile } from '../../hooks/user';
import { fetchUserProfile } from '../../hooks/user/api';
import { getGoogleAuthUrlForNewAccount } from '../../hooks/auth/api';
import { fetchCalendarsOnServer } from '../../hooks/event/api';
import { useFetchCalendars } from '../../hooks/event';


// Store
import useStaticStore from '../../store/staticStore';


// `getServerSideProps function`
export async function getServerSideProps({ req, res }) {
  const cookies = parseCookies(req);
  const queryClient = new QueryClient();

  try {
    await queryClient.prefetchQuery(['fetchUserProfile'], () =>
      fetchUserProfile(cookies.userAccessToken)
    );
    await queryClient.prefetchQuery(['fetchCalendars'], () => fetchCalendarsOnServer(cookies.userAccessToken));
    await queryClient.prefetchQuery(['getGoogleAuthUrlForNewAccount'], () =>
      getGoogleAuthUrlForNewAccount(cookies.userAccessToken)
    );
  } catch (error) {

  }

  return {
      props: {
        dehydratedState: dehydrate(queryClient),
      },
    };
}


function Home() {

  const {
    data: userProfileData, // This data is immediately made available without any loading as a result of the hydration and fetching that has occurred in `getServerSideProps`
    isLoading: isUserProfileDataLoading,
    error: userProfileDataError,
  } = useFetchUserProfile();

  const { data: savedCalendarsData } = useFetchCalendars(); // This data is immediately made available without any loading as a result of the hydration and fetching that has occurred in `getServerSideProps`

  return (
    <>
      <Head>
        <title>
          {userProfileData.data.firstName} {userProfileData.data.lastName} Dashboard
        </title>
        <meta
          name="description"
          content={`${userProfileData.data.firstName} ${userProfileData.data.lastName} Dashboard`}
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <PageContentWrapper
      >
        Page Content
      </PageContentWrapper>
    </>
  );
}

Home.getLayout = getDashboardLayout; // This layout also needs data from userProfileData to be available. There is no problem and it never loads because the data is immediately available on mount.

export default Home;
Run Code Online (Sandbox Code Playgroud)

这是旧的 DashboardLayout 组件:

// Packages
import PropTypes from 'prop-types';

// Hooks
import { useFetchUserProfile } from '../../hooks/user';

DashboardLayout.propTypes = {
  children: PropTypes.node.isRequired,
};

function DashboardLayout({ children }) {

  const { isLoading, error, data: userProfileData } = useFetchUserProfile(); // Data is immediately available and never loads because it has been fetched using SSR in getServerSideProps

 
  if (isLoading)
    return (
      <div className="w-screen h-screen flex items-center justify-center text-white text-3xl font-medium">
        Loading...
      </div>
    );

  if (error) {
    return (
      <div className="w-screen h-screen flex items-center justify-center text-brand-red-300 text-3xl font-medium">
        {error.message}
      </div>
    );
  }

  return (
    <>
      <div className="the-dashboard-layout">
        {/* Start of Main Page */}

        <p className="mb-2 text-brand-gray-300 text-sm leading-5 font-normal">
          <span className="capitalize">{`${userProfileData.data.firstName}'s`}</span> Layout
        </p>
      </div>
    </>
  );
}

export default function getDashboardLayout(page) {
  return <DashboardLayout>{page}</DashboardLayout>;
}
Run Code Online (Sandbox Code Playgroud)

使用新的 Next JS 13,无法使用 React Query Hydration 方法,即使我能够使用新方法获取数据,在组件安装时仍然会重新获取数据,这会导致布局处于加载状态,因为数据不能立即可用。

在Next 13中,您只需要调用数据获取方法并将其直接传递给客户端组件,因为app目录现在直接支持服务器组件。

首先,根布局文件替换了 Next 13 中旧的 _app.js 和 _document.js 文件:值得注意的是pagePropsdehydratedState.

这是RootLayout服务器组件:

// Packages
import PropTypes from 'prop-types';

// Components
import RootLayoutClient from './root-layout-client';

RootLayout.propTypes = {
  children: PropTypes.node.isRequired,
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <RootLayoutClient>{children}</RootLayoutClient>
      </body>
    </html>
  );
}
Run Code Online (Sandbox Code Playgroud)

RootLayoutClient由于使用了客户端操作 Context 和 State,因此需要布局的客户端组件如下:

'use client';

// Packages
import React, { useRef, useEffect } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import PropTypes from 'prop-types';

// Context
import { AppProvider } from '../contexts/AppContext';

// Config
import queryClientConfig from '../queryClientConfig';

RootLayoutClient.propTypes = {
  children: PropTypes.node.isRequired,
};

export default function RootLayoutClient({ children }) {

  const queryClient = useRef(new QueryClient(queryClientConfig));
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return (
    <QueryClientProvider client={queryClient.current}>
      <AppProvider>
         {children}
      </AppProvider>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
Run Code Online (Sandbox Code Playgroud)

getServerSideProps方法现已替换为使用 FETCH API 的基于 Promise 的普通数据获取方法。返回的获取数据现在可以传递到需要它的页面/组件中。

这是我的数据获取函数:

import { getCookie } from 'cookies-next';

export const fetchUserProfile = async (token) => {
  if (token) {
    try {
      const response = await fetch(process.env.NEXT_PUBLIC_EXTERNAL_API_URL + FETCH_USER_PROFILE_URL, {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      if (response.ok) {
        const data = await response.json();
        return data;
      } else {
        Promise.reject(response);
      }
    } catch (error) {
      Promise.reject(error);
    }
  } else {
    try {
      const response = await fetch(process.env.NEXT_PUBLIC_EXTERNAL_API_URL + FETCH_USER_PROFILE_URL, {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${getCookie('userAccessToken')}`,
        },
      });

      if (response.ok) {
        const data = await response.json();
        return data;
      } else {
        Promise.reject(response);
      }
    } catch (error) {
      Promise.reject(error);
    }
  }
};
Run Code Online (Sandbox Code Playgroud)

以下是在主页中获取和使用数据的方式。请注意,主页位于应用程序目录中home/page.js::

import { cookies } from 'next/headers';

// Hooks
import { fetchUserProfile } from '../../../hooks/user/api';

// Components
import HomeClient from './home-client';

export default async function Page() {
  const nextCookies = cookies();
  const userAccessToken = nextCookies.get('accessToken');

  const userProfileData = await fetchUserProfile(userAccessToken.value);


// This is essentially prop passing which was not needed using the previous hydration and getServerSideProps methods.
// Now, I have to pass this data down to a client component called `HomeClient` that needs the data. This is done because I may need to  perform some client-side operations on the component.

  return <HomeClient userData={userProfileData} />;
}

Run Code Online (Sandbox Code Playgroud)

这是HomeClient客户端组件:

'use client';

import { useEffect } from 'react';
import PropTypes from 'prop-types';

// Hooks
import { useFetchUserProfile } from '../../hooks/user';


HomeClient.propTypes = {
  userData: PropTypes.any.isRequired,
  calendarData: PropTypes.any.isRequired,
};

export default function HomeClient({ userData }) {

const { isLoading, error, data: userProfileData } = useFetchUserProfile();
  useEffect(() => {
    console.log(JSON.stringify(userData));
  }, [userData]);
  
  // This now loads instead of being immediately available. This can be mitigated by directly using the userData passed
  // through props but I don't want to engage in prop drilling in case I need it to be passed into deeper nested child components
  
  if (isLoading) {
    return (
        <div>Loading...</div>
    )
  }

  return (
    <>
      <AnotherChildComponent profileData={userProfileData.data.profile}/>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

这是上面客户端组件中使用的 useFetchUserProfile 钩子函数HomeClient

export const useFetchUserProfile = (conditional = true) => {
// Used to be immediately available as a result of the key 'fetchUserProfile' being used to fetch data on getServerSideProps but that's not available in the app directory

  return useQuery(['fetchUserProfile'], () => fetchUserProfile(), {
    enabled: conditional,
    cacheTime: 1000 * 60 * 5,
  });
};
Run Code Online (Sandbox Code Playgroud)

layout.js这是NextJS 13 共享通用布局所需的父文件。这layout.js也需要获取数据,但即使通过 也无法将数据传递给此props。过去,由于在中react-query进行水合,因此可以立即获得数据getServerSideProps

// Packages
import PropTypes from 'prop-types';

// Hooks
import { useFetchUserProfile } from '../../hooks/user';

DashboardLayout.propTypes = {
  children: PropTypes.node.isRequired,
};

function DashboardLayout({ children }) {

  const { isLoading, error, data: userProfileData } = useFetchUserProfile(); 
  // Used to be that data was immediately available and never loaded because it has been fetched using SSR in getServerSideProps
  // Now, it has to load the same data. This is even more complex because props can't be passed as there is no way or any abstraction method 
  // to share data between the layout and child components
  


  if (isLoading)
    return (
      <div className="w-screen h-screen flex items-center justify-center text-white text-3xl font-medium">
        Loading...
      </div>
    );

  if (error) {
    return (
      <div className="w-screen h-screen flex items-center justify-center text-brand-red-300 text-3xl font-medium">
        {error.message}
      </div>
    );
  }

  return (
    <>
      <div className="the-dashboard-layout">
        {/* Start of Main Page */}

        <p className="mb-2 text-brand-gray-300 text-sm leading-5 font-normal">
          <span className="capitalize">{`${userProfileData.data.firstName}'s`}</span> Layout
        </p>
      </div>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

如何消除重复的多个请求,并使数据在无需进行 prop-drill 的情况下获取相同数据的所有组件中可用?即使我想使用 props,我该如何解决无法将数据传递到父布局组件的限制。

先感谢您。