Custom methods inside FormData service

A common practice is to add custom methods to services to simplify common tasks.

For example, we can add a get static method that uses Effect.fromNullable to make sure that a form value is present:

import { Context, Effect } from "effect";

export class FormData extends Context.Tag("FormData")<
  FormData,
  globalThis.FormData
>() {
  static readonly get = (name: string) =>
    this.pipe(
      Effect.flatMap(
        (formData) => Effect.fromNullable(
          // 👇 `formData` is the actual `FormData` web API inside the service
          formData.get(name)?.toString()
        )
      )
    );
}

get returns Effect<string, NoSuchElementException, FormData>:

  • string: the value of the form with the given name if present
  • NoSuchElementException: error when the form doesn't contain the name value
  • FormData: dependency on the FormData service

We can now use FormData.get inside Form:

export default function Form() {
  const [_, action] = useActionState(
    () =>
      RuntimeClient.runPromise(
        Effect.gen(function* () {
          const username = yield* FormData.get("username");
        })
      ),
    null
  );
  return (
    <form action={action}>
      <input type="text" name="username" />
      <button>Submit</button>
    </form>
  );
}

You could then send username (or any other form value) to an API endpoint (which may be implemented using HttpClient and composed as a service).

In this example we simply log the value to the console (using the Console service provided by effect):

export default function Form() {
  const [_, action] = useActionState(
    () =>
      RuntimeClient.runPromise(
        Effect.gen(function* () {
          const username = yield* FormData.get("username");
          yield* Console.log(username);
        })
      ),
    null
  );
  return (
    <form action={action}>
      <input type="text" name="username" />
      <button>Submit</button>
    </form>
  );
}