20

With the new NextJS 13 introducing the app directory, would redux still make sense?

It's already possible to wrap redux providers around a Client Component as per next docs. But how would sharing states with redux impact Next performance and optmization?

Next docs ask to fetch data where the data is needed instead of passing down the component tree. Request will be automatically deduped.

My usage for Redux would be to control certain pieces of the application that I want to be consistent.

E.g: If I change an user name I want that reflected in the whole application where that data is needed.

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
Luna
  • 467
  • 1
  • 3
  • 11
  • I won't pretend to have a full grasp of the new NextJS 13 component model and its component trees, but I'd have to imagine that it boils down to each component's implementation of monitoring for changes, such as your user name example. If there's an active observer for a value in a component, and that value is updated, I'd expect that the component would automatically update the affected trees. There were a few things that seemed closer to magic in the samples than that, though, so I'll just post this as a comment pending someone more in the know coming along. – bsplosion Nov 17 '22 at 19:56

4 Answers4

15

I recently started using NextJS and I had the same problem, after a bit of a research I have managed to get it working without wrapping each client component with a ReduxProvider.

Following the documentation given by the NextJS team it is suggested to a Providers client component which will give context to any other client components.

More details here: NextJS Context documentation

step 1: Create a Provider component in app/providers (make sure this is a client component)

 "use client";

import { useServerInsertedHTML } from "next/navigation";
import { CssBaseline, NextUIProvider } from "@nextui-org/react";
import { PropsWithChildren } from "react";
import ReduxProvider from "./redux-provider";

type P = PropsWithChildren;

export default function Providers({ children }: P) {
  useServerInsertedHTML(() => {
    return <>{CssBaseline.flush()}</>;
  });

  return ( // you can have multiple client side providers wrapped, in this case I am also using NextUIProvider
    <>
      <ReduxProvider>
        <NextUIProvider>{children}</NextUIProvider>
      </ReduxProvider>
    </>
  );
}

create a redux provider component.

"use client";

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

export default function ReduxProvider({ children }: PropsWithChildren) {
  return <Provider store={store}>{children}</Provider>;
}

Use your provider component within your RootLayout component. (app/layout.tsx)

import Header from "./components/organisms/Header/Header";
import { PropsWithChildren } from "react";
import Footer from "./components/molecules/Footer/Footer";
import Providers from "./providers/Providers";
import MyBasketTab from "./components/organisms/MyBasketTab/MyBasketTab";

type Props = PropsWithChildren;

export default function RootLayout({ children }: Props) {
  return (
    <html lang="en">
      <head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </head>
      <body>
        <Providers>
          <Header />
          {children}
          <Footer />
          <MyBasketTab /> // this client component can now fully use the redux hooks and access the store.
        </Providers>
      </body>
    </html>
  );
}

"use client";

import styles from "./MyBasketTab.module.css";
import { useAppSelector } from "../../../redux/hooks/hooks";

export default function MyBasketTab() {
  const isBasketSideMenuOpened = useAppSelector(
    (x) => x.basket.isBasketSideMenuOpened
  );

  return (
    <div
      className={`${styles.container} ${
        !isBasketSideMenuOpened ? styles.opened : styles.closed
      }`}
    >
      <p>My Basket</p>
    </div>
  );
}
Konrad
  • 151
  • 2
  • Seems to be the go to solution for now. – friartuck Nov 28 '22 at 09:21
  • 1
    I'd just wanted to also mention that server components can still access to the store state via `store.getState()`. They just can't use `useSelector` like client components. – Fer Toasted Mar 16 '23 at 22:47
  • 3
    By having a client component in the root layout, do all pages then become Client Components? Seems to defeat the purpose of Server Side Components – Merlin -they-them- Apr 17 '23 at 04:03
  • No, if I understand the docs correctly, Next.js will fill in the client/server components at the appropriate boundaries. See: https://nextjs.org/docs/getting-started/react-essentials – Andrew K Jun 17 '23 at 18:26
3

The main focus of server components is to send minimum javascript code to the browser so the browser will have less code to parse and it will display the content quicker. In the case of redux state management, we were populating the state on the server, sending the store and related code to the browser and the browser was hydrating in other words it was synchronizing the server store with the client code. Hydration is a very expensive operation. we were technically recreating a new store on the client. Every time user visits a page with server-side functions, HYDRATE action was triggered and a new store was created. Imagine you have a very large application and applying hydration would slow down your app drastically.

With the app directory redux store, less code will be sent to the browser and I believe only the store that needed use client components will be hydrated (as of now we cannot set next-redux-wrapper in app directory. I think the default redux package will be redux-toolkit). this will increase the performance. Although marking some components as clients or some as servers does not seem nice, but the overall goal is to increase performance. And app directory is still in beta, as time goes by, they will make it better.

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
1

There is much easier solution than writing tons of lines, just import the store directly from the store.tsx file into the rootlayout:

in my projects I use it like this:

store/Store.tsx:

import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import { useDispatch } from 'react-redux';
import musicSlice from './musicSlice';

const store = configureStore({
  reducer: {
    music: musicSlice,
  },
});

export const wrapper = createWrapper(() => store);
export const store_0001 = store;
export type AppState = ReturnType<typeof store.getState>;
export const useAppDispatch = () => useDispatch();

App/layout.tsx:

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

export default function RootLayout({children, ...rest}: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head />
      <body>
        
          <Provider store={store_0001}>
            <Header/>
            <NavMenu/>
            <div>
              {children}
            </div>
            <Footer/>
          </Provider>
       
      </body>
    </html>
  )
}

Good luck!

  • 2
    How are you making that work? I have the same thing and get `react.createContext is not a function` – Merlin -they-them- Apr 17 '23 at 04:01
  • @Merlin-they-them- Me too. Tried replicating the setup as listed above and getting `TypeError: (0 , _react.createContext) is not a function` – Jens Væver Hartfelt Jun 04 '23 at 13:47
  • I found a solution, but i think it is a terrible one. I am completely new to next.js so feel free to tell me why this solution is rubbish.. I put `"use client"` in the root layout and moved the metadata export down as hardcoded html-elements in the `` section instead. I think this kinda defeats the purpose of server components, but at least its working ‍♂️ – Jens Væver Hartfelt Jun 04 '23 at 13:52
-1

Adding to Konrad's answer, if you would want to share state between server components - which is the new default w/o the "use client" directive - you would need to implement a native solution, like a singleton.

Say you want to share database connection across multiple component - access to the database is shared by importing the respective database module.

Set up the database module first:

utils/database.js

export const db = new DatabaseConnection(...);

Import into depending modules

app/users/layout.js
import { db } from "@utils/database";

export async function UsersLayout() {
  let users = await db.query(...);
  // ...
}

app/users/[id]/page.js
import { db } from "@utils/database";

export async function DashboardPage() {
  let user = await db.query(...);
  // ...
}

See here for reference.

friartuck
  • 468
  • 5
  • 6