DEV Community

e4c5Nf3d6
e4c5Nf3d6

Posted on

Using React Select with Formik

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:

  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.

// imports

function AddGame(props) {
    const [white, setWhite] = useState(null);
    const [black, setBlack] = useState(null);

    // the rest of the code
}

// exports
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 });
                }
            });
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

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)}
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)