I’m making a booking form using shadcn/ui in nextjs13. I’m mapping over hairstyles in my postgres db to make selects for the user to choose a hairstyle. I’m getting the error Encountered two children with the same key , 'Coils/Butterfly Locs/Individual Braids/etc.
. I confirmed using a postgres query that all my hairstkye ids are unique. I’m not sure if it has something to do with the order of the components are what. Any help is greatly appreciated and maybe a better way to do this will be helpful. It’s certain hairstyles that are giving this error and they have unique ids so I’m confused. I can send the csv of my hairstyles table if necessary.
My booking form component:
"use client";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { format } from "date-fns";
import { Calendar } from "@/components/ui/calendar";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Card, CardTitle, CardContent } from "@/components/ui/card";
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { CalendarIcon } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import React, { useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Hairstyles } from "@prisma/client";
interface BookingFormProps {
adultHairstyles: Hairstyles[];
colourServices: Hairstyles[];
kidsHairstyles: Hairstyles[];
locsHairTreatmentHairstyles: Hairstyles[];
locsPackageHairstyles: Hairstyles[];
locsStylesHairstyles: Hairstyles[];
naturalHairHairstyles: Hairstyles[];
silkPressHairstyles: Hairstyles[];
simpleLocsStylesHairstyles: Hairstyles[];
starterLocsHairstyles: Hairstyles[];
}
const phoneRegex = new RegExp(
/^([+]?[s0-9]+)?(d{3}|[(]?[0-9]+[)])?([-]?[s]?[0-9])+$/
);
const formSchema = z.object({
firstname: z.string().min(2, "Too Short!").max(50, "Too Long!"),
lastname: z.string().min(2, "Too Short!").max(50, "Too Long!"),
phone: z.string().regex(phoneRegex, "Invalid Number!"),
hairIsWashed: z.boolean().default(false).optional(),
date: z.date().min(new Date(), "Invalid Date!"),
time: z.string().min(4, "Invalid Time!").max(5, "Invalid Time!"),
hairstyle: z.string().min(2, "Too Short!").max(50, "Too Long!"),
additionalInfo: z.string().min(2, "Too Short!").max(50, "Too Long!"),
});
function BookingForm({
adultHairstyles,
colourServices,
kidsHairstyles,
locsHairTreatmentHairstyles,
locsPackageHairstyles,
locsStylesHairstyles,
naturalHairHairstyles,
silkPressHairstyles,
simpleLocsStylesHairstyles,
starterLocsHairstyles,
}: BookingFormProps) {
const [loading, setLoading] = useState<boolean>(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
firstname: "",
lastname: "",
phone: "",
hairIsWashed: false,
date: new Date(),
time: "",
hairstyle: "",
additionalInfo: "",
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
console.log(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card className="w-full p-10 space-y-8 gap-2 mb-20 bg-slate-100">
<CardTitle className="text-center">Book an Appointment</CardTitle>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
name="firstname"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Firstname</FormLabel>
<FormControl>
<Input {...field} type="text" placeholder="John" />
</FormControl>
</FormItem>
)}
/>
<FormField
name="lastname"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Lastname</FormLabel>
<FormControl>
<Input {...field} type="text" placeholder="Doe" />
</FormControl>
</FormItem>
)}
/>
<FormField
name="phone"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Phone</FormLabel>
<FormControl>
<Input {...field} type="text" placeholder="" />
</FormControl>
</FormItem>
)}
/>
<FormField
name="date"
control={form.control}
render={({ field }) => (
<FormItem className="flex flex-col mt-2">
<FormLabel>Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-[280px] justify-start text-left font-normal",
!field.name && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent>
<FormControl>
<Calendar
mode={"single"}
{...field}
selected={field.value}
onSelect={(date) => field.onChange(date)}
/>
</FormControl>
</PopoverContent>
</Popover>
</FormItem>
)}
/>
<FormField
name="time"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Time</FormLabel>
<FormControl>
<Input {...field} type="time" placeholder="" />
</FormControl>
</FormItem>
)}
/>
<FormField
name="hairstyle"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Hairstyle</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Choose a hairstyle..." />
</SelectTrigger>
</FormControl>
<SelectContent>
<ScrollArea className="h-[200px] w-[350px] rounded-md border p-4">
<p className="text-base text-gray-400">Adults</p>
{adultHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">
Colour Services
</p>
{colourServices.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">Kids</p>
{kidsHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">
Locs Hair Treatment
</p>
{locsHairTreatmentHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">
Locs Package
</p>
{locsPackageHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">Locs Styles</p>
{locsStylesHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">
Natural Hair & External Care
</p>
{naturalHairHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">Silk Press</p>
{silkPressHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">
Simple Locs Styles
</p>
{simpleLocsStylesHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
<p className="text-base text-gray-400">
Starter Locs
</p>
{starterLocsHairstyles.map((hairstyle) => (
<SelectItem
key={hairstyle.id}
value={hairstyle.name}
className="font-semibold text-sm"
>
{hairstyle.name}
</SelectItem>
))}
</ScrollArea>
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
name="hairIsWashed"
control={form.control}
render={({ field }) => (
<FormItem className="flex flex-col space-y-4">
<FormLabel>
Will your hair be prewashed before appointment?
</FormLabel>
<p className="text-sm text-muted-foreground">
Check the box if so.
</p>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
name="additionalInfo"
control={form.control}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Additional Info</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="Any additional info you want to add..."
/>
</FormControl>
</FormItem>
)}
/>
</div>
</CardContent>
<div className="flex justify-center items-center">
<Button type="submit" className="text-center">
{loading ? "Loading..." : "Book an Appointment"}
</Button>
</div>
</Card>
</form>
</Form>
);
}
export default BookingForm;
my page.tsx:
import BookingForm from "@/components/BookingForm";
import prisma from "@/lib/db";
async function getAdultHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "adults",
},
});
return hairstyles;
}
async function getColourServicesHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Colour-Services",
},
});
return hairstyles;
}
async function getKidsHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "kids",
},
});
return hairstyles;
}
async function getLocsHairTreatmentHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Locs-Hair-Treatment",
},
});
return hairstyles;
}
async function getLocsPackageHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Locs-Package",
},
});
return hairstyles;
}
async function getLocsStylesHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Locs-Styles-and-Protective-Styles",
},
});
return hairstyles;
}
async function getNaturalHairHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Natural-Hair-and-External-Care",
},
});
return hairstyles;
}
async function getSilkPressHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Silk-Press",
},
});
return hairstyles;
}
async function getSimpleLocsHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Simple-Locs-Styles",
},
});
return hairstyles;
}
async function getStarterLocsHairstyles() {
const hairstyles = await prisma.hairstyles.findMany({
where: {
category: "Starter-Locs",
},
});
return hairstyles;
}
async function BookingPage() {
const adultHairstyles = await getAdultHairstyles();
const colourServices = await getColourServicesHairstyles();
const kidsHairstyles = await getKidsHairstyles();
const locsHairTreatmentHairstyles = await getLocsHairTreatmentHairstyles();
const locsPackageHairstyles = await getLocsPackageHairstyles();
const locsStylesHairstyles = await getLocsStylesHairstyles();
const naturalHairHairstyles = await getNaturalHairHairstyles();
const silkPressHairstyles = await getSilkPressHairstyles();
const simpleLocsStylesHairstyles = await getSimpleLocsHairstyles();
const starterLocsHairstyles = await getStarterLocsHairstyles();
return (
<BookingForm
adultHairstyles={adultHairstyles}
colourServices={colourServices}
kidsHairstyles={kidsHairstyles}
locsHairTreatmentHairstyles={locsHairTreatmentHairstyles}
locsPackageHairstyles={locsPackageHairstyles}
locsStylesHairstyles={locsStylesHairstyles}
naturalHairHairstyles={naturalHairHairstyles}
silkPressHairstyles={silkPressHairstyles}
simpleLocsStylesHairstyles={simpleLocsStylesHairstyles}
starterLocsHairstyles={starterLocsHairstyles}
/>
);
}
export default BookingPage;
2
Answers
Fixed the issue by making all
hairstyle.name
unique. Therefore, I had to make sure all the hairstyles names in the database was unique. I don't know why I got an error withkey
when that was the issue. I would prefer if I could keep the duplicate hairstyle names.There’s somewhat limited amount of information here, but generally this kind of error can easily be avoid as long as the
key
remains not only unique and predictable (so ReactJS doesn’t treat the looped component as new component) by doing soso you can make your key a bit more unique like so —
Finally, together, you will have this —
Hope this will solve your issue.