Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SolidJS]: How to handle errors from server? #189

Open
CallumVass opened this issue Mar 5, 2024 · 3 comments
Open

[SolidJS]: How to handle errors from server? #189

CallumVass opened this issue Mar 5, 2024 · 3 comments
Assignees
Labels
question Further information is requested

Comments

@CallumVass
Copy link

Hi,

I'm wondering how I can handle server errors when I submit my form? Ie think a login form but the email has already been taken etc..

Schema/Type code:

const LoginSchema = object({
  email: string([email("Email is invalid"), minLength(1, "Email is required")]),
  password: string([minLength(1, "Password is required")]),
});

type LoginForm = Input<typeof LoginSchema>;

Component code:

export default function Index() {
  const [loginForm, { Form, Field }] = createForm<LoginForm>({
    validate: valiForm(LoginSchema),
  });
  const act = useAction(login);
  const handleSubmit: SubmitHandler<LoginForm> = async (values) => {
    await act(values);
  };
  return (
    <>
      <h1>Login</h1>
      <Form onSubmit={handleSubmit}>
        <Show when={loginForm.response.message}>
          {(message) => <div>{message()}</div>}
        </Show>
        <Field name="email">
          {(field, props) => (
            <>
              <input {...props} type="email" required />
              {field.error && <div>{field.error}</div>}
            </>
          )}
        </Field>
        <Field name="password">
          {(field, props) => (
            <>
              <input {...props} type="password" required />
              {field.error && <div>{field.error}</div>}
            </>
          )}
        </Field>
        <button type="submit">Login</button>
      </Form>

      <a href="/register">Create an account</a>
    </>
  );
}

Action code:

const login = action(async (values: LoginForm) => {
  "use server";

  const result = safeParse(LoginSchema, values);
  if (!result.success) {
    const flattened = flatten<typeof LoginSchema>(result.issues);
    const errors = Object.entries(flattened.nested).reduce(
      (acc, [key, value]) => {
        acc[key] = value[0];
        return acc;
      },
      {} as { [key: string]: string },
    );

    if (flattened.root && flattened.root.length > 0) {
      throw new FormError<LoginForm>(flattened.root[0], errors);
    }

    throw new FormError<LoginForm>(errors);
  }

  throw new FormError<LoginForm>("Email exists in database");
});

I would have thought I would see a message here, but I only see 500 internal server errors in my network tab:

        <Show when={loginForm.response.message}>
          {(message) => <div>{message()}</div>}
        </Show>
@CallumVass
Copy link
Author

Okay I think I've figured it out but it seems a bit quirky, please let me know if there is a better way?

Action code:

const login = action(async (values: LoginForm) => {
  "use server";

  const result = safeParse(LoginSchema, values);
  if (!result.success) {
    const flattened = flatten<typeof LoginSchema>(result.issues);
    const errors = Object.entries(flattened.nested).reduce(
      (acc, [key, value]) => {
        acc[key] = value[0];
        return acc;
      },
      {} as { [key: string]: string },
    );

    if (flattened.root && flattened.root.length > 0) {
      return new FormError<LoginForm>(flattened.root[0], errors);
    }

    return new FormError<LoginForm>(errors);
  }

  return new FormError<LoginForm>("Email exists in database");
});

Submit code:

  const [loginForm, { Form, Field }] = createForm<LoginForm>({});
  const act = useAction(login);
  const sub = useSubmission(login);
  const handleSubmit: SubmitHandler<LoginForm> = async (values) => {
    await act(values);
    if (sub.result) {
      Object.entries(sub.result.errors).forEach(([field, message]) => {
        setError(loginForm, field as keyof LoginForm, message);
      });
      throw new FormError<LoginForm>(sub.result.message);
    }
  };

@fabian-hiller fabian-hiller self-assigned this Mar 6, 2024
@fabian-hiller
Copy link
Owner

Hey, I haven't had time to take a closer look at the new SolidStart beta release. I plan to integrate it in the future with a nice API and DX, but that will take some time.

@fabian-hiller fabian-hiller added the question Further information is requested label Mar 6, 2024
@Ashyni
Copy link

Ashyni commented Mar 8, 2024

Hello, I end up with something like that to reuse on different form/action.

//@types/action.d.ts
import { FieldValues, FormErrors } from "@modular-forms/solid";
import { Action } from "@solidjs/router";

export type ActionForm<Schema extends FieldValues> = Action<
  [data: Schema],
  { success?: boolean; error?: string; fields?: FormErrors<Schema> } | void
>;
// autocomplete on fields from TS
//lib/utils.ts
export function flatIssues<K extends string | number>(
  issues: SchemaIssues,
): { [key in K]?: string } {
  let errors: { [key in K]?: string } = issues ? {} : {};

  return issues.reduce((acc, issue) => {
    if (
      issue.path?.every(
        ({ key }) => typeof key === "string" || typeof key === "number",
      )
    ) {
      const key = issue.path!.map(({ key }) => key).join(".") as K;
      if (acc[key]) {
        acc[key] = acc[key] + "\n" + issue.message; // merge multiple issues, split in components
      } else acc[key] = issue.message;
    }
    return acc;
  }, errors);
}
//login.tsx
export default function Index() {
  const action = useAction(login);
  const submission = useSubmission(login);
  const [loginForm, { Form, Field }] = createForm<LoginForm>({
    validate: valiForm(LoginSchema),
  });
  const handleSubmit: SubmitHandler<LoginForm> = async (values) => {
    await action(values);
    if (submission.result) {
      const { error = "", fields = {} } = submission.result;
      throw new FormError<LoginForm>(error, fields);
    }
  };
  return ( ... );
};

const LoginSchema = v.object({ ... })
type LoginForm = v.Input<typeof LoginSchema>;

const login: ActionForm<LoginForm> = action(async (data) => {
  "use server";
  const result = v.safeParse(LoginSchema, data);
  if (!result.success) {
    const errors = flatIssues<keyof LoginForm>(result.issues);
    return { fields: errors };
  }

  const { username, password } = result.output;

  // db call
  if (failed) return { error: "user does not exist" };

  // all good
  throw redirect("/");
}, "login");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants