Created
August 25, 2024 22:38
-
-
Save AlexGeb/a159f045b97895780da25e445d1acf4d to your computer and use it in GitHub Desktop.
React 19 form example, with custom hook and effect schema validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ArrayFormatter, Schema } from '@effect/schema'; | |
| import { Effect, Either, flow, Record } from 'effect'; | |
| import { Suspense, use, useActionState } from 'react'; | |
| const updateName = async (name: string) => { | |
| await Effect.runPromise(Effect.sleep(2000)); | |
| return name; | |
| }; | |
| type State<T> = | |
| | { | |
| data: T; | |
| error?: undefined; | |
| } | |
| | { | |
| data?: undefined; | |
| error: Record<string, string>; | |
| }; | |
| const parseErrorToObject = flow( | |
| ArrayFormatter.formatError, | |
| Effect.map(issues => { | |
| const errors = issues.reduce( | |
| (acc, current) => { | |
| const key = current.path.join('.'); | |
| acc[key] = current.message; | |
| return acc; | |
| }, | |
| {} as Record<string, string>, | |
| ); | |
| return errors; | |
| }), | |
| Effect.runSync, | |
| ); | |
| function useFormState<FormModel, Data>( | |
| schema: Schema.Schema<FormModel>, | |
| onSubmit: (props: FormModel) => Promise<Data>, | |
| initialState: State<Data> | null = null, | |
| ) { | |
| const [state, submitAction, isPending] = useActionState< | |
| State<Data> | null, | |
| FormData | |
| >(async (previousState, formData) => { | |
| try { | |
| const args = Schema.decodeUnknownEither(schema)( | |
| Record.fromEntries(formData.entries()), | |
| ); | |
| if (Either.isLeft(args)) { | |
| return { | |
| error: { | |
| ...parseErrorToObject(args.left), | |
| root: 'Invalid form data', | |
| }, | |
| }; | |
| } | |
| const data = await onSubmit(args.right); | |
| return { previousState, data }; | |
| } catch (error) { | |
| return { | |
| error: { | |
| root: 'Something unexpected happened, please try again later', | |
| }, | |
| }; | |
| } | |
| }, initialState); | |
| return { state, submitAction, isPending }; | |
| } | |
| const FormReact19Child = ({ | |
| initialNamePromise, | |
| }: { | |
| initialNamePromise: Promise<string>; | |
| }) => { | |
| const initialName = use(initialNamePromise); | |
| const { state, submitAction, isPending } = useFormState( | |
| Schema.Struct({ | |
| name: Schema.NonEmptyString, | |
| }), | |
| async ({ name }) => { | |
| await updateName(name); | |
| return { name }; | |
| }, | |
| { data: { name: initialName } }, | |
| ); | |
| return ( | |
| <form action={submitAction}> | |
| <input type="text" name="name" placeholder="Name" /> | |
| {state?.error?.name && <div>{state.error.name}</div>} | |
| <button type="submit">submit</button> | |
| {state?.error?.root && <div>{state.error.root}</div>} | |
| <div>{isPending ? 'Loading...' : state?.data?.name}</div> | |
| </form> | |
| ); | |
| }; | |
| const initialNamePromise = updateName('initialName'); | |
| export const FormReact19 = () => { | |
| return ( | |
| <Suspense fallback={<div>Loading...</div>}> | |
| <FormReact19Child initialNamePromise={initialNamePromise} /> | |
| </Suspense> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment