When I refresh my page I got these erorrs :
Text content does not match server-rendered HTML.
Hydration failed because the initial UI does not match what was rendered on the server.
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
This is my file code :
/* eslint-disable #next/next/no-img-element */
import Header from '#/components/Header'
import blockContent from '#/medium-sanity-copy/schemas/blockContent'
import { PostType, SlugType } from '#/types'
import { Formik, Field, Form, ErrorMessage } from 'formik'
import { FieldProps } from 'formik/dist/Field'
import { GetStaticProps } from 'next'
import Head from 'next/head'
import React from 'react'
import PortableText from 'react-portable-text'
import { client, urlFor } from './../../sanity'
import * as yup from 'yup'
interface PostProps {
post: PostType
}
let schema = yup.object().shape({
name: yup.string().required('Name field is required'),
comment: yup.string().required('Comment field is required'),
email: yup.string().email('Email field must be email type').required('Email field is required'),
})
const Post: React.FC<PostProps> = ({ post }) => {
const initialValues = {
name: '',
email: '',
comment: '',
}
return (
<div className="mb-10">
<Head>
<title>{post.title}</title>
<link rel="icon" href="https://cdn.iconscout.com/icon/free/png-256/medium-47-433328.png" />
</Head>
<Header />
<div className="max-w-3xl mx-auto px-5 md:px-0">
<div className="my-5">
<h1 className="text-3xl font-bold mb-3">{post.title}</h1>
<h2 className="text-xl text-gray-600">{post.description}</h2>
<div className="flex items-center space-x-3 my-3">
<img
className="h-10 w-10 rounded-full drop-shadow-md"
src={urlFor(post.author?.image).url()}
alt="avatar"
/>
<p className="font-extralight text-gray-600">
Blog post by <span className="text-yellow-600">{post.author?.name}</span> - Published
at {new Date(post._createdAt!).toLocaleString()}
</p>
</div>
</div>
<PortableText
projectId={process.env.NEXT_PUBLIC_SANITY_PROJECT_ID}
dataset={process.env.NEXT_PUBLIC_SANITY_DATASET}
content={post.body!}
serializers={{
h1: (props: any) => <h1 className="text-2xl text-bold my-5" {...props} />,
h2: (props: any) => <h2 className="text-xl text-bold my-5" {...props} />,
li: ({ children }: any) => <li className="ml-4 list-disc">{children}</li>,
link: ({ href, children }: any) => (
<a href={href} className="text-blue-500 hover:underline">
{children}
</a>
),
}}
/>
</div>
<hr className="color bg-yellow-600 h-1 max-w-xl mx-auto mt-10 mb-5" />
<Formik
initialValues={initialValues}
onSubmit={(values) => {
alert(`${values.comment}, ${values.email}, ${values.name}`)
}}
validationSchema={schema}>
{() => (
<Form className="max-w-2xl mx-auto p-5">
<span className="text-yellow-600">Enjoyed this article?</span>
<h1 className="text-3xl font-bold">Leave the comment below!</h1>
<hr className="my-2" />
<div className="flex flex-col my-3">
<label htmlFor="name">Name</label>
<Field name="name">
{({ field }: FieldProps) => (
<input
type="text"
{...field}
className="outline-none border py-2 px-3 rounded shadow"
/>
)}
</Field>
</div>
<div className="flex flex-col my-3">
<label htmlFor="email">Email</label>
<Field name="email">
{({ field }: FieldProps) => (
<input
type="text"
{...field}
className="outline-none border py-2 px-3 rounded shadow"
/>
)}
</Field>
</div>
<div className="flex flex-col my-3">
<label htmlFor="comment">Comment</label>
<Field name="comment">
{({ field }: FieldProps) => (
<textarea
{...field}
rows={8}
className="outline-none border resize-none py-2 px-3 rounded shadow"
/>
)}
</Field>
</div>
<div className="flex flex-col space-y-1 text-red-500 items-center">
<ErrorMessage name="name">{(err) => <span>{err}</span>}</ErrorMessage>
<ErrorMessage name="email">{(err) => <span>{err}</span>}</ErrorMessage>
<ErrorMessage name="comment">{(err) => <span>{err}</span>}</ErrorMessage>
</div>
</Form>
)}
</Formik>
</div>
)
}
export default Post
export const getStaticPaths = async () => {
const query = `
*[_type == "post"]{
_id,
slug
}
`
const posts = await client.fetch(query)
const paths = posts.map((post: { _id: string; slug: SlugType }) => ({
params: {
slug: post.slug.current,
},
}))
return { paths, fallback: false }
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const query = `
*[_type == "post" && slug.current == $slug][0]{
_id,
_createdAt,
description,
slug,
mainImage,
title,
author -> {
name,
image
},
body
}
`
const post = await client.fetch(query, { slug: params?.slug })
return {
props: {
post,
},
revalidate: 60,
}
}
I didn't find out a solution that I can understand and agree with
This is usually caused when you have a <div> or <h1> element inside a <p> elemnt.
You can find what is causing this issue if you check the console
Related
Please i need help
i am a beginer using react
I find it difficult get values and i just hope my explanation is clear
What will i do or which other approch is best for this
i want to get value={} from main <Controller> and pass it through render={} to be used in the child component <PhoneInput> the value={phone}
is an object that consist of arrays
this is the content of phone
[
{
country: 'bs',
phoneNumber: '122 234 4309',
label: 'Mobile',
},
{
country: 'bs',
phoneNumber: '898 776 9087',
label: 'Work',
},
];
the best of my knowledge is what i did but not it working
My aim is to enable user to add more phone number field by their self
i want a user to be able to delete field or add more phone field
Sample Image
<Controller
control={control}
name="phones"
value={phones} //phone is an array
render={({ field: { onChange, className, value, ref } }) => ( // Transfering value from value={phones} to use as value.map not working
<div className={clsx('mt-32', className)} ref={ref}>
{value.map((item, index) => ( //the value here becomes undefined
<PhoneInput
value={item}
key={index}
onChange={(val) => {
console.log(value);
onChange(value.map((_item, _index) => (index === _index ? val : _item)));
}}
onRemove={() => {
console.log(value);
onChange(value.filter((_item, _index) => index !== _index));
}}
hideRemove={value.length === 1}
/>
))}
<Button
className="group inline-flex items-center mt-2 -ml-4 py-2 px-4 rounded cursor-pointer"
onClick={() =>
onChange([...testPhone, Model().phoneNumbers[0]])
}
>
<span className="ml-8 font-medium text-secondary group-hover:underline">
Add a phone number
</span>
</Button>
</div>
)}
/>
if i send the value directly it works fine
<Controller
control={control}
name="phones"
value={phones}
render={({ field: { onChange, className, value, ref } }) => (
// <div className={clsx('mt-32', className)} ref={ref}>
{phones.map((item, index) => ( //Using variable phone directly
<PhoneInput
value={item}
key={index}
onChange={(val) => {
console.log(phones);
onChange(phones.map((_item, _index) => (index === _index ? val : _item)));
}}
onRemove={() => {
console.log(phones);
onChange(phones.filter((_item, _index) => index !== _index));
}}
hideRemove={phones.length === 1}
/>
))}
<Button
className="group inline-flex items-center mt-2 -ml-4 py-2 px-4 rounded cursor-pointer"
onClick={() =>
onChange([...phones, Model().phoneNumbers[0]])
}
>
<span className="ml-8 font-medium text-secondary group-hover:underline">
Add a phone number
</span>
</Button>
</div>
)}
/>
Here is the complete code
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import {
Button,
Card,
} from '#mui/material';
import clsx from 'clsx';
import { Controller, useForm } from 'react-hook-form';
import axios from 'axios';
import PhoneInput from '../../PhoneInput';
import Model from '../../Model';
/**
* Form Validation Schema
*/
const schema = yup.object().shape({
phone: yup.string().required('Phone is required'),
});
function Settings() {
const { control, watch, reset, handleSubmit, formState, getValues } = useForm({
mode: 'onChange',
resolver: yupResolver(schema),
});
const [phones, setPhones] = useState([]);
useEffect(() => {
axios.get('/api/v1/account').then((res) => {
if (res.data.success === true) {
setPhones(res.data.phones);
}
});
}, []);
return (
<Card
component={motion.div}
variants={IT}
className="w-full overflow-hidden w-full mb-32"
>
<div className=" p-24 w-full">
<Controller
control={control}
name="first_name"
render={({ field }) => (
<TextField
className="mt-32"
{...field}
label="First Name"
placeholder="First Name"
id="first_name"
error={!!errors.first_name}
helperText={errors?.first_name?.message}
variant="outlined"
required
fullWidth
/>
)}
/>
<Controller
control={control}
name="phones"
value={phones}
render={({ field: { onChange, className, value, ref } }) => (
<div className={clsx('mt-32', className)} ref={ref}>
{phones.map((item, index) => (
<PhoneInput
value={item}
key={index}
onChange={(val) => {
console.log(phones);
onChange(phones.map((_item, _index) => (index === _index ? val : _item)));
}}
onRemove={() => {
console.log(phones);
onChange(phones.filter((_item, _index) => index !== _index));
}}
hideRemove={phones.length === 1}
/>
))}
<Button
className="group inline-flex items-center mt-2 -ml-4 py-2 px-4 rounded cursor-pointer"
onClick={() =>
onChange([...phones, Model().phoneNumbers[0]])
}
>
<span className="ml-8 font-medium text-secondary group-hover:underline">
Add a phone number
</span>
</Button>
</div>
)}
/>
</div>
</Card>
);
}
export default Settings;
Thank you very much
I've been trying to figure out my invalid time value issue. I built an airbnb clone and I'm using daterangepicker to work with the calendar, save the range selected in state and use that information to display it as a placeholder in the search bar after the search. I followed the video online precisely but for some reason I get this error and the person online didn't. I searched related posts here but nothing really helped so hopefully someone can help me resolve the issue so that I can finally deploy it. It all works fine without any issues on my local server but once I refresh the results page it defaults to this error
RangeError: Invalid time value in pages/search.js
15 | //ES6 destructuring
16 | const { location, startDate, endDate, noOfGuests} = router.query;
> 17 | const formattedStartDate = format(new Date(startDate), "MM/dd/yy");
| ^
18 | const formattedEndDate = format(new Date(endDate), "MM/dd/yyyy");
19 | const range = `${formattedStartDate} - ${formattedEndDate}`;
Here is the search component:
```
import React from 'react'
import Header from '../components/Header'
import Footer from '../components/Footer'
import { useRouter } from 'next/dist/client/router'
import {format} from 'date-fns'
import InfoCard from '../components/InfoCard'
import searchResults from '../files/searchResults.JSON'
function Search() {
const router = useRouter();
//ES6 destructuring
const { location, startDate, endDate, noOfGuests} = router.query;
const formattedStartDate = format(new Date(startDate), "MM/dd/yy");
const formattedEndDate = format(new Date(endDate), "MM/dd/yyyy");
const range = `${formattedStartDate} - ${formattedEndDate}`;
return (
<div>
<Header
placeholder= {`${location} | ${formattedStartDate}| ${formattedEndDate} | ${noOfGuests} guests `}
/>
<main className='flex'>
<section className='flex-grow pt-14 px-6'>
<p className='text-xs '>500+ Stays - {range} - {noOfGuests} guests</p>
<h1 className='text-3xl font-semibold mb-6 mt-2'>Stays in {location}</h1>
<div className='hidden md:inline-flex mb-5 space-x-3 text-gray-800 whitespace-nowrap'>
<p className='button'>Cancellation Flexibilty</p>
<p className='button'>Type of Place</p>
<p className='button'>Price</p>
<p className='button'>Rooms and Beds</p>
<p className='button'>More Filters</p>
</div>
<div className='flex flex-col'>
{searchResults.map(({ key, img, description, location, star, price, total, title, long, lat }) => (
<InfoCard
key={key}
img= {img}
location = {location}
title={title}
description={description}
star={star}
price={price}
total={total}
long={long}
lat={lat}
/>
))}
</div>
</section>
</main>
<Footer />
</div>
)
}
export default Search;
```
And here is the Header component containing the daterangepicker
```
import React from 'react';
import Image from 'next/image';
import {SearchIcon, GlobeAltIcon, MenuIcon, UserCircleIcon, UserIcon} from "#heroicons/react/solid"
import {useState} from "react"
import 'react-date-range/dist/styles.css'; // main style file
import 'react-date-range/dist/theme/default.css'; // theme css file
import { DateRangePicker } from 'react-date-range';
import { useRouter } from 'next/dist/client/router';
function Header({placeholder}) {
const [searchInput, setSearchInput] = useState("");
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [noOfGuests, setNoOfGuests] = useState(1);
const router = useRouter();
const selectionRange = {
startDate: startDate,
endDate: endDate,
key: "selection"
};
const handleSelect = (ranges) => {
setStartDate(ranges.selection.startDate);
setEndDate(ranges.selection.endDate);
};
const resetInput = () => {
setSearchInput("");
};
const search = () => {
router.push({
pathname: '/search',
query: {
location: searchInput,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
noOfGuests,
}
});
}
return (
<header className='sticky top-0 z-50 grid grid-cols-3 bg-white my-0 shadow-md p-5 w-full md:px-10'>
<div onClick={() => router.push("/")} className="relative flex items-center h-10 cursor-pointer my-auto">
<Image src="https://links.papareact.com/qd3"
layout="fill"
objectFit="contain"
objectPosition="left"
alt=''
/>
</div>
<div className='flex item-center md:border-2 rounded-full py-2 md:shadow-sm'>
<input value={searchInput} onChange={(e) => setSearchInput(e.target.value)}
type="text" placeholder={placeholder} className='pl-5 bg-transparent outline-none flex-grow text-sm text-gray-600 placeholder-gray-400' />
<SearchIcon onClick={search} className='hidden md:inline-flex h-8 bg-red-400 text-white rounded-full p-2 cursor-pointer md:mx-2' />
</div>
<div className='flex items-center space-x-4 justify-end text-gray-400'>
<p className='hidden md:inline cursor-pointer'>Become a Host</p>
<GlobeAltIcon className='h-6 cursor-pointer' />
<div className='flex items-center space-x-2 border-2 p-2 rounded-full'>
<MenuIcon className='h-6 cursor-pointer'/>
<UserCircleIcon className='h-6 cursor-pointer' />
</div>
</div>
{searchInput && (
<div className='flex flex-col col-span-3 mx-auto'>
<DateRangePicker
ranges={[selectionRange]}
minDate={new Date()}
rangeColors={["#FD5B61"]}
onChange={handleSelect}
/>
<div className='flex items-center border-b mb-4'>
<h2 className='text-2xl flex-grow font-semibold'>Number of Guests</h2>
<UserIcon className='h-5' />
<input onChange={e => setNoOfGuests(e.target.value)} value={noOfGuests} type="number" min="1" className='w-12 pl-2 text-lg outline-none text-red-400' />
</div>
<div className='flex'>
<button className='flex-grow text-gray-500' onClick={search}>Search</button>
<button className='flex-grow text-red-400' onClick={resetInput}>Cancel</button>
</div>
</div>
)}
</header>
)
}
export default Header
```
I made a blog app with Nextjs + Sanity but I'm having an error on my [slug].tsx
I'm getting the error:
Type error: 'Post' refers to a value, but is being used as a type
here. Did you mean 'typeof Post'?
The file has the .tsx extension and Post interface is declared on a typings.d.ts file. I read on other solutions that I should make sure that .tsx is the extension instead of .ts but does that refer also to the typings file? Because I tried to change the .ts file into .tsx but it doesn't get solved.
If I change the Post to typeof Post error disappears but then a lot of warning appears when calling properties of Post.
[slug].tsx file
import { GetStaticProps } from 'next'
import React, { useState } from 'react'
import Header from '../../components/Header'
import { sanityClient, urlFor } from '../../sanity'
import PortableText from 'react-portable-text'
import { useForm, SubmitHandler } from 'react-hook-form'
interface IFormInput {
_id: string;
name: string;
email: string;
comment: string;
}
interface Props {
post: Post;
}
export default function Post({ post }: Props) {
const [submitted, setSubmitted] = useState(false);
console.log(post);
const {
register,
handleSubmit,
formState: { errors }
} = useForm<IFormInput>()
const onSubmit: SubmitHandler<IFormInput> = (data) => {
fetch('/api/createComment', {
method: 'POST',
body: JSON.stringify(data),
}).then(() => {
console.log(data);
setSubmitted(true);
}).catch((err) => {
console.log(err);
setSubmitted(false);
})
};
return (
<main>
<Header />
<img
className='w-full h-40 object-cover'
src={urlFor(post.mainImage).url()!}
alt="" />
<article className='max-w-3xl mx-auto p-5'>
<h1 className='text-3xl mt-10 mb-3 font-extrabold text-[#292929] '>{post.title}</h1>
<h2 className='text-xl font-light text-gray-500 mb-2'>{post.description}</h2>
<div className='flex items-center space-x-2'>
<img className='h-10 w-10 rounded-full' src={urlFor(post.author.image).url()!} alt="" />
<p className='font-extralight text-sm'>
Blog post by <span className='text-green-600'>{post.author.name}</span> - Published at {new Date(post._createdAt).toLocaleString()}
</p>
</div>
<div className='mt-10'>
<PortableText
className=''
dataset={process.env.NEXT_PUBLIC_SANITY_DATASET!}
projectId={process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!}
content={post.body}
serializers={{
h1: (props: any) => (
<h1 className='text-2xl font-bold my-5' {...props} />
),
h2: (props: any) => (
<h2 className='text-xl font-bold my-5' {...props} />
),
li: ({ children }: any) => (
<li className='ml-4 list-disc'>{children}</li>
),
link: ({ href, children }: any) => (
<a href={href} className='text-blue-500 hover:underline'>
{children}
</a>
),
}}
/>
</div>
</article>
<hr className='max-w-lg my-5 mx-auto border border-yellow-500' />
{submitted ? (
<div className='flex flex-col p-10 my-10 bg-yellow-500 text-white max-w-2xl mx-auto'>
<h3 className='text-3xl font-bold'>
Thank you for submitting your comment!
</h3>
<p>Once it has been approved, it will appear below!</p>
</div>
): (
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col p-5 max-w-2xl mx-auto mb-10' action="">
<h3 className='text-sm text-yellow-500'>Enjoyed this article?</h3>
<h4 className='text-3xl font-extrabold text-[#292929]'>Leave a comment below!</h4>
<hr className='py-3 mt-2' />
<input
{...register("_id")}
type="hidden"
name="_id"
value={post._id}
/>
<label className='block mb-5'>
<span className='text-gray-700'>Name</span>
<input {...register("name", {required: true})}
className='shadow border rounded py-2 px-3 form-input mt-1 block w-full ring-yellow-500 focus:ring-1 focus:outline-none' type="text" name="name" placeholder='John Appleseed' />
</label>
<label className='block mb-5'>
<span className='text-gray-700'>Email</span>
<input {...register("email", {required: true})}
className='shadow border rounded py-2 px-3 form-input mt-1 block w-full ring-yellow-500 focus:ring-1 focus:outline-none' placeholder='John Appleseed' type="text" />
</label>
<label className='block mb-5'>
<span className='text-gray-700'>Comment</span>
<textarea {...register("comment", {required: true})}
className='shadow border rounded py-2 px-3 mt-1 block w-full ring-yellow-500 focus:ring-1 focus:outline-none' placeholder='John Appleseed' rows={8} />
</label>
{/* errors will return when field validation fails */}
<div className='flex flex-col p-5'>
{errors.name && (<span className='text-red-500'>- The Name field is required</span>)}
{errors.comment && (<span className='text-red-500'>- The Comment field is required</span>)}
{errors.email && (<span className='text-red-500'>- The Email field is required</span>)}
</div>
<input className='shadow bg-yellow-500 hover:bg-yellow-400 transition-colors focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded cursor-pointer' type="submit" />
</form>
)}
{/* Comments */}
<div className='flex flex-col p-10 my-10 max-w-2xl mx-auto shadow-yellow-500 shadow space-y-2'>
<h3 className='text-4xl font-extrabold text-[#292929]'>Comments</h3>
<hr className='pb-2' />
{post.comments.map((comment) => (
<div key={comment._id}>
<p><span className='text-yellow-500'>{comment.name}: </span>{comment.comment}</p>
</div>
))}
</div>
</main>
);
}
export const getStaticPaths = async () => {
const query = `*[_type == "post"]{
_id,
slug {
current
}
}`;
const posts = await sanityClient.fetch(query);
const paths = posts.map((post: Post) => ({
params: {
slug: post.slug.current,
},
}));
return {
paths,
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps = async ({params}) => {
const query = `*[_type == "post" && slug.current == $slug][0]{
_id,
_createdAt,
title,
author-> {
name,
image
},
'comments': *[
_type == "comment" &&
post._ref == ^._id &&
approved == true],
description,
mainImage,
slug,
body
}`
const post = await sanityClient.fetch(query, {
slug: params?.slug,
});
if (!post) {
return {
notFound: true
}
}
return {
props: {
post,
},
revalidate: 60, // after 60s it will update the old cached version
}
}
typings.d.ts file
export interface Post {
_id: string;
_createdAt: string;
title: string;
author: {
name: string;
image: string;
};
comments: Comment[];
description: string;
mainImage: {
asset: {
url: string;
};
};
slug: {
current: string;
};
body: [object];
}
export interface Comment {
approved: boolean;
comment: string;
email: string;
name: string;
post: {
_ref: string;
_type: string;
};
_createdAt: string;
_id: string;
_rev: string;
_type: string;
_updatedAt: string;
}
You have interface Post and function Post(). I'm assuming you want the former in interface Props, but the compiler, and the JS engine, infers the latter.
Rename one of them, naming different things with the same name is rarely a good idea anyway.
I'm trying to create an image upload window where users can upload product images based for different product variations.
The intended output looks like:
I've to store the image file and url linked to the variationId and variationOptionId that the rendered component belongs to.
However, no matter which "Add Image" button i click, images get added only to the first component (blue, in the above example).
The imageVariations state looks like:
variationId: '',
variationName: '',
variationOptionId: '',
variationOptionName: '',
images: [
{
file: null,
url: '',
}
],
Code of Parent Component:
import React, { useEffect, useState } from 'react';
import ItemContainer from '../../common/ItemContainer';
import ProductImageUpload from '../imageStockPrice/ProductImageUpload';
const Tab3ProductImages = ({ product, loading, setLoading }) => {
const [imageVariations, setImageVariations] = useState([]);
// loop over product.productVariations and get the variation
// where variation.variesImage = true
useEffect(() => {
let tempVar = []; // has variationID, variationOptionID, and images array
product &&
product.productVariations.forEach((item) => {
if (item.variation.variesImage) {
tempVar.push({
variationId: item.variationId,
variationName: item.variation.name,
variationOptionId: item.variationOptionId,
variationOptionName: item.variationOption.name,
images: [],
});
}
});
setImageVariations(tempVar);
}, [product]);
// console.log('imageVariations', imageVariations);
const imageUploadHandler = () => {};
const imageDefaultHandler = () => {};
const imageDeleteHandler = () => {};
return (
<div className="py-4">
<div className="space-y-4">
{imageVariations &&
imageVariations.map((imageVar, index) => (
<div key={index}>
<ItemContainer
title={`${imageVar.variationName}: ${imageVar.variationOptionName}`}
>
<ProductImageUpload
loading={loading}
index={index}
imageVar={imageVar}
setImageVariations={setImageVariations}
imageUploadHandler={imageUploadHandler}
/>
</ItemContainer>
</div>
))}
</div>
</div>
);
};
export default Tab3ProductImages;
Code of the Child component - ProductImageUpload:
import React from 'react';
import { RiImageAddFill } from 'react-icons/ri';
import { MdOutlineCancel } from 'react-icons/md';
import LoadingButton from '../../formComponents/LoadingButton';
const ProductImageUpload = ({
loading,
index,
imageVar,
setImageVariations,
imageUploadHandler,
}) => {
// Handling Images
const handleImageChange = (e) => {
const tempArr = [];
[...e.target.files].forEach((file) => {
tempArr.push({
file: file,
url: URL.createObjectURL(file),
});
});
setImageVariations((prevState) => {
const newState = [...prevState];
newState[index].images = [...newState[index].images, ...tempArr];
return newState;
});
};
const removeImageFromList = (e) => {
setImageVariations((prevState) => {
const newState = [...prevState];
newState[index].images = newState[index].images.filter((item) => {
return item.url !== e.target.src;
});
return newState;
});
};
return (
<div className="space-y-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="my-auto">
<form onSubmit={imageUploadHandler}>
<div className="flex gap-4">
<label
htmlFor="categoryImages"
className="block w-full py-1 px-2 rounded cursor-pointer border
border-violet-700 bg-violet-50 hover:bg-violet-100 text-violet-700"
>
<span className="flex items-center gap-2">
<RiImageAddFill />
<span className="text-sm">Click to Add Images</span>
<span className="font-barlow text-sm">(max 10 images)</span>
</span>
<input
type="file"
id="categoryImages"
accept="image/*"
multiple
onChange={handleImageChange}
className="sr-only"
/>
</label>
{loading ? (
<LoadingButton />
) : (
<button className="text-sm py-1 px-4 rounded cursor-pointer border
border-blue-700 bg-blue-50 hover:bg-blue-100 text-blue-700">
Upload
</button>
)}
</div>
</form>
</div>
{/* upload progress bar */}
<div className="flex items-center">
<div className="bg-gray-200 w-full h-4 rounded-full overflow-hidden">
<div
className="h-4 bg-violet-500 text-xs font-medium text-center p-0.5
leading-none rounded-full transition-all duration-75"
style={{ width: `${progress}%` }}
>
<span
className={`ml-2 ${
progress === 0 ? 'text-gray-600' : 'text-blue-100'
}`}
>
{progress}%
</span>
</div>
</div>
</div>
{/* upload progress bar ends */}
{/* image preview section */}
<div>
<ul className="flex flex-wrap gap-2">
{imageVar &&
imageVar.images.length > 0 &&
imageVar.images.map((item, index) => (
<li key={index} className="relative">
<img
src={item.url}
alt="preview"
className="w-20 h-20 object-cover rounded shadow-lg border
hover:scale-110 transition duration-200"
/>
<button
onClick={removeImageFromList}
className="absolute -top-2 -right-2"
>
<MdOutlineCancel className="text-red-400 bg-white" />
</button>
</li>
))}
</ul>
</div>
{/* image preview section ends */}
</div>
);
};
export default ProductImageUpload;
I have the following components
import React from 'react';
import PropTypes from 'prop-types';
export default function FormField({
type,
name,
label,
register,
required,
placeholder,
validationSchema
}) {
return (
<>
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900">
{label}
<span className="text-red-500 font-bold text-lg">{required && '*'}</span>
</label>
<div className="mt-1">
<input
{...register(name, validationSchema)}
type={type}
name={name}
id={name}
className={['field', `field--${type}`].join(' ')}
placeholder={placeholder}
/>
</div>
</div>
</>
);
}
FormField.propTypes = {
type: PropTypes.oneOf(['text', 'email', 'password', 'file', 'checkbox']),
register: PropTypes.func,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
placeholder: PropTypes.string
};
FormField.defaultProps = {
type: 'text',
name: 'text',
label: 'Label',
placeholder: ''
};
and the following stories file
import FormField from './FormField';
export default {
title: 'Forms/FormField',
component: FormField,
argTypes: {
type: {
options: ['text', 'email', 'password', 'file', 'checkbox'],
control: {
type: 'select'
}
}
}
};
const Template = (args) => <FormField {...args} />;
export const Text = Template.bind({});
Text.args = {
type: 'text',
label: 'Text Field',
name: 'text-field',
errorMsg: 'Text field is required',
placeholder: 'Text goes here'
};
However, the message I am getting is the following:
TypeError: register is not a function
How do I pass register into stories even if I'm not using it for my stories?
I've tried passing in and using FormProvider and wrapping the template but that didn't seem to work unless I'm missing something 🤔
...
edit 3
After chats with Joris I now have the following component
import React from 'react';
import PropTypes from 'prop-types';
const FormField = React.forwardRef(
({ type, name, label, required, placeholder, ...props }, ref) => {
return (
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900">
{label}
<span className="text-red-500 font-bold text-lg">{required && '*'}</span>
</label>
<div className="mt-1">
<input
{...props}
name={name}
ref={ref}
type={type}
id={name}
className={['field', `field--${type}`].join(' ')}
placeholder={placeholder}
/>
</div>
</div>
);
}
);
export default FormField;
FormField.propTypes = {
type: PropTypes.oneOf(['text', 'email', 'password', 'file', 'checkbox']),
register: PropTypes.func,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
placeholder: PropTypes.string
};
FormField.defaultProps = {
type: 'text',
name: 'text',
label: 'Label',
placeholder: ''
};
and now the following page
import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
import Background from '../../../public/images/option1.png';
import Router from 'next/router';
import { signIn } from 'next-auth/client';
import { useForm, FormProvider } from 'react-hook-form';
// components
import ErrorsPopup from '../../components/ErrorsPopup';
import FormField from '../../components/Forms/FormField';
import Button from '../../components/Button';
export default function Login() {
const methods = useForm();
const { handleSubmit, register } = methods;
const onSubmit = async (data) => {
await signIn('credentials', {
redirect: false,
data
});
Router.push('/dashboard');
};
return (
<>
<Head>
<title>Ellis Development - Login</title>
</Head>
<div className="relative">
<div className="md:flex">
{/* Image */}
<div className="flex items-center justify-center bg-blue-700 h-screen lg:w-96">
<Image src={Background} width={350} height={350} layout="fixed" />
</div>
{/* Contact form */}
<div className="flex flex-col justify-center px-6 sm:px-10 w-full">
<h1 className="text-4xl font-extrabold text-grey-800">Login</h1>
{/* errors */}
<FormProvider {...methods}>
<ErrorsPopup />
</FormProvider>
<form
onSubmit={handleSubmit(onSubmit)}
className="mt-6 flex flex-col gap-y-6 sm:gap-x-8">
{/* email field */}
<FormField
{...register('email', {
required: 'Email is required',
pattern: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i
})}
type="email"
label="Email"
placeholder="john.doe#ebrookes.dev"
required
/>
{/* password field */}
<FormField
{...register('password', {
required: 'Password is required'
})}
type="password"
label="Password"
placeholder="*******"
required
/>
<div className="flex items-center justify-between sm:col-span-2">
<div>
<Button type="submit" label="Login" icon="SaveIcon" />
</div>
<div>
<Link href="/dashboard/auth/register">
<a className="underline decoration-blue-500 decoration-4 hover:decoration-2 mr-4">
Register
</a>
</Link>
<Link href="/dashboard/auth/forgot">
<a className="underline decoration-blue-500 decoration-4 hover:decoration-2">
Forgot your password?
</a>
</Link>
</div>
</div>
</form>
</div>
</div>
</div>
</>
);
}
The issue here now is that the form is still not submitting.
To get this working I now have the following component
import React from 'react';
import PropTypes from 'prop-types';
const FormField = React.forwardRef(
({ type, name, label, required, placeholder, ...props }, ref) => {
return (
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900">
{label}
<span className="text-red-500 font-bold text-lg">{required && '*'}</span>
</label>
<div className="mt-1">
<input
{...props}
name={name}
ref={ref}
type={type}
id={name}
className={['field', `field--${type}`].join(' ')}
placeholder={placeholder}
/>
</div>
</div>
);
}
);
export default FormField;
FormField.propTypes = {
type: PropTypes.oneOf(['text', 'email', 'password', 'file', 'checkbox']),
register: PropTypes.func,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
placeholder: PropTypes.string
};
FormField.defaultProps = {
type: 'text',
name: 'text',
label: 'Label',
placeholder: ''
};
stories
import { useForm } from 'react-hook-form';
import FormField from './FormField';
export default {
title: 'Forms/FormField',
component: FormField,
argTypes: {
type: {
options: ['text', 'email', 'password', 'file', 'checkbox'],
control: {
type: 'select'
}
}
}
};
const Template = (args) => {
const { register } = useForm();
return <FormField {...args} register={register} />;
};
export const Text = Template.bind({});
Text.args = {
type: 'text',
label: 'Text Field',
name: 'text-field',
errorMsg: 'Text field is required',
placeholder: 'Text goes here'
};
export const Email = Template.bind({});
Email.args = {
type: 'email',
name: 'email',
label: 'Email',
placeholder: 'john.doe#email.com'
};
export const Password = Template.bind({});
Password.args = {
type: 'password',
name: 'password',
label: 'Password',
placeholder: '********'
};
export const File = Template.bind({});
File.args = {
type: 'file',
name: 'file-upload',
label: 'File upload'
};
export const Checkboxes = Template.bind({});
Checkboxes.args = {
type: 'checkbox',
name: 'check-1',
label: 'Checkboxes'
};
and page
import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
import Background from '../../../public/images/option1.png';
import Router from 'next/router';
import { signIn } from 'next-auth/client';
import { useForm, FormProvider } from 'react-hook-form';
// components
import ErrorsPopup from '../../components/ErrorsPopup';
import FormField from '../../components/Forms/FormField';
import Button from '../../components/Button';
export default function Login() {
const methods = useForm();
const { handleSubmit, register } = methods;
const onSubmit = async (data) => {
await signIn('credentials', {
redirect: false,
email: data.email,
password: data.password
});
Router.push('/dashboard');
};
return (
<>
<Head>
<title>Ellis Development - Login</title>
</Head>
<div className="relative">
<div className="md:flex">
{/* Image */}
<div className="flex items-center justify-center bg-blue-700 h-screen lg:w-96">
<Image src={Background} width={350} height={350} layout="fixed" />
</div>
{/* Contact form */}
<div className="flex flex-col justify-center px-6 sm:px-10 w-full">
<h1 className="text-4xl font-extrabold text-grey-800">Login</h1>
{/* errors */}
<FormProvider {...methods}>
<ErrorsPopup />
</FormProvider>
<form
onSubmit={handleSubmit(onSubmit)}
className="mt-6 flex flex-col gap-y-6 sm:gap-x-8">
{/* email field */}
<FormField
{...register('email', {
required: 'Email is required',
pattern: /^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i
})}
type="email"
name="email"
label="Email"
placeholder="john.doe#ebrookes.dev"
required
/>
{/* password field */}
<FormField
{...register('password', {
required: 'Password is required'
})}
type="password"
name="password"
label="Password"
placeholder="*******"
required
/>
<div className="flex items-center justify-between sm:col-span-2">
<div>
<Button type="submit" label="Login" icon="SaveIcon" />
</div>
<div>
<Link href="/dashboard/auth/register">
<a className="underline decoration-blue-500 decoration-4 hover:decoration-2 mr-4">
Register
</a>
</Link>
<Link href="/dashboard/auth/forgot">
<a className="underline decoration-blue-500 decoration-4 hover:decoration-2">
Forgot your password?
</a>
</Link>
</div>
</div>
</form>
</div>
</div>
</div>
</>
);
}
Here is a way to achieve your needs.
const Template = (args) => {
const { register } = useForm();
return (
<form>
<FormField {...args} register={register} />
</form>
);
}
At react-hook-form, we recommend to forward ref instead of passing down the register method.
const FormField = React.forwardRef(({
type,
label,
required,
placeholder,
validationSchema,
...props
}, ref) => {
return (
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900">
{label}
<span className="text-red-500 font-bold text-lg">{required && '*'}</span>
</label>
<div className="mt-1">
<input
{...props}
ref={ref}
type={type}
id={name}
name={name}
className={['field', `field--${type}`].join(' ')}
placeholder={placeholder}
/>
</div>
</div>
);
});
export default FormField;
And use it a the following:
function MyForm() {
const { register } = useForm();
return (
<form>
<FormField {...register('field-name') />
</form>
)
}
Edited: full working example
https://codesandbox.io/s/hopeful-mahavira-pfkqs
import React from "react";
import { useForm } from "react-hook-form";
interface Props extends React.PropsWithRef<JSX.IntrinsicElements["input"]> {
label: string;
}
const FormField = React.forwardRef<HTMLInputElement, Props>(
({ label, ...props }, ref) => {
return (
<div>
<label
htmlFor={props.name}
className="block text-sm font-medium text-gray-900"
>
{label}
<span className="text-red-500 font-bold text-lg">
{props.required && "*"}
</span>
</label>
<div className="mt-1">
<input {...props} ref={ref} id={props.name} />
</div>
</div>
);
}
);
FormField.displayName = "FormField";
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
console.log(errors);
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<FormField
required
{...register("myField", { required: "Field is required" })}
label="Field example"
/>
<pre>{errors.myField?.message}</pre>
<button type="submit">Submit</button>
</form>
);
}