Home > Software design >  Problem with select Field in Formik when onChange option given
Problem with select Field in Formik when onChange option given

Time:12-20

I have a problem with my <Field as="select"> in Formik. The onChange doesn't work well - no matter what option I choose, when I submit my form, it sends player1 and player2 as empty string.

E.g. output from console.log(values):

{ id: '10', player1: '', player2: '', winner: '1', date: '2021-12-16' }

When I remove the onChange it works well, but I need onChange and selectedPlayer states, because if I pick a player1, I don't want him to be visible in second option and vice versa. Why the value property doesn't change in my Field when the selectedPlayer is being changed? It seems like it gets the initialValue from selectedPlayer which is empty string.

import { Field, Form, Formik } from 'formik';
import { connect } from 'react-redux';
import {addMatchAction} from "../../ducks/matches/operations";
import {getAllPlayers} from "../../ducks/players/selectors";
import {useState} from "react";
const MatchesForm = ({ addMatchAction,error,players }, props) => {
    const handleSubmit = (values) => {

        console.log(values)
        addMatchAction(values);
    }

    const [selectedPlayer1,setSelectedPlayer1] = useState('')
    const [selectedPlayer2,setSelectedPlayer2] = useState('')
    return (
        <div>
            <h3>Add match</h3>
            <Formik
                initialValues={{
                    id: '',
                    player1: '',
                    player2: '',
                    winner: '',
                    date: ''
                }}
                onSubmit={(values) => handleSubmit(values)}
                enableReinitialize={true}>
                <Form>
                    <label htmlFor="id">Id: </label>
                    <Field name="id" />
                    <label htmlFor="player1">Player 1: </label>
                    <Field as="select" name="player1" onChange={(e) => setSelectedPlayer1(e.target.value)} value={selectedPlayer1}>
                        <option disabled value="">(Select a player 1)</option>
                        {players && players.map(player => {
                            if (player.id !== selectedPlayer2) {
                                return (
                                    <option value={player.id}>{player.firstName} {player.lastName}</option>
                                )
                            }
                        }) }
                    </Field>
                    <p>
                        <label htmlFor="player2">Player 2: </label>
                        <Field as="select" name="player2" onChange={(e) => setSelectedPlayer2(e.target.value)} value={selectedPlayer2}>
                        <option disabled value="">(Select a player 2)</option>
                        {players && players.map(player => {
                            if (player.id !== selectedPlayer1) {
                                return (
                                    <option value={player.id}>{player.firstName} {player.lastName}</option>
                                )
                        }
                        }) }
                        </Field>
                    </p>
                    <label htmlFor="winner">Winner: </label>
                    <Field as="select" name="winner">
                        <option disabled value="">Pick a winner</option>
                        {players && players.map(player => {
                            if (player.id === selectedPlayer1 || player.id === selectedPlayer2 ) {
                                return (
                                    <option value={player.id}>{player.firstName} {player.lastName}</option>
                                )
                            }
                        })}
                    </Field>
                    <label htmlFor="date">Date: </label>
                    <Field name="date" type="date" />
                    <button type="submit">
                        Zatwierdz
                    </button>
                </Form>
            </Formik>
            <p>{error && (<span>{error.name} {error.response.message}</span>)}</p>
        </div>
    )
}

const mapStateToProps = (state) => {
    return {
        error: state.error.error,
        players: getAllPlayers(state)
    }
}

const mapDispatchToProps = ({
    addMatchAction
});


export default connect(mapStateToProps, mapDispatchToProps)(MatchesForm);

CodePudding user response:

I think that you are not understanding how Formik and this Field component work. Formik is using its own internal state to handle all Form states, so you should be using it and avoiding useState in this scenario. Here is what you can do:

  • Remove onChange handler and value as they are not allowing Formik to do its job.
  • Use the function just under Formik component as it is allowed and it will allow you to access the Form state values.

Here's the code. I've also refactored the array.map functions because it is more correct to use an array filter for these scenarios (map should always return the same number of elements in the array)

const MatchesForm = ({ addMatchAction,error,players }, props) => {
  const handleSubmit = (values) => {
      console.log('submit', values)
      addMatchAction(values);
  }

  return (
      <div>
          <h3>Add match</h3>
          <Formik
              initialValues={{
                  id: '',
                  player1: '',
                  player2: '',
                  winner: '',
                  date: ''
              }}
              onSubmit={(values) => handleSubmit(values)}
              enableReinitialize={true}>
                  {props => {
                      // Try a console.log here to see props and props.values and you will see them updating on every change
                      // console.log('Values', props.values);

                      return (
                        <Form>
                        <label htmlFor="id">Id: </label>
                        <Field name="id" />
                        <label htmlFor="player1">Player 1: </label>
                        <Field as="select" name="player1">
                            <option disabled value="">(Select a player 1)</option>
                            {players && players.filter(player => player.id !== props.values.player2).map(player => (
                                <option value={player.id}>{player.firstName} {player.lastName}</option>
                            ))}
                        </Field>
                        <p>
                            <label htmlFor="player2">Player 2: </label>
                            <Field as="select" name="player2" >
                            <option disabled value="">(Select a player 2)</option>
                            {players && players.filter(player => player.id !== props.values.player1).map(player => (
                                <option value={player.id}>{player.firstName} {player.lastName}</option>
                            ))}
                            </Field>
                        </p>
                        <label htmlFor="winner">Winner: </label>
                        <Field as="select" name="winner">
                            <option disabled value="">Pick a winner</option>
                            {players && players.filter(player => player.id === props.values.player1 || player.id === props.values.player2).map(player => (
                                <option value={player.id}>{player.firstName} {player.lastName}</option>
                            ))}
                        </Field>
                        <label htmlFor="date">Date: </label>
                        <Field name="date" type="date" />
                        <button type="submit">
                            Zatwierdz
                        </button>
                    </Form>
          
                      )
                  }}
          </Formik>
          <p>{error && (<span>{error.name} {error.response.message}</span>)}</p>
      </div>
  )
}

CodePudding user response:

If you set the onChange hander for Formik, then you're overwriting the default handler, and would need to manual setFieldValue

i.e.

onChange={(e) => {
  setSelectedPlayer1(e.target.value)
  setFieldValue('player1', e.target.value)
}}

What you could do as an alternative is make Formik your top-most parent component, which would allow all its children to consume its context like so:

const ChildComponent = () => {
  const { values } = useFormikContext()
  const player1Value = values['player1']
  return <div>Player 1 is: {player1Value}</div>
}

This way, you don't have to do additional state management like you're doing with useState, it's already kept for you in Formik's context.

See here: https://formik.org/docs/api/useFormikContext

  • Related