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.
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;
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
.