Create a Custom React Hook to Handle Form Fields

Now before we move on to creating our sign up page, we are going to take a short detour to simplify how we handle form fields in React. We built a form as a part of our login page and we are going to do the same for our sign up page. You'll recall that in our login component we were creating two state variables to store the username and password.

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

And we also use something like this to set the state:

onChange={e => setEmail(e.target.value)}

Now we are going to do something similar for our sign up page and it'll have a few more fields than the login page. So it makes sense to simplify this process and have some common logic that all our form related components can share. Plus this is a good way to introduce the biggest benefit of React Hooks — reusing stateful logic between components.

Creating a Custom React Hook

Let's pull the above logic into a custom React Hook.

Create a `src/libs/` directory to store all our common code.
$ mkdir src/libs/
Add the following to `src/libs/hooksLib.ts`.
import { useState } from "react";
import { FormControl } from "react-bootstrap";
interface InitialState {
[key: string]: string
}
export const useFormFields: (initialState: InitialState) => [InitialState, (event: React.FormEvent<FormControl>) => void] = (initialState) => {
const [fields, setValues] = useState(initialState);
return [
fields,
(event: React.FormEvent<FormControl>) => {
const castEvent = event as unknown as React.ChangeEvent<HTMLInputElement>;
setValues({
...fields,
[castEvent.target.id]: castEvent.target.value
});
}
];
}

Creating a custom hook is amazingly simple. Let's go over how this works:

  1. A custom React Hook starts with the word use in its name. So ours is called useFormFields.

  2. Our Hook takes the initial state of our form fields as an object and saves it as a state variable called fields.

  3. It returns an array with fields and a function that sets the new state based on the event object. The only difference here is that we are using event.target.id (which contains the id of our form field) to store the value (event.target.value).

And that's it! We can now use this in our Login component.

Using Our Custom Hook

Replace our `src/containers/Login.tsx` with the following:
import React, { useState, FormEvent } from "react";
import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
import { Auth } from "aws-amplify";
import "./Login.css";
import { AppProps } from "../App";
import { RouteComponentProps } from "react-router";
import LoaderButton from "../components/LoaderButton";
import { useFormFields } from "../libs/hooksLib";
interface LoginProps extends AppProps, RouteComponentProps {}
const Login = (props: LoginProps) => {
const [isLoading, setIsLoading] = useState(false);
const [fields, handleFieldChange] = useFormFields({
email: "",
password: ""
});
function validateForm() {
return fields.email.length > 0 && fields.password.length > 0;
}
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
setIsLoading(true);
try {
await Auth.signIn(fields.email, fields.password);
props.userHasAuthenticated(true);
props.history.push("/");
} catch (e) {
alert(e.message);
setIsLoading(false);
}
};
return (
<div className="Login">
<form onSubmit={handleSubmit}>
<FormGroup controlId="email" bsSize="large">
<ControlLabel>Email</ControlLabel>
<FormControl
autoFocus
type="email"
value={fields.email}
onChange={handleFieldChange}
/>
</FormGroup>
<FormGroup controlId="password" bsSize="large">
<ControlLabel>Password</ControlLabel>
<FormControl
value={fields.password}
onChange={handleFieldChange}
type="password"
/>
</FormGroup>
<LoaderButton
block
type="submit"
bsSize="large"
isLoading={isLoading}
disabled={!validateForm()}
>
Login
</LoaderButton>
</form>
</div>
);
};
export default Login;

You'll notice that we are using our useFormFields Hook. A good way to think about custom React Hooks is to simply replace the line where we use it, with the Hook code itself. So instead of this line:

const [fields, handleFieldChange] = useFormFields({
email: "",
password: ""
});

Simply imagine the code for the useFormFields function instead!

Finally, we are setting our fields using the function our custom Hook is returning.

onChange={handleFieldChange}

Now we are ready to tackle our sign up page.