I was recently building an application that, among other features, allows a user to submit chess players and chess games to a database. I was utilizing Yup for form schema and Formik for error handling, validation, and form submission.
In order to submit a chess game, the user must provide the game's PGN and choose the game's players from players that exist in the database.
I decided that I wanted to use React Select for the player input, specifically React Select Creatable, which allows users to create a new option if the one that they are looking for does not exist. With this approach, if the player that the user is trying to add as one of the game's players is not already in the database, they can add the player directly from the React Select component instead of having to do this though the seperate form for adding a new player.
The Problem
The React Select options used to display the choices to the user in the component need to be in a specific format. For instance, the option for the player Mikhail Tal would need to be in the following format:
{ value: 'Mikhail Tal', label: 'Mikhail Tal' }
However, for validation purposes, the white-player
and black_player
values used with Yup and Formik and ultimately submitted using Formik would need to be in string format, such as 'Mikhail Tal'
. I considered trying to store these values in Formik in the format requred for React Select and simply extracting the necessary value when the form is submitted. However, doing so would cause Formik to throw errors since the Formik field values would not be in the expected format when the form was submitted.
This meant a couple of things for my code:
- I would not be able to use Formik to keep track of the values of the React Select fields. They would need to be tracked another way.
- I would need a way of setting the Formik values when the values of the React Select fields changed as well as when a new option was created.
The Solution
First, I set up the state that would be needed to control my React Select input values.
// imports
function AddGame(props) {
const [white, setWhite] = useState(null);
const [black, setBlack] = useState(null);
// the rest of the code
}
// exports
Here is the general setup of the CreatableSelect
components rendered in the AddGame
component:
<CreatableSelect
isClearable
onChange={(player) => setWhite(player)}
options={options}
placeholder="Choose the player with the white pieces"
value={white}
/>
<CreatableSelect
isClearable
onChange={(player) => setBlack(player)}
options={options}
placeholder="Choose the player with the black pieces"
value={black}
/>
The values are determined by the white
or black
states and the onChange
props use setWhite
or setBlack
to set said states.
In order for the Formik values to reflect the change in state, I needed to call a more robust function in the onChange
prop. I needed to be able to manually set the Formik values, something that is usually handled by Formik but can also be accomplished with formik.setFieldValue()
. For example, if I wanted to set the white_player
value to 'Mikhail Tal', I could use the following statement:
formik.setFieldValue(white_player, 'Mikhail Tal')
(It was at this point in the process that I set up a custom hook to handle the logic, since I would need this functionality in a few different places in my code. However, it can definitely be accomplished in one file as well.)
I created a handleSelect
function that would update both the state that controlled the React Select fields and the corresponding Formik values.
function handleSelect(color, player) {
if (player === null) {
formik.setFieldValue(color, '');
} else {
formik.setFieldValue(color, player["value"]);
}
if (color === "white_player") {
setWhite(player);
} else if (color === "black_player") {
setBlack(player);
}
}
The CreatableSelect
components' onChange
functions need to be updated accordingly. For example:
onChange={(player) => setBlack(player)}
becomes
onChange={(player) => handleSelect('white_player', player)}
Finally, I needed to implement the creatable aspect of React Select. The CreatableSelect
components have the onCreateOption
prop to help with this.
I made a handleCreate
function to handle the creation of a new player. It makes a POST request to the "/players" endpoint of my API and then, assuming it recieves a 201 (created) status code, sets the corresponding Formik field value and player state using the response data.
function handleCreate(color, newPlayer) {
fetch("/players", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({name: newPlayer}, null, 2)
}).then((r) => {
if (r.status === 201) {
r.json()
.then((player) => {
onSetPlayers([...players, player]);
formik.setFieldValue(color, player.name);
if (color === "white_player") {
setWhite({ value: player.name, label: player.name });
} else if (color === "black_player") {
setBlack({ value: player.name, label: player.name });
}
});
}
});
}
The CreatableSelect
components end up looking like this:
<CreatableSelect
isClearable
onChange={(player) => handleSelect('black_player', player)}
options={options}
placeholder="Choose the player with the black pieces"
value={black}
onCreateOption={(newPlayer) => handleCreate('black_player', newPlayer)}
/>
Conclusion
Yup and Formik are extremely useful tools that help handle many of the more frustrating aspects of setting up forms in React, such as data validation and error handling. React Select has a number of useful features for creating select inputs in React, including built-in styling as well as Creatable components that allow for the creation of new options. Utilizing these tools together can greatly improve the functionality of your form. It is also relatively simple to use them in concert with each other - once you know how.
Full Solution Code
import React, { useState } from "react";
import { useFormik } from "formik";
import * as yup from "yup";
import CreatableSelect from 'react-select/creatable';
import useSelectData from "../hooks/useSelectData";
function AddGame({ games, onSetGames, players, onSetPlayers }) {
const [isEditing, setIsEditing] = useState(false);
const [showError, setShowError] = useState(false);
const [white, setWhite] = useState(null);
const [black, setBlack] = useState(null);
const options = [
...players.map((player) => ({ value: player.name, label: player.name }))
];
function handleClose() {
formik.resetForm();
setShowError(false);
setWhite(null);
setBlack(null);
setIsEditing(false);
}
const formSchema = yup.object().shape({
pgn: yup.string()
.required("Please enter the game PGN")
.matches(/^(\s*(?:\[\s*(\w+)\s*"([^"]*)"\s*\]\s*)*(?:(\d+)(\.|\.{3})\s*((?:[PNBRQK]?[a-h]?[1-8]?x?[a-h][1-8](?:\=[PNBRQK])?|O(-?O){1,2})[\+#]?(\s*[\!\?]+)?)(?:\s*(?:\{([^\}]*?)\}\s*)?((?:[PNBRQK]?[a-h]?[1-8]?x?[a-h][1-8](?:\=[PNBRQK])?|O(-?O){1,2})[\+#]?(\s*[\!\?]+)?))?\s*(?:\(\s*((?:(\d+)(\.|\.{3})\s*((?:[PNBRQK]?[a-h]?[1-8]?x?[a-h][1-8](?:\=[PNBRQK])?|O(-?O){1,2})[\+#]?(\s*[\!\?]+)?)(?:\s*(?:\{([^\}]*?)\}\s*)?((?:[PNBRQK]a-h]?[1-8]?x?[a-h][1-8](?:\=[PNBRQK])?|O(-?O){1,2})[\+#]?(\s*[\!\?]+)?))'?\s*(?:\((.*)\)\s*)?(?:\{([^\}]*?)\}\s*)?)*)\s*\)\s*)*(?:\{([^\}]*?)\}\s*)?)*(1\-?0|0\-?1|1\/2\-?1\/2|\*)?\s*)$/ , 'Invalid PGN format'),
white_player: yup.string()
.required("Please choose the player with the white pieces"),
black_player: yup.string()
.required("Please choose the player with the black pieces"),
});
const formik = useFormik({
initialValues: {
pgn: "",
white_player: "",
black_player: ""
},
validateOnChange: false,
validationSchema: formSchema,
onSubmit: (values, { resetForm }) => {
fetch("/games", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(values, null, 2)
})
.then((r) => {
if (r.status === 201) {
r.json()
.then((game) => {
onSetGames([...games, game]);
handleClose();
});
} else if (r.status === 422) {
setShowError(true);
resetForm();
formik.setFieldValue('white_player', white['value']);
formik.setFieldValue('black_player', black['value']);
}
});
}
});
const { handleSelect, handleCreate } = useSelectData(formik, setWhite, setBlack, players, onSetPlayers);
return (
<div>
{isEditing ?
<div className="add">
<h3>Add a Game</h3>
{showError ? <p style={{ color: "red" }}>Failed PGN Validation</p> : null}
<form onSubmit={formik.handleSubmit}>
<textarea
type="text"
id="pgn"
name="pgn"
placeholder="Game PGN"
value={formik.values.pgn}
onChange={formik.handleChange}
/>
{formik.errors.pgn ? <p style={{ color: "red" }}>{formik.errors.pgn}</p> : null}
<div className="select">
<CreatableSelect
isClearable
onChange={(player) => handleSelect('white_player', player)}
options={options}
placeholder="Choose the player with the white pieces"
value={white}
onCreateOption={(newPlayer) => handleCreate('white_player', newPlayer)}
menuPortalTarget={document.body}
styles={{ menuPortal: base => ({ ...base, zIndex: 9999 }) }}
/>
{formik.errors.white_player ? <p style={{ color: "red" }}>{formik.errors.white_player}</p> : null}
</div>
<div className="select">
<CreatableSelect
isClearable
onChange={(player) => handleSelect('black_player', player)}
options={options}
placeholder="Choose the player with the black pieces"
value={black}
onCreateOption={(newPlayer) => handleCreate('black_player', newPlayer)}
menuPortalTarget={document.body}
styles={{ menuPortal: base => ({ ...base, zIndex: 9999 }) }}
/>
{formik.errors.black_player ? <p style={{ color: "red" }}>{formik.errors.black_player}</p> : null}
</div>
<button className="submit-button" type="submit">Submit</button>
<button type="reset" onClick={handleClose}>Close</button>
</form>
</div>
:
<button onClick={() => setIsEditing(!isEditing)}>Add Game</button>
}
</div>
);
}
export default AddGame;
function useSelectData(formik, setWhite, setBlack, players, onSetPlayers) {
function handleSelect(color, player) {
if (player === null) {
formik.setFieldValue(color, '');
} else {
formik.setFieldValue(color, player["value"]);
}
if (color === "white_player") {
setWhite(player);
} else if (color === "black_player") {
setBlack(player);
}
}
function handleCreate(color, newPlayer) {
fetch("/players", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({name: newPlayer}, null, 2)
}).then((r) => {
if (r.status === 201) {
r.json()
.then((player) => {
onSetPlayers([...players, player]);
formik.setFieldValue(color, player.name);
if (color === "white_player") {
setWhite({ value: player.name, label: player.name });
} else if (color === "black_player") {
setBlack({ value: player.name, label: player.name });
}
});
}
});
}
return { handleSelect, handleCreate };
}
export default useSelectData;
Top comments (0)