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.