Search code examples
reactjstypescriptmongodbnext.jscaching

Why does my component's values not change even when i am re-rendering the same component but with different values? Could it be due to Nextjs caching?


I am not sure if i explained it properly on the title but here is an explanation.

I am creating a habit tracker, this habit tracker allows users to create their own habits which gets stored in a habits mongodb table, then there is a calendar UI on the app where the user can click different dates and it sets the currentDate state to whatever the date is selected as, i have a useEffect hook that listen's to the currentDate state being changed so that it fetches the habits for that particular date. Users can then add a progress value (e.g 2 out of 3 glasses of water drank) to their habit and then submit which will now create a habitInstance on mongoDB, which contains the currentValue and the goalValue and the completed value along with the habitId and date for that instance.

I have found that, i can check how many instances are made for that day and see how many are completed to find out the user's progress for that particular day as i already have all the number of habits for that day already.

The issue now is, when i switch between different dates and the same habit is rendered, the progress value does not change, only when i reload the whole page it does.

let me show examples:

As you can see here, the 2nd of july habit is completed 2/3, and that's right because there is an instance made for it already:

However when i now change the date, the same habit shows the same value although it isn't even an instance yet.

Only when i refresh the page it shows me the correct one.

I do not know if the way i am doing my project is the most efficient way, so if there's better ways to do it please advise me.

but here is my code:

Page.tsx

"use client";

import AddHabitDialog from "@/components/add-habit-dialog";
import { DayCarousel } from "@/components/day-carousel";
import GoalCard from "@/components/goal-card";
import Habit from "@/components/habit";
import { cn } from "@/lib/utils";
import { useUser } from "@clerk/nextjs";
import React, { useEffect, useState, useCallback } from "react";
import { getUserHabitsByDay } from "./_actions";
import { HabitType } from "@/types/types";

function Dashboard() {
  const { user } = useUser();
  const [currentDate, setCurrentDate] = useState(() => {
    const date = new Date();
    return date.toISOString().split("T")[0]; // Format: yyyy-mm-dd
  });

  const getDayOfWeek = useCallback((dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString("en-US", { weekday: "long" });
  }, []);

  const [currentView, setCurrentView] = useState("All Day");
  const [baseDate, setBaseDate] = useState(new Date());
  const [habits, setHabits] = useState<HabitType[]>([]);

  useEffect(() => {
    if (!user) return;
    const fetchCurrentDayHabits = async () => {
      try {
        const habits = await getUserHabitsByDay({
          clerkUserId: user.id,
          day: getDayOfWeek(currentDate),
        });
        setHabits(habits);
      } catch (e) {
        console.error(e);
      }
    };
    fetchCurrentDayHabits();
  }, [currentDate, user, getDayOfWeek]);

  const views = ["All Day", "Morning", "Afternoon", "Evening"];

  return (
    <section className="h-screen container flex flex-col items-center">
      <AddHabitDialog />
      <DayCarousel
        baseDate={baseDate}
        setCurrentDate={setCurrentDate}
        currentDate={currentDate}
      />
      <div className="container px-20 xl:px-80 flex flex-col mt-10 gap-y-5">
        <div className="flex justify-between">
          {views.map((view, index) => (
            <h1
              key={index}
              onClick={() => setCurrentView(view)}
              className={cn(
                "text-xl font-light hover:cursor-pointer relative pb-1",
                currentView === view && "relative"
              )}
            >
              {view}
              {currentView === view && (
                <span
                  className="absolute bottom-0 left-0 bg-secondary h-1 rounded-xl"
                  style={{ width: "3rem" }}
                ></span>
              )}
            </h1>
          ))}
        </div>
        <div className="space-y-2">
          <h1 className="font-light text-gray-500 text-2xl">Goals</h1>
          <div className="space-y-2">
            <GoalCard />
            <GoalCard />
          </div>
        </div>
        <div className="space-y-2">
          <h1 className="font-light text-gray-500 text-2xl">Habits</h1>
          <div className="space-y-2">
            {habits.map((habit) => (
              <Habit
                key={habit._id}
                habitId={habit._id}
                title={habit.title}
                date={currentDate}
                frequency={habit.frequency}
                color={habit.color}
                unit={habit.unit}
              />
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

export default Dashboard;

day-carousel

import * as React from "react";
import { format, addDays, subDays, startOfWeek } from "date-fns";
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";
import CalendarDay from "./calendar-day";

export function DayCarousel({
  currentDate,
  baseDate,
  setCurrentDate,
}: {
  currentDate: string;
  baseDate: Date;
  setCurrentDate: (date: string) => void;
}) {
  const generateWeekDates = (startDate: Date) => {
    const start = startOfWeek(startDate, { weekStartsOn: 1 }); // Assuming week starts on Monday
    return Array.from({ length: 7 }).map((_, i) => addDays(start, i));
  };

  const getWeeksAroundCurrentDate = () => {
    const previousWeek = generateWeekDates(subDays(baseDate, 7));
    const currentWeek = generateWeekDates(new Date(baseDate));
    const nextWeek = generateWeekDates(addDays(baseDate, 7));
    const nextNextWeek = generateWeekDates(addDays(baseDate, 14));
    return [previousWeek, currentWeek, nextWeek, nextNextWeek];
  };

  const weeks = getWeeksAroundCurrentDate();

  return (
    <Carousel className="mt-10 w-[90%]">
      <CarouselContent className="">
        {weeks.map((week, weekIndex) => (
          <CarouselItem
            key={weekIndex}
            className="flex w-20 justify-between xl:justify-center xl:gap-x-6 xl:px-40"
          >
            {week.map((date, index) => (
              <CalendarDay
                key={index}
                setCurrentDate={setCurrentDate}
                day={format(date, "E")}
                date={date}
                dateNum={format(date, "d")}
                isCurrentDay={
                  format(date, "yyyy-MM-dd") ===
                  format(currentDate, "yyyy-MM-dd")
                }
              />
            ))}
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  );
}

_Actions.ts

"use server";

import HabitInstance from "@/models/HabitInstancesSchema";
import Habit from "@/models/HabitSchema";
import User from "@/models/UserSchema";
import { revalidatePath } from "next/cache";

const findUserByClerkId = async (clerkUserID: string) => {
  const user = await User.findOne({ clerkUserID });
  if (!user) throw new Error("User not found");
  return user;
};

const findUserById = async (userId: string) => {
  const user = await User.findById(userId);
  if (!user) throw new Error("User not found");
  return user;
};

const findHabitById = async (habitId: string) => {
  const habit = await Habit.findById(habitId);
  if (!habit) throw new Error("Habit not found");
  return habit;
};

const isHabitCompleted = async ({
  habitId,
  userId,
  date,
}: {
  habitId: string;
  userId: string;
  date: string;
}) => {
  const habitInstance = await HabitInstance.findOne({
    habitId,
    userId,
    date,
  });
  return !!habitInstance;
};

const getUserHabitsByDay = async ({
  clerkUserId,
  day,
}: {
  clerkUserId: string;
  day: string;
}) => {
  try {
    const user = await findUserByClerkId(clerkUserId);
    const habits = await Habit.find({ userId: user._id, repeat: day });
    return JSON.parse(JSON.stringify(habits));
  } catch (error) {
    console.error(`Error getting user habits: ${error}`);
    return [];
  }
};

const deleteHabitInstance = async ({
  habitId,
  userId,
  date,
}: {
  habitId: string;
  userId: string;
  date: string;
}) => {
  try {
    await HabitInstance.deleteOne({ habitId, userId, date });
  } catch (error) {
    console.error(`Error deleting habit instance: ${error}`);
  }
};

const findHabitInstance = async ({
  clerkUserId,
  habitId,
  date,
}: {
  clerkUserId: string;
  habitId: string;
  date: string;
}) => {
  try {
    const user = await findUserByClerkId(clerkUserId);
    const habitInstance = await HabitInstance.findOne({
      userId: user._id,
      date,
    });
    return JSON.parse(JSON.stringify(habitInstance)) || null;
  } catch (e) {
    return null;
  }
};

const createHabitInstance = async ({
  clerkUserId,
  habitId,
  value,
  goal,
  date,
}: {
  clerkUserId: string;
  habitId: string;
  date: string;
  value: number;
  goal: number;
}) => {
  try {
    const user = await findUserByClerkId(clerkUserId);
    const habitInstance = await findHabitInstance({
      clerkUserId,
      date,
      habitId,
    });

    if (!habitInstance) {
      const newHabitInstance = new HabitInstance({
        userId: user._id,
        habitId,
        value,
        goal,
        date,
      });
      await newHabitInstance.save();
    } else {
      await HabitInstance.updateOne(
        { _id: habitInstance._id },
        { value, completed: value >= goal }
      );
    }
  } catch (error) {
    console.error(`Error creating habit instance: ${error}`);
  }
};

const createHabit = async ({
  clerkUserID,
  title,
  color,
  description,
  repeat,
  frequency,
  unit,
  time,
}: {
  clerkUserID: string;
  title: string;
  color: string;
  description: string;
  repeat: string[];
  frequency: number;
  unit: string;
  time: string;
}) => {
  try {
    const user = await findUserByClerkId(clerkUserID);

    const newHabit = new Habit({
      userId: user._id,
      title,
      color,
      description,
      repeat,
      frequency,
      unit,
      time,
    });
    await newHabit.save();
    revalidatePath("/dashboard");
  } catch (error) {
    console.error(`Error creating habit: ${error}`);
  }
};

export {
  createHabit,
  getUserHabitsByDay,
  createHabitInstance,
  deleteHabitInstance,
  isHabitCompleted,
  findHabitInstance,
};

edit-habit-dialog.tsx

"use client";

import React, { useEffect, useState } from "react";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "./ui/dialog";
import { Button } from "./ui/button";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "./ui/form";
import { Input } from "./ui/input";
import {
  createHabitInstance,
  findHabitInstance,
} from "@/app/dashboard/_actions";
import { useUser } from "@clerk/nextjs";
import { HabitInstance } from "@/types/types";

const formSchema = z.object({
  currValue: z.preprocess((val) => Number(val), z.number()),
});

const EditHabitDialog = ({
  habitId,
  date,
  goal,
  value,
  unit,
}: {
  habitId: string;
  date: string;
  goal: number;
  value: number;
  unit: string;
}) => {
  const { user } = useUser();

  const methods = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      currValue: value,
    },
  });

  const onSubmit = async (values: z.infer<typeof formSchema>) => {
    if (!user) return;
    await createHabitInstance({
      clerkUserId: user.id,
      habitId,
      value: values.currValue,
      date,
      goal,
    });
  };

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button className="bg-tertiary rounded-md w-12 h-12 flex items-center justify-center hover:cursor-pointer hover:bg-secondary text-4xl text-secondary hover:text-white">
          +
        </Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Edit Habit</DialogTitle>
          <DialogDescription>
            Here you can update the value of your habit.
          </DialogDescription>
        </DialogHeader>
        <FormProvider {...methods}>
          <form onSubmit={methods.handleSubmit(onSubmit)} className="space-y-8">
            <FormField
              name="currValue"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Value</FormLabel>
                  <FormControl>
                    <Input className="w-20" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <div className="flex justify-end">
              <Button type="submit">Submit</Button>
            </div>
          </form>
        </FormProvider>
        {JSON.stringify({ habitId, date, goal, value, unit })}
      </DialogContent>
    </Dialog>
  );
};

export default EditHabitDialog;

habit.tsx

"use client";
import React, { useEffect, useState } from "react";
import EditHabitDialog from "./edit-habit-dialog";
import { cn } from "@/lib/utils";
import { findHabitInstance } from "@/app/dashboard/_actions";
import { useUser } from "@clerk/nextjs";
import { HabitInstance } from "@/types/types";

const Habit = ({
  title,
  frequency,
  unit,
  date,
  habitId,
  color,
}: {
  title: string;
  frequency: number;
  unit: string;
  date: string;
  habitId: string;
  color: string;
}) => {
  const { user } = useUser();
  const [habitInstance, setHabitInstance] = useState<HabitInstance | null>(
    null
  );

  //TODO: Habit progress does not change properly when swithcing between dates

  useEffect(() => {
    const isInstance = async () => {
      if (!user) return null;
      const habitInstance = await findHabitInstance({
        clerkUserId: user.id,
        date,
      });

      if (!habitInstance) {
        return null;
      }
      console.log("It's an instance!");
      setHabitInstance(habitInstance);
    };

    isInstance();
  }, []);

  return (
    <div
      className={cn(
        "bg-white border-l-8 rounded-md p-4 flex items-center justify-between",
        `border-l-${color}`
      )}
    >
      <div className="flex gap-x-3">
        <div className="h-12 w-12 bg-secondary rounded-full" />
        <div className="space-y-[1px]">
          <h1>{title}</h1>
          <p className="text-gray-300">{`${
            habitInstance ? habitInstance.value : 0
          } / ${frequency} ${unit}`}</p>
        </div>
      </div>
      <EditHabitDialog
        date={date}
        habitId={habitId}
        value={habitInstance ? habitInstance.value : 0}
        goal={frequency}
        unit={unit}
      />
    </div>
  );
};

export default Habit;

Solution

  • The solution to this problem was that my habit.tsx component does not fully re-render, and that means that the useState's value from the last render remains the same even though new habits are rendered:

    const [habitInstance, setHabitInstance] = useState<HabitInstance | null>(
       null
    );
    

    So in the line of:

    if (!habitInstance) {
       return null;
    }
    

    You have to set the habit instance as null instead of returning null.