Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lauslim12/61618bdb1c10b8f46bedea475c343f1e to your computer and use it in GitHub Desktop.
Save lauslim12/61618bdb1c10b8f46bedea475c343f1e to your computer and use it in GitHub Desktop.
Dynamic form with Chakra UI, React Hook Form, and TypeScript.
// Codesandbox: https://codesandbox.io/s/silly-wu-0fz2t1
import React, { useState, useRef } from "react";
import ReactDOM from "react-dom/client";
import { ChakraProvider } from "@chakra-ui/react";
import {
Controller,
useForm,
useFormState,
useFieldArray
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Button,
Flex,
HStack,
Heading,
Text,
Input,
FormControl,
FormLabel,
VStack,
FormErrorMessage,
FormHelperText,
Textarea
} from "@chakra-ui/react";
import { z } from "zod";
import type { Control } from "react-hook-form";
/**
* Schema
*/
const identitySchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1)
});
const identityFormSchema = z.object({
identities: z.array(identitySchema)
});
type IdentityFormSchema = z.infer<typeof identityFormSchema>;
/**
* Isolate submission to prevent re-renders
*/
interface SubmitButtonProps {
control: Control<IdentityFormSchema>;
}
function SubmitButton({ control }: SubmitButtonProps) {
const { isValid, isSubmitting } = useFormState({ control });
return (
<Button
size="sm"
colorScheme="green"
type="submit"
isDisabled={!isValid || isSubmitting}
>
Submit
</Button>
);
}
/**
* App
*/
function App() {
// Calculate re-render, first time we have to start from
// -1 because initially it will only render one time.
const renderCount = useRef(-1);
renderCount.current = renderCount.current + 1;
const [submittedData, setSubmittedData] = useState("");
const { control, handleSubmit } = useForm<IdentityFormSchema>({
resolver: zodResolver(identityFormSchema),
mode: "all",
defaultValues: {
identities: [
{ firstName: "Naruto", lastName: "Uzumaki" },
{ firstName: "Sasuke", lastName: "Uchiha" },
{ firstName: "Boruto", lastName: "Uzumaki" },
{ firstName: "Kawaki", lastName: "Uzumaki" }
]
}
});
const { append, fields, remove } = useFieldArray({
control,
name: "identities"
});
const submitChanges = handleSubmit((changes) => {
setSubmittedData(JSON.stringify(changes, null, 4));
});
return (
<Flex as="main" p={2} direction="column" textAlign="center">
<Heading>
Hello CodeSandbox{" "}
<span role="img" aria-label="Zap icon" aria-labelledby="zap icon">
</span>
</Heading>
<Text>Field Array Dynamic Form</Text>
<Text>
Rerender Count (Strict Mode, so twice, starting from 0):{" "}
{renderCount.current}
</Text>
<form onSubmit={submitChanges}>
{fields.map((item, index) => (
<VStack key={item.id} px={4} py={2} spacing={4}>
<HStack key={item.id} py={2} width="full">
<FormControl isRequired>
<FormLabel htmlFor={`${index}.firstName`}>First Name</FormLabel>
<Controller
control={control}
name={`identities.${index}.firstName`}
defaultValue=""
render={({ fieldState, field }) => (
<FormControl
isInvalid={fieldState.invalid}
textAlign="left"
>
<Input id={`${index}.firstName`} type="text" {...field} />
{fieldState.error ? (
<FormErrorMessage>
{fieldState.error.message}
</FormErrorMessage>
) : (
<FormHelperText>Input first name.</FormHelperText>
)}
</FormControl>
)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel htmlFor={`${index}.lastName`}>Last Name</FormLabel>
<Controller
control={control}
name={`identities.${index}.lastName`}
defaultValue=""
render={({ fieldState, field }) => (
<FormControl
isInvalid={fieldState.invalid}
textAlign="left"
>
<Input id={`${index}.lastName`} type="text" {...field} />
{fieldState.error ? (
<FormErrorMessage>
{fieldState.error.message}
</FormErrorMessage>
) : (
<FormHelperText>Input last name.</FormHelperText>
)}
</FormControl>
)}
/>
</FormControl>
</HStack>
<Button
colorScheme="red"
size="sm"
onClick={() => remove(index)}
width="full"
>
Remove
</Button>
</VStack>
))}
<HStack as="section" justify="center" mt={4}>
<Button
colorScheme="yellow"
size="sm"
onClick={() =>
append({ firstName: "", lastName: "" }, { shouldFocus: false })
}
>
Add New Identity
</Button>
<SubmitButton control={control} />
</HStack>
</form>
{submittedData && (
<Textarea height="500px" mt={4} value={submittedData} isReadOnly />
)}
</Flex>
);
}
/**
* React 18 render
*/
const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<ChakraProvider>
<App />
</ChakraProvider>
</React.StrictMode>
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment