Form with useState

With useState all logic is implemented inside the component.

We start by storing Context inside useState:

import { useState } from "react";
import { initialContext, type Context } from "./shared";

export default function UseState() {
  const [context, setContext] = useState<Context>(initialContext);
  return (<></>);
}

Updating the context requires to add a function inside the component:

import { useState } from "react";
import { initialContext, type Context } from "./shared";

export default function UseState() {
  const [context, setContext] = useState<Context>(initialContext);

  const onUpdateUsername = (value: string) => {
    setContext({ username: value });
  };

  return (<></>);
}

With this we can already define the initial UI code:

import { useState } from "react";
import { initialContext, type Context } from "./shared";

export default function UseState() {
  const [context, setContext] = useState<Context>(initialContext);

  const onUpdateUsername = (value: string) => {
    setContext({ username: value });
  };

  return (
    <form>
      <input
        type="text"
        value={context.username}
        onChange={(e) => onUpdateUsername(e.target.value)}
      />
      <button>Confirm</button>
    </form>
  );
}

Function to send async request

Making an async request with useState requires another function. Since we also want to avoid making multiple requests, we introduce a new loading state:

import { useState } from "react";
import { initialContext, type Context } from "./shared";

export default function UseState() {
  const [context, setContext] = useState<Context>(initialContext);
  const [loading, setLoading] = useState(false);

  const onUpdateUsername = (value: string) => {
    setContext({ username: value });
  };

  return (
    <form>
      <input
        type="text"
        value={context.username}
        onChange={(e) => onUpdateUsername(e.target.value)}
      />
      <button disabled={loading}>
        Confirm
      </button>
    </form>
  );
}

Inside onSubmit of <form> we send the request passing the context as argument.

We also need to update the loading state to true while the request is in progress, as well as prevent the form from being submitted when loading:

import { useState } from "react";
import { initialContext, postRequest, type Context } from "./shared";

export default function UseState() {
  const [context, setContext] = useState<Context>(initialContext);
  const [loading, setLoading] = useState(false);

  const onUpdateUsername = (value: string) => {
    setContext({ username: value });
  };

  const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (!loading) { // 👈 Prevent form from being submitted while request is in progress
      setLoading(true);
      await postRequest(context);
      setLoading(false);
    }
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        type="text"
        value={context.username}
        onChange={(e) => onUpdateUsername(e.target.value)}
      />
      <button disabled={loading}>
        Confirm
      </button>
    </form>
  );
}

This is the basic implementation of a form with useState.

This is intentionally not a complete implementation since it's missing things like error handling, validation, and more.

Again, for these initial lessons we are trying to keep the implementation as simple but complete as possible.

We start to see the first problems of using useState:

  • We need to add a new useState hook to handle the current state (loading, error, etc.)
  • All the logic is implemented as functions inside the component
  • Hard to test since the logic is coupled to the component
  • Mixing UI and logic makes implementing styles and layout more confusing

The component becomes even more complex if we add more states and form fields.