Skip to content

Instantly share code, notes, and snippets.

@AlexGeb
Created August 25, 2024 22:38
Show Gist options
  • Select an option

  • Save AlexGeb/a159f045b97895780da25e445d1acf4d to your computer and use it in GitHub Desktop.

Select an option

Save AlexGeb/a159f045b97895780da25e445d1acf4d to your computer and use it in GitHub Desktop.
React 19 form example, with custom hook and effect schema validation
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