I am implementing a form with the help of the Formik and yup validation in Nextjs or Typescript application. I am using the react-select library for the select box to choose multiple and single values. I have built reusable components of Input, Select, or DatePicker, and Tiptap editor input as shown below. Everything is working fine but there is a problem coming when I am trying to submit the form, form with submitting I am using the Fromik resetForm method to reset all the values from all inputs(Select, Input, DatePicker, Tiptap editor). But is only resetting the Input value not the Select and DatePicker value after submitting the form. I have used the Formik setFieldValue method to reset the Select or DatePicker and Tiptap but not work. Please help me to resolve this issue on how to reset the Select DatePicker and Tiptap input in Formik.
Tiptap.tsx
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import BulletList from "@tiptap/extension-bullet-list";
import OrderedList from "@tiptap/extension-ordered-list";
import ListItem from "@tiptap/extension-list-item";
import Bold from "@tiptap/extension-bold";
import Italic from "@tiptap/extension-italic";
import Underline from "@tiptap/extension-underline";
import Placeholder from "@tiptap/extension-placeholder";
import { FC } from "react";
import {
BoldIcon,
ItalicIcon,
List,
ListOrdered,
UnderlineIcon,
} from "lucide-react";
type TiptapProps = {
id: string;
name: string;
label?: string;
errorMsg?: string;
error?: boolean;
value: string;
onChange: (value: string) => void;
};
const Tiptap: FC<TiptapProps> = ({
label,
error,
id,
name,
value,
onChange,
errorMsg,
}) => {
const editor = useEditor({
extensions: [
StarterKit,
Paragraph,
Text,
BulletList,
OrderedList,
ListItem,
Bold,
Italic,
Underline,
Placeholder.configure({
placeholder: "Write something …",
}),
],
content: value,
onUpdate: (value) => {
const text = value.editor.getHTML().toString();
onChange(text);
},
});
if (!editor) {
return null;
}
const btnClass = `opacity-70 p-1 rounded`;
return (
<div>
{label && (
<label
htmlFor={id}
className={`text-sm mb-0.5 font-medium ${
error ? "text-rose-600" : "text-gray-900"
}`}
>
{label}
</label>
)}
<div className="border relative border-gray-300 w-full rounded-md h-[250px] dark:bg-zinc-700 dark:border-zinc-400">
<div className="flex flex-wrap gap-2 px-4 py-2.5 border-b border-gray-300 dark:border-zinc-400 w-full">
<button
type="button"
onClick={() => editor.chain().focus().toggleBold().run()}
className={`${btnClass} ${
editor.isActive("bold")
? "bg-neutral-300 dark:bg-neutral-400 opacity-100 text-black"
: ""
}`}
>
<BoldIcon className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().toggleItalic().run()}
className={`${btnClass} ${
editor.isActive("italic")
? "bg-neutral-300 dark:bg-neutral-400 opacity-100 text-black"
: ""
}`}
>
<ItalicIcon className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => editor.chain().focus().toggleUnderline().run()}
className={`${btnClass} ${
editor.isActive("underline")
? "bg-neutral-300 dark:bg-neutral-400 opacity-100 text-black"
: ""
}`}
>
<UnderlineIcon className="w-5 h-5" />
</button>
<div className="sm:block hidden w-[1px] h-6 mx-4 bg-zinc-500 rounded-full"></div>
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
className={`${btnClass} ${
editor.isActive("bulletList")
? "bg-neutral-300 dark:bg-neutral-400 opacity-100 text-black"
: ""
}`}
onClick={() => editor.chain().focus().toggleBulletList().run()}
>
<List className="w-5 h-5" />
</button>
<button
type="button"
className={`${btnClass} ${
editor.isActive("orderedList")
? "bg-neutral-300 dark:bg-neutral-400 opacity-100 text-black"
: ""
}`}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
>
<ListOrdered className="w-5 h-5" />
</button>
</div>
</div>
<EditorContent editor={editor} name={name} id={id} />
<span className="absolute bottom-4 right-4 text-sm opacity-60">
{Math.min(value.length, 200)}/200
</span>
</div>
{errorMsg && <small className="text-rose-600 ">{errorMsg}</small>}
</div>
);
};
export default Tiptap;
DatePicker.tsx
import dayjs from "dayjs";
import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
interface DatePickerProps {
error?: boolean;
errorMsg?: string;
placeholder?: string;
id: string;
label?: string;
value: string;
name: string;
onChange: (date: string | null) => void;
}
const DatePicker = (props: DatePickerProps) => {
const { error, id, label, placeholder, errorMsg, value, onChange, name } =
props;
const [currentDate, setCurrentDate] = useState(dayjs());
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const [showYearPicker, setShowYearPicker] = useState(false);
const [showMonthPicker, setShowMonthPicker] = useState(false);
const [selectedYear, setSelectedYear] = useState(currentDate.year());
const [selectedMonth, setSelectedMonth] = useState(currentDate.month());
const datePickerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
datePickerRef.current &&
!datePickerRef.current.contains(event.target as Node)
) {
setShowDatePicker(false);
setShowYearPicker(false);
setShowMonthPicker(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
useEffect(() => {
if (value) {
const parsedDate = dayjs(value, "DD MMM, YYYY");
if (parsedDate.isValid()) {
setSelectedDate(parsedDate);
setCurrentDate(parsedDate);
setSelectedYear(parsedDate.year());
setSelectedMonth(parsedDate.month());
}
}
}, [value]);
const daysInMonth = currentDate.daysInMonth();
const startOfMonth = currentDate.startOf("month").day();
const days = Array.from(
{ length: daysInMonth + startOfMonth },
(_, index) => {
const day = currentDate.startOf("month").add(index - startOfMonth, "day");
return {
day,
isCurrentMonth: day.month() === currentDate.month(),
isToday: day.isSame(dayjs(), "day"),
isSelected: day.isSame(selectedDate, "day"),
};
}
);
const handlePreviousMonth = () =>
setCurrentDate(currentDate.subtract(1, "month"));
const handleNextMonth = () => setCurrentDate(currentDate.add(1, "month"));
const handleDateSelect = (day: dayjs.Dayjs) => {
setSelectedDate(day);
setCurrentDate(day);
onChange(day.format("DD MMM, YYYY"));
setShowDatePicker(false);
};
const toggleDatePicker = useCallback(
() => setShowDatePicker((prev) => !prev),
[]
);
const toggleYearPicker = () => setShowYearPicker(!showYearPicker);
const handleYearSelect = (year: number) => {
setSelectedYear(year);
setCurrentDate(currentDate.year(year).month(selectedMonth));
setShowYearPicker(false);
setShowMonthPicker(true);
};
const handleMonthSelect = (month: number) => {
setSelectedMonth(month);
setCurrentDate(currentDate.year(selectedYear).month(month)); // Update currentDate
setShowMonthPicker(false);
setShowYearPicker(false);
};
const weeks = ["S", "M", "T", "W", "T", "F", "S"];
const yearOptions = Array.from(
{ length: dayjs().year() - 1992 + 1 },
(_, index) => 1992 + index
);
const renderButton = (
key: React.Key,
onClick: () => void,
label: string,
isActive: boolean
) => (
<button
key={key}
onClick={onClick}
className={`mx-1 p-1 rounded transition ${
isActive
? "bg-primary text-white"
: "dark:hover:bg-zinc-500 hover:bg-gray-200"
}`}
>
{label}
</button>
);
return (
<div>
{label && (
<label
htmlFor={id}
className={`text-sm mb-0.5 font-medium ${
error ? "text-rose-600" : "text-gray-900"
}`}
>
{label}
</label>
)}
<div
ref={datePickerRef}
className="relative w-full"
onClick={toggleDatePicker}
>
<input
id={id}
type="text"
placeholder={placeholder}
name={name}
value={selectedDate ? selectedDate.format("DD MMM, YYYY") : ""}
readOnly
className={`
w-full
outline-0
border
p-2
h-11
rounded-md
text-sm
text-gray-900
focus:ring-1
${
error
? "border-rose-600 focus:ring-rose-600 placeholder:text-red-400"
: "border-gray-300 focus:ring-primary"
}
bg-white
dark:bg-zinc-700
dark:text-white
dark:border-zinc-400
peer
`}
/>
<CalendarDays className="absolute top-1/2 -translate-y-1/2 right-2 w-5 h-5 opacity-65 peer-focus:opacity-100 cursor-pointer" />
{showDatePicker && (
<div
onClick={(event) => event.stopPropagation()}
className="absolute w-full top-12 rounded-md drop-shadow left-0 right-0 bg-white dark:bg-zinc-700 h-fit shadow z-10"
>
<div className="h-12 border-b w-full px-4 flex items-center justify-between dark:border-zinc-500 ">
<button
type="button"
onClick={handlePreviousMonth}
className="w-9 h-9 rounded-full border flex items-center justify-center hover:bg-neutral-300 transition duration-300 dark:hover:bg-zinc-500 dark:border-zinc-400"
>
<ChevronLeft size={20} />
</button>
<span
onClick={toggleYearPicker}
className="font-medium cursor-pointer"
>
{dayjs(currentDate).format("MMMM YYYY")}
</span>
<button
type="button"
onClick={handleNextMonth}
className="w-9 h-9 rounded-full border flex items-center justify-center hover:bg-neutral-300 transition duration-300 dark:hover:bg-zinc-500 dark:border-zinc-400"
>
<ChevronRight />
</button>
</div>
{showYearPicker ? (
<div className="grid sm:grid-cols-5 grid-cols-3 max-h-[350px] overflow-y-auto gap-4 py-2 px-4">
{yearOptions
.reverse()
.map((year) =>
renderButton(
year,
() => handleYearSelect(year),
year.toString(),
year === selectedYear
)
)}
</div>
) : showMonthPicker ? (
<div className="px-4 py-2 grid grid-cols-3 gap-4">
{Array.from({ length: 12 }, (_, index) =>
renderButton(
index,
() => handleMonthSelect(index),
dayjs().month(index).format("MMMM"),
index === selectedMonth
)
)}
</div>
) : (
<>
<div className="grid grid-cols-7 px-4 py-2 place-items-center">
{weeks.map((week, weekIdx) => (
<div
key={weekIdx.toString()}
className="text-sm font-medium text-gray-600 dark:text-neutral-300"
>
{week}
</div>
))}
</div>
<div className="grid grid-cols-7 place-items-center gap-2 px-4 py-2">
{days.map((day, dayIdx) => {
const isToday = day.isToday;
const isSelected = day.isSelected;
const isCurrentMonth = day.isCurrentMonth;
return (
<div key={dayIdx.toString()}>
<button
type="button"
onClick={() => handleDateSelect(day.day)}
className={`w-8 h-8 flex items-center text-sm rounded-md justify-center ${
isToday
? "bg-primary text-white"
: isSelected
? "bg-primary text-white"
: ""
} ${isCurrentMonth ? "" : "opacity-60"}`}
>
{dayjs(day.day).format("D")}
</button>
</div>
);
})}
</div>
</>
)}
</div>
)}
</div>
{errorMsg && <small className="text-rose-600 px-1">{errorMsg}</small>}
</div>
);
};
export default DatePicker;
Select.tsx
import { Option } from "@/types";
import { useTheme } from "next-themes";
import { useMemo } from "react";
import ReactSelect, {
CSSObjectWithLabel,
GroupBase,
MultiValue,
SingleValue,
StylesConfig,
} from "react-select";
interface SelectProps {
id: string;
placeholder?: string;
options: string[];
isMulti?: boolean;
name: string;
onChange: (selectedValue: string | string[]) => void;
value: string | string[];
error?: boolean;
label?: string;
errorMsg?: string;
}
const Select = (props: SelectProps) => {
const {
id,
isMulti,
placeholder,
name,
value,
onChange,
options,
error,
label,
errorMsg,
} = props;
const { theme } = useTheme();
const colorStyles: StylesConfig<Option, boolean, GroupBase<Option>> = {
control: (base: CSSObjectWithLabel) => ({
...base,
borderColor: error
? "red"
: theme === "dark"
? "#a1a1aa"
: base.borderColor,
boxShadow: error ? "0 0 0 1px red" : base.boxShadow,
"&:hover": {
borderColor: error
? "red"
: theme === "dark"
? "#a1a1aa"
: base.borderColor,
},
padding: "3px 0",
"::placeholder": {
color: error ? " #e11d48" : "",
},
backgroundColor: theme === "dark" ? "#3f3f46" : "#ffffff",
}),
};
const inputOptions = useMemo(
() =>
options.map((option) => ({
value: option,
label: option,
})),
[options]
);
const handleSelectChange = (
selected: SingleValue<Option> | MultiValue<Option>
) => {
if (isMulti) {
const values = (selected as MultiValue<Option>).map(
(option) => option.value
);
onChange(values);
} else {
onChange((selected as Option).value);
}
};
return (
<div className="flex flex-col">
<label
htmlFor={id}
className={`text-sm mb-0.5 font-medium ${
error ? "text-rose-600" : "text-gray-900"
}`}
>
{label}
</label>
<ReactSelect
isClearable
name={name}
id={id}
options={inputOptions}
isMulti={isMulti}
placeholder={placeholder}
value={
isMulti
? inputOptions.filter((option) =>
(value as string[]).includes(option.value)
)
: inputOptions.find((option) => option.value === value)
}
onChange={handleSelectChange}
styles={colorStyles}
/>
{errorMsg && <small className="text-red-600 px-1">{errorMsg}</small>}
</div>
);
};
export default Select;
GenaeralInfo.tsx
"use client";
import DatePicker from "../Inputs/DatePicker";
import Input from "../Inputs/Input";
import Select from "../Inputs/Select";
import Tiptap from "../Tiptap";
import * as Yup from "yup";
import useCountries from "@/hooks/useCountries";
import useStates from "@/hooks/useStates";
import { useFormik } from "formik";
import { teams } from "@/constants";
const schema = Yup.object().shape({
team: Yup.string().required("Team is required"),
contractName: Yup.string().required("Contract Name is required"),
country: Yup.string().required("Country is required"),
state: Yup.string().required("State is required"),
startDate: Yup.string().required("Start Date is required"),
workDescription: Yup.string()
.required("Work description are required")
.min(50, "Work description should be at least 50 characters long")
.max(200, "Work description can't exceed 200 characters"),
});
const GeneralInfo = () => {
const countries = useCountries();
const formik = useFormik({
initialValues: {
team: "",
contractName: "",
country: "",
state: "",
startDate: "",
workDescription: "",
},
validationSchema: schema,
onSubmit: (values, { resetForm }) => {
console.log(values);
resetForm();
},
});
const isoCode = countries.find(
(country) => country.name === formik.values.country
)?.isoCode;
const states = useStates(isoCode);
return (
<div className="w-full space-y-6">
<Select
label="Team"
id="team"
name="team"
placeholder="Select team"
options={teams.map((team) => team.name)}
value={formik.values.team}
onChange={(selectedOption) => {
formik.setFieldValue("team", selectedOption);
}}
error={!!formik.errors.team}
errorMsg={formik.errors?.team}
/>
<Input
id="contractName"
name="contractName"
label="Contract Name"
placeholder="Contract for Product Design"
value={formik.values.contractName}
onChange={formik.handleChange}
error={!!formik.errors.contractName}
errorMsg={formik.errors.contractName}
/>
<div className="grid grid-cols-2 gap-4">
<Select
label="Contractor's Country"
id="country"
name="country"
placeholder="Select country"
options={countries.map((country) => country.name)}
value={formik.values.country}
onChange={(selectedOption) => {
formik.setFieldValue("country", selectedOption);
}}
error={!!formik.errors.country}
errorMsg={formik.errors?.country}
/>
<Select
label="State"
id="state"
name="state"
placeholder="Select team"
options={states.map((state) => state.name)}
value={formik.values.state}
onChange={(selectedOption) => {
formik.setFieldValue("state", selectedOption);
}}
error={!!formik.errors.state}
errorMsg={formik.errors?.state}
/>
</div>
<DatePicker
label="Start Date"
id="startDate"
name="startDate"
placeholder="Select date"
onChange={(date) => formik.setFieldValue("startDate", date)}
value={formik.values.startDate}
error={!!formik.errors.startDate}
errorMsg={formik.errors?.startDate}
/>
<Tiptap
id="workDescription"
name="workDescription"
label="Work Description"
value={formik.values.workDescription}
onChange={(value) => {
formik.setFieldValue("workDescription", value);
}}
errorMsg={formik.errors?.workDescription as string}
/>
<button type="button" onClick={() => formik.handleSubmit()}>
Submit
</button>
</div>
);
};
export default GeneralInfo;