reactjs - Hur man initialiserar en redux-store från en serverkomponent med next.js, app-router


Jag arbetar med Next.js 13, React och Redux för att hantera tillstånd.

Mitt mål är att ladda initial data på serversidan och visa den på klientsidan. Jag trodde att detta skulle vara ett standardförfarande med Redux och Next.js, men jag har inte hittat något sätt att göra det.

Här är vad jag förstår:

Redux-butiken är konfigurerad två gånger: en gång på serversidan och en gång på klientsidan. Jag trodde att jag helt enkelt kunde hämta data på serversidan, använda en dispatch-funktion för att initiera, och sedan låta klientsidan ladda datan. Men problemet är att komponenter på serversidan inte kan använda dispatch eller manipulera butikens data.

Redux-dokumentationen säger att React Server Components (RSCs) inte bör läsa eller skriva till Redux-butiken eftersom de inte kan använda hooks eller context och inte är avsedda att ha tillstånd. Så, hur laddar vi då initial data? Är initial data undantag? Och om så är fallet, hur uppdaterar vi denna initial data?

Här är min kod, detta är RSC

KontaktListaServerKomponent.tsx

import ContactList from "@/component/Contacts/page"
import {
  getInitialContactsData,
  initializeContact,
} from "@/lib/features/contacts/contactSlice"
import { makeStore } from "@/lib/store"
import styles from "./contacts.module.scss"

const ContactListPage = async () => {
  const initialData = await getInitialContactsData()
  console.log("server component")
  const store = makeStore()
  store.dispatch(initializeContact(initialData))
  return (
    <div className={styles.container}>
      <h1>Contacts</h1>
      <ContactList />
    </div>
  )
}

export default ContactListPage

Detta är klientens sida (Kontaktlista)

const ContactList = () => {
  const dispatch = useAppDispatch()
  const contacts = useSelector((state: RootState) => state.contacts)
  const [filter, setFilter] = useState<string | null>(null)
  useEffect(() => {
    dispatch(getContacts())
  }, [dispatch] 

  .... etc

Jag har en serverkomponent (ContactListServerComponent.tsx) där jag försöker hämta och distribuera den initiala datan. Sedan har jag en klientkomponent (ContactList) där jag för närvarande använder useEffect för att hämta data, vilket inte fullt ut utnyttjar fördelarna med SSR. Jag är inte säker på hur jag ska implementera min idé om att hämta initial data i serverkomponenten istället för i klientkomponenten.

Jag skulle vilja veta hur jag kan ladda data från API på serverkomponenten och skicka dem till klientkomponenten.

Svar #1

Lite sent, men om du vill undvika prop-drilling kan du helt enkelt först ladda den initiala datan i en Sida eller en Layout och skicka den till en Klientkomponent som kommer att ta den initiala datan och distribuera den i din klient-redux-butik :

'use client';

const InitialDataLoader = ({initialData}: any) => {
    useEffect(() => {
        store.dispatch(
            actions.setState({key: 'initialData', value: initialData}),
        );
    }, []);
    return null;
}

Ett annat alternativ skulle vara att använda en React Context som:

interface ContextProps {initialData: any}
const InitialDataContext = createContext<ContextProps | undefined>(undefined);
export const InitialDataProvider = ({
    children,
    initialData,
}: {
    children: ReactNode,
} & ContextProps) => {
    return (
        <InitialDataContext.Provider value={{initialData}}>
            {children}
        </InitialDataContext.Provider>
    );
};

export const useInitialData = () => {
    const context = useContext(InitialDataContext);
    if (!context) {
        throw new Error('useInitialData must be used within an InitialDataProvider');
    }
    return context.initialData;
};

Och använd det sedan i serverkomponenten där du hämtar de initiala uppgifterna:

<InitialDataProvider initialData={initialData}>
  {children}
</InitialDataProvider>

All server-side omvalidering skulle också ändra de data som levereras i sammanhanget.

Svar #2

Jag höll på att bli galen tills jag bestämde mig för att sluta tänka på att använda SSR och Redux samtidigt. Som andra föreslog är det bättre att hämta datan i page.tsx-filen och skicka den som props inom komponenterna, och sedan göra dispatch på klientsidan. Nedan är mitt exempel på tillvägagångssätt.

//page.tsx

import { fetchProducts } from "@/api/api";
import { Hero } from "@/components/Hero/Hero";
import { BrandBanner } from "@/components/BrandBanner/BrandBanner";
import { TopSelling } from "@/components/TopSelling/TopSelling";
import { NewArrivals } from "@/components/NewArrivals/NewArrivals";
import { BrowseByDressStyle } from "@/components/BrowseByDressStyle/BrowseByDressStyle";
import { OurHappyCustomers } from "@/components/OurHappyCustomers/OurHappyCustomers";
import { ProductsProps } from "@/types/ProductsProps";

export default async function Home() {
    const products: ProductsProps = await fetchProducts();
    
    return (
        <>
            <Hero />
            <BrandBanner />
            <TopSelling allProducts={products} />
            <NewArrivals allProducts={products}/>
            <BrowseByDressStyle />
            <OurHappyCustomers allProducts={products} />
        </>
    );
}

//TopSelling.tsx

"use client";
import { useSelector } from "react-redux";
import { RootState } from "@/store";
import { ProductCard } from "../ProductCard/ProductCard";
import { ProductsProps } from "@/types/ProductsProps";
import styles from "./TopSelling.module.scss";

interface TopSellingProps {
    allProducts: ProductsProps;
}

export const TopSelling: React.FC<TopSellingProps> = ({ allProducts }) => {
    const { items } = useSelector((state: RootState) => state.products);
    const productsToRender = items.length > 0 ? items : allProducts.products;
    return (
        <section className={styles.topSelling}>
            <h3 className={styles.sectionTitle}>TOP SELLING</h3>
            <div className={styles.productList}>
                {productsToRender.slice(10, 14).map((product) => (
                    <ProductCard
                        key={product.id}
                        id={product.id}
                        thumbnail={product.thumbnail}
                        title={product.title}
                        rating={product.rating}
                        price={product.price}
                        discountPercentage={product.discountPercentage}
                    />
                ))}
            </div>
            <button className={styles.viewAllBtn}>View All</button>
        </section>
    );
};