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.