This is great. And for a first project with React, it all looks idiomatic to me. I certainly agree that form validation can be a bit of a rabbit hole. Using React Hook Form here was the sensible option.
For matching off against individual parts of the password validation, I think the way to do it is to break up the regex. Have one regex for each criterion, and use that inside a custom validator function. This is with the caveat that I have yet to try it myself.
Another alternative would be to use a schema library like Zod. That way you can define all your rules, and the failure messages associated with each, in the schema. Then pass the schema to RHF in one place rather than wiring up all the field validators separately. If you were building an app with a lot of forms, I would definitely recommend this. Though for a single form the extra bundle-size might not be worth it.
A couple of other things I noticed while digging through the code:
Composition
In general all the components are nicely composable - accepting children
. In the page intro component though there's a mix of a heading
text prop, plus children
. My personal preference is to avoid those kinds of text props in React. Mainly because it becomes more difficult to pass markup into them. If, for instance, you wanted to wrap a specific word in the heading in a span, you now have to pass in a fragment which feels awkward. I would probably export a PageIntroHeading
from the same file and use that to wrap up the h1
styles and let the consumer pass it in
<PageIntro>
<PageIntroHeading>Learn to code...</PageIntroHeading>
<p>See how other dev...</p>
</PageIntro>
Tailwind class strings
In the form component, you've extracted the input styling into its own string to be reused in the various fields. I would try to avoid that. Unless you explicitly add config to target named variables, I believe the tailwind compiler will struggle to properly purge unused class names, as it will just look through the jsx for the className
prop. That said, it looks ok in the deployed site, so Tailwind may have made the compiler more intelligent at spotting classes wherever they are used.
I personally would prefer to extract a styled <Input/>
component that packages up the class names instead. Or you could use @apply
and create a component class for it. At work we tend to do the former so there is only ever one way that a component can be styled, but both are valid options.
Visually hidden
One more small thing. You might want to accept an additional as
prop in the VisuallyHidden component. That way you can pass in either a div or a span, which would mean this can be used in places where one or the other of those would be invalid HTML. E.g. if you needed to wrap a <p>
function VisuallyHidden({as = 'span', children}) {
const Element = as
return <Element className="sr-only">{children}</Element>
}
In general, I try to avoid as
props. They can make the components much more complicated. Especially if you try and do tricks like make your buttons work as links etc. But I think here is a good use case for them.