Using React Select with Formik
Tags: javascript, react, fullstack, webdev
Published: 2024-03-17 23:30:17

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](https://github.com/jquense/yup) for form schema and [Formik](https://formik.org/) 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](https://react-select.com/) for the player input, specifically [React Select Creatable](https://react-select.com/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:
1. 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.
2. 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.
```{js}
// 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:
```{js}
<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.
```{js}
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.

```{js}
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:
```{js}
<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

```{js}
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;
```


```{js}
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;
```