Rerendering of one sibling component due to data update in other sibling component in Next JS

431 views Asked by At

I have a Next JS app where I am using Layout feature in the _app.tsx component. I am showing a sidebar in the layout which is getting populated from an api call(get request).

Now I have another api call (update request) on button click in Index.tsx file. This update request changes data in the layout's sidebar component.

Here are my code snippets

_app.tsx

import { type AppType } from "next/app";
import { type Session } from "next-auth";
import { SessionProvider } from "next-auth/react";

import { trpc } from "../utils/trpc";

import "../styles/globals.css";
import MainLayout from "../components/layouts/MainLayout";

const MyApp: AppType<{ session: Session | null }> = ({
  Component,
  pageProps: { session, ...pageProps },
}) => {
  return (
    <SessionProvider session={session}>
      <MainLayout>
        <Component {...pageProps} />
      </MainLayout>
    </SessionProvider>
  );
};

export default trpc.withTRPC(MyApp);

MainLayout.tsx

import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";

import { trpc } from "../../utils/trpc";

import { AiOutlineOrderedList } from "react-icons/ai";
import { BsCartCheckFill, BsGraphUp, BsSuitHeartFill } from "react-icons/bs";
import { IoRefresh } from "react-icons/io5";
import { IoMdArrowBack } from "react-icons/io";
import { MdModeEditOutline } from "react-icons/md";

import { Space_Grotesk } from "@next/font/google";

import { SelectedData } from "@prisma/client";

const spaceGrotesk = Space_Grotesk({
  weight: "500",
  subsets: ["latin"],
});

type MainLayoutProps = {
  children: React.ReactNode;
};

type DefaultItemProps = {
  setItemMode: React.Dispatch<React.SetStateAction<"default" | "add" | "show">>;
  data: SelectedData[]
};

type ItemProps = {
  setItemMode: React.Dispatch<React.SetStateAction<"default" | "add" | "show">>;
};

const MainLayout = ({ children }: MainLayoutProps) => {
  const [itemMode, setItemMode] = useState<"default" | "add" | "show">(
    "default"
  );
  const pathname = useRouter().pathname.split("/")[1];

  const { data: selectedData } = trpc?.selectedData?.getAllData?.useQuery();

  return (
    <div className={`flex h-screen flex-row gap-0 ${spaceGrotesk.className}`}>
      <div className="flex min-h-full w-20 flex-col items-center justify-between bg-white py-6">
        <div className="rounded-full bg-[#3f3d56] p-3">
          <BsSuitHeartFill size="1.3rem" className="text-[#f9a109]" />
        </div>

        <div className="flex flex-col gap-12">
          <Link href="/">
            <span
              className={`tooltip rounded-full p-3 ${
                pathname === "" && "text-[#f9a109]"
              }`}
              id="Items"
            >
              <span className="hidden">Items</span>
              <AiOutlineOrderedList size="1.3rem" />
            </span>
          </Link>
          <Link href="/history">
            <span
              className={`tooltip rounded-full p-3 ${
                pathname === "history" && "text-[#f9a109]"
              }`}
              id="History"
            >
              <span className="hidden">History</span>
              <IoRefresh size="1.3rem" />
            </span>
          </Link>
          <Link href="/statistics">
            <span
              className={`tooltip rounded-full p-3 ${
                pathname === "statistics" && "text-[#f9a109]"
              }`}
              id="Statistics"
            >
              <span className="hidden">Statistics</span>
              <BsGraphUp size="1.3rem" />
            </span>
          </Link>
        </div>

        <div className="relative rounded-full bg-[#f9a109] p-3">
          <span className="absolute top-[-6px] right-[-6px] flex h-5 w-5 items-center justify-center rounded-lg bg-[#eb5757] p-2 text-white">
            3
          </span>
          <BsCartCheckFill size="1.3rem" className="text-white" />
        </div>
      </div>

      {children}

      {itemMode === "default" && <DefaultItem setItemMode={setItemMode} data={selectedData!} />}
      {itemMode === "add" && <AddItem setItemMode={setItemMode} />}
      {itemMode === "show" && <ShowItem setItemMode={setItemMode} />}
    </div>
  );
};

export default MainLayout;

const DefaultItem = ({ setItemMode, data }: DefaultItemProps) => {
  return (
    <div className="flex w-[20%] flex-col justify-between bg-white">
      <div className="flex h-full flex-col items-center gap-5 bg-[#fff0de] py-6 px-5">
        <div className="flex items-center justify-center gap-7 rounded-lg bg-[#80485b] px-6">
          <img
            className="relative right-1 h-32 w-12 -translate-y-4 rotate-[-20deg]"
            src="/kissclipart-cola-bottle-png-clipart-fizzy-drinks-beer-bottle-853f3ea787dad24a-removebg-preview.png"
            alt="bear-bottle"
          />

          <div className="flex flex-col items-center justify-center gap-3">
            <span className="text-white">Didn't find what you need?</span>
            <button
              onClick={() => setItemMode("add")}
              className="w-fit rounded-xl bg-white px-4 py-2 text-[#80485b]"
            >
              Add Item
            </button>
          </div>
        </div>

        <div className="flex w-full items-center justify-between">
          <span className="text-xl">Shopping List</span>
          <MdModeEditOutline size="1.3rem" />
        </div>

        <div className="flex flex-col gap-5">
          <div className="flex max-h-[440px] flex-col gap-4 overflow-y-auto px-1">
            {data?.map((item) => (
              <div className="flex flex-col gap-2" key={item.id}>
                <span className="text-[14px] text-slate-500">
                  {item.name}
                </span>
                <div className="flex flex-col gap-1">
                  {item?.items.map((item: string, i: number) => (
                    <div key={i} className="flex items-center justify-between">
                      <span className="min-w-[179px] max-w-[180px] truncate">
                        {item}
                      </span>
                      <button className="rounded-lg border-2 border-[#f9a109] px-2 py-1 text-[#f9a109]">
                        3 pcs
                      </button>
                    </div>
                  ))}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      <div className="my-5 flex items-center justify-center rounded-xl  border-2 border-[#f9a109]">
        <input
          className="w-full bg-transparent px-4 outline-none"
          placeholder="Enter a name"
          type="text"
          title="name"
        />
        <button className="rounded-lg bg-[#f9a109] px-5 py-3 text-white">
          Save
        </button>
      </div>
    </div>
  );
};

const AddItem = ({ setItemMode }: ItemProps) => {
  return (
    <div className="flex min-h-full w-[20%] flex-col justify-between gap-7 bg-white py-5 px-4">
      <div className="flex flex-col gap-5">
        <h3 className="text-2xl">Add a new item</h3>

        <div className="flex flex-col gap-5">
          <div className="flex flex-col gap-1">
            <label htmlFor="name">Name</label>
            <input
              className="rounded-lg border-2 border-stone-500 px-4 py-2 outline-none hover:border-[#f9a109] focus:border-[#f9a109]"
              placeholder="Enter a name"
              type="text"
              id="name"
            />
          </div>

          <div className="flex flex-col gap-1">
            <label htmlFor="note">Note (optional)</label>
            <textarea
              className="rounded-lg border-2 border-stone-500 px-4 py-2 outline-none hover:border-[#f9a109] focus:border-[#f9a109]"
              placeholder="Enter a note"
              rows={4}
              id="note"
            />
          </div>

          <div className="">
            <label htmlFor="images">Images</label>
            <input
              className="w-full rounded-lg border-2 border-stone-500 px-4 py-2 outline-none hover:border-[#f9a109] focus:border-[#f9a109]"
              placeholder="Enter an url"
              type="text"
              id="images"
            />
          </div>

          <div className="flex flex-col gap-1">
            <label htmlFor="category">Category</label>
            <input
              className="rounded-lg border-2 border-stone-500 px-4 py-2 outline-none hover:border-[#f9a109] focus:border-[#f9a109]"
              placeholder="Enter a category"
              type="text"
              id="category"
            />
          </div>
        </div>
      </div>

      <div className="flex items-center justify-between">
        <button
          onClick={() => setItemMode("default")}
          className="rounded-lg px-5 py-3"
        >
          Cancel
        </button>

        <button
          onClick={() => setItemMode("show")}
          className="rounded-lg bg-[#f9a109] px-5 py-3 text-white"
        >
          Save
        </button>
      </div>
    </div>
  );
};

const ShowItem = ({ setItemMode }: ItemProps) => {
  return (
    <div className="flex min-h-full w-[20%] flex-col justify-between gap-7 bg-white py-5 px-4">
      <div className="flex flex-col gap-5">
        <button className="flex items-center gap-2 px-3 text-[#f9a109]">
          <IoMdArrowBack size="1.3rem" /> back
        </button>

        <div className="flex flex-col gap-5">
          <div className="flex items-center justify-center">
            <img
              className="h-44 rounded-lg"
              src="/avocado-slices-500x500.jpg"
              alt="item-image"
            />
          </div>

          <div className="flex flex-col gap-1">
            <h6 className="text-gray-500">Name</h6>
            <span className="text-lg">Avocado</span>
          </div>

          <div className="flex flex-col gap-1">
            <h6 className="text-gray-500">Category</h6>
            <span className="text-lg">Fruits and Vegetable</span>
          </div>

          <div className="flex flex-col gap-1">
            <h6 className="text-gray-500">Note</h6>
            <span className="text-lg">
              Lorem ipsum dolor sit amet consectetur, adipisicing elit. Nihil
              soluta culpa, fugit aut, ea corrupti rem molestias dolor dolorem
              odit voluptate, facilis fuga reprehenderit commodi perspiciatis
              optio aperiam recusandae aliquam.
            </span>
          </div>
        </div>
      </div>

      <div className="flex items-center justify-between">
        <button
          onClick={() => setItemMode("add")}
          className="rounded-lg px-5 py-3"
        >
          Back
        </button>

        <button
          onClick={() => setItemMode("default")}
          className="rounded-lg bg-[#f9a109] px-5 py-3 text-white"
        >
          Save
        </button>
      </div>
    </div>
  );
};

Index.tsx

import { useState } from "react";
import { GetServerSideProps, type NextPage } from "next";
import Head from "next/head";
// import Link from "next/link";
// import { signIn, signOut, useSession } from "next-auth/react";

import { type Data, PrismaClient } from "@prisma/client";

import { Dancing_Script } from "@next/font/google";

import { IoAddOutline } from "react-icons/io5";
import { AiOutlineSearch } from "react-icons/ai";
import Overlay from "../components/common/Feedback/Overlay";

import { trpc } from "../utils/trpc";

type HomePageProps = {
  datas: Data[];
};

const dancingScript = Dancing_Script({
  weight: "700",
  subsets: ["latin"],
});

const Home: NextPage<HomePageProps> = ({ datas }) => {
  const [filteringList, setFilteringList] = useState(datas);
  const [search, setSearch] = useState("");
  const mutation = trpc.selectedData.addItemToShopingList.useMutation();

  const searchHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    const filteredList = datas.map((list) => {
      return {
        ...list,
        items: list.items.filter((item) =>
          item.toLowerCase().includes(e.target.value.toLowerCase())
        ),
      };
    });
    setFilteringList(filteredList);
  };

  const addItemToShopingList = async (
    item: string,
    name: string,
    id: string
  ) => {
    if (mutation.error) {
      return alert("Something went wrong");
    }
    mutation.mutate({
      id,
      name,
      item,
    });
  };

  return (
    <>
      <Head>
        <title>Shoppingify | Items</title>
        <meta name="description" content="Generated by create-t3-app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Overlay isShowing={false} />

      <div className="flex min-h-full flex-1 flex-col gap-12 bg-[#fafafe] px-16 py-8">
        <div className="flex w-full items-center gap-40">
          <h1 className="text-4xl">
            <span className={`${dancingScript.className} text-[#f9a109]`}>
              Shoppingify
            </span>{" "}
            allows you take your shopping list wherever you go
          </h1>

          <div className="relative flex items-start rounded-lg shadow-lg">
            <AiOutlineSearch className="absolute top-4 left-3" size="1.5rem" />
            <input
              className="rounded-lg px-12 py-4 outline-none"
              onChange={searchHandler}
              value={search}
              type="search"
              name="search"
              placeholder="Search Items"
              id="search"
            />
          </div>
        </div>

        <div className="flex max-h-[600px] flex-col gap-10 overflow-y-auto py-2">
          {filteringList?.map((list) => (
            <div key={list?.id} className="flex flex-col gap-7">
              <h3 className="text-2xl">{list?.name}</h3>
              <div className="flex flex-wrap items-center justify-center gap-5 space-x-3">
                {list?.items.map((item: string, idx: number) => (
                  <div
                    className="flex items-center justify-center gap-3 rounded-md px-4 py-2 shadow-md"
                    key={idx}
                  >
                    {item}{" "}
                    <button
                      onClick={() =>
                        addItemToShopingList(item, list?.name, list?.id)
                      }
                      title="add-item"
                    >
                      <IoAddOutline size="1.3rem" />
                    </button>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
      </div>
    </>
  );
};

export default Home;

export const getServerSideProps = async () => {
  const prisma = new PrismaClient();
  const datas = await prisma.data.findMany();

  return { props: { datas } };
};

So my question is how can I update the layout's sidebar data after data is updated from Index.tsx file.

1

There are 1 answers

0
lua_python_java On

A solution is to update the Home component to contain a state representing the datas prop and any added data the user adds when using the shopping list. The filteringList state can use this "fluctuating" state instead of using the prop passed in. The extra state would look something like this:

Note: Anywhere you use the prop datas, you'd instead use fluctuatingList.

const Home: NextPage<HomePageProps> = ({ datas }) => {
  const [fluctuatingList, setFluctuatingList] = useState(datas);
  // You can still set the filtered list to initially be set with `datas`
  const [filteringList, setFilteringList] = useState(datas);
  const [search, setSearch] = useState("");
  const mutation = trpc.selectedData.addItemToShopingList.useMutation();
...

Then when updating the search, map using the fluctuatingList:

  const searchHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    // Check the line below
    const filteredList = fluctuatingList.map((list) => {
      return {
        ...list,
        items: list.items.filter((item) =>
          item.toLowerCase().includes(e.target.value.toLowerCase())
        ),
      };
    });
    setFilteringList(filteredList);
  };

Now, I'll be honest, I did not take the time to digest your snippets to give a usable solution, but guessing what @prisma/client's Data type is, I am going to assume it looks like an object with the three parameters in this function:

  const addItemToShopingList = async (
    item: string,
    name: string,
    id: string
  ) => {
    if (mutation.error) {
      return alert("Something went wrong");
    }

    // Pulling this into a `const` for reuse
    const newData = {
      id,
      name,
      item,
    };

    // Here is where we mutate the data, I am guessing through some API call
    // We also want to set the fluctuating state here:
    setFluctuatingList((prevList) => ([ ...Array.from(prevList), newData ]));
    mutation.mutate(newData);
  };

Thumbs me up if you think its a good answer!