如何为 Formik 和 Yup 创建的 Form 设置初始值

how to set an initial value to Form created by Formik and Yup

我有一个关于如何将初始值设置到由 Formik 和 Yup 创建的表单中的问题。我想让用户在更新页面上编辑他们的测验,所以我想将用户更新的测验值插入到每个输入中。

我可以通过登录控制台获取一个测验对象作为 q 并进行检查,但出现无法读取问题的错误 属性。


import { useParams } from 'react-router-dom';
import { collection, query, setDoc, addDoc, getDoc, doc } from 'firebase/firestore';

import db from '../config/firebase';
import { useState, useEffect } from 'react';

import { Formik, Form, Field, FieldArray, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { ioRemoveCircleSharp } from '../icons/icons';

Yup.addMethod(Yup.array, 'unique', function (message, mapper = a => a) {
  return this.test('unique', message, function (list) {
    return list.length === new Set(list.map(mapper)).size;
  });
});

const quizSchema = Yup.object().shape({
  question: Yup.string()
    .min(10, 'Too Short!')
    .max(250, 'Too Long!')
    .required('Required'),
  answers: Yup.array()
    .of(Yup.string().max(50, 'Too Long!').required('Required'))
    .unique('Duplicate answers are not allowed')
    .min(2, `Minimum of 2 answers`),
  correctAnswer: Yup.number('only numbers are allowed')
    .min(1, 'type more than 1 please')
    .max(4, 'type less than 5')
    .required('Required'),
  category: Yup.string().required('Required'),
  createdAt: Yup.date(),
  likes: Yup.number(),
});

const QuizEdit = () => {
  const { id } = useParams();
  const [focused, setFocused] = useState(false);
  const [submitBtnHover, setSubmitBtnHover] = useState(false);
  const [q, setQuiz] = useState()

  useEffect(() => {
    const getData = async () => {
      const snap = await getDoc(doc(db, 'quizzes', id));
      console.log(snap.data().answers);
      const quiz = snap.data();
      console.log(quiz);
      setQuiz(quiz)
    };
    getData();
  }, []);

  console.log(`q => ${q}`)

  return (
    <div className='formikNewQuiz'>
      <Formik
        initialValues={{
          question: q["question"],
          answers: ['', ''],
          correctAnswer: '',
          category: '',
          createdAt: new Date(),
          likes: 0,
        }}
        validateOnChange
        validationSchema={quizSchema}
        onSubmit={async (values, { resetForm }) => {
          // same shape as initial values
          console.log(values);
          const quizCollectionRef = collection(db, 'quizzes');
          const payload = values;
          await addDoc(quizCollectionRef, payload);
          // values["question"] = "";
          // values["answers"] = ["", ""];
          // values.correctAnswer = ""
          // values.category = ""
          resetForm();
        }}
      >
        {({ errors, touched, values, q }) => (
          <Form>
            <div style={labelInputContainer}>
              <label htmlFor='question' style={label}>
                Question
              </label>
              <Field
                name='question'
                onFocus={() => {
                  setFocused('question');
                }}
                onBlur={() => {
                  setFocused('');
                }}
                style={focused === 'question' ? focusStyle : quizFormInputText}
              />
              {errors.question && touched.question ? (
                <div style={quizFormErrMsg}>{errors.question}</div>
              ) : null}
            </div>

            <FieldArray
              name='answers'
              style={quizFormInputText}
              render={arrayHelpers => (
                <div style={labelInputContainer}>
                  <label htmlFor='answers1' style={label}>
                    Answers
                  </label>
                  {errors.answers &&
                  touched.answers &&
                  errors.answers === 'Duplicate answers are not allowed' ? (
                    <div style={quizFormErrMsg}>{errors.answers}</div>
                  ) : null}
                  {values.answers && values.answers.length > 0
                    ? values.answers.map((answer, index) => (
                        <div key={index} style={answerContainer}>
                          <div style={indexAnswerIconContainer}>
                            <span style={answerIndex}>{index + 1}</span>
                            <Field
                              name={`answers.${index}`}
                              onFocus={() => {
                                switch (index) {
                                  case 0:
                                    return setFocused('a1');
                                  case 1:
                                    return setFocused('a2');
                                  case 2:
                                    return setFocused('a3');
                                  case 3:
                                    return setFocused('a4');
                                  default:
                                    return setFocused('');
                                }
                              }}
                              onBlur={() => {
                                switch (index) {
                                  case 0:
                                    return setFocused('');
                                  case 1:
                                    return setFocused('');
                                  case 2:
                                    return setFocused('');
                                  case 3:
                                    return setFocused('');
                                  default:
                                    return setFocused('');
                                }
                              }}
                              style={
                                focused === 'a1' && index === 0
                                  ? focusStyle
                                  : focused === 'a2' && index === 1
                                  ? focusStyle
                                  : focused === 'a3' && index === 2
                                  ? focusStyle
                                  : focused === 'a4' && index === 3
                                  ? focusStyle
                                  : quizFormInputText
                              }
                            />
                            {values.answers.length >= 3 ? (
                              <i
                                onClick={() => arrayHelpers.remove(index)}
                                style={removeIcon}
                              >
                                {ioRemoveCircleSharp}
                              </i>
                            ) : (
                              ''
                            )}
                          </div>
                          {/* <div style={quizFormErrMsg}>
                            <ErrorMessage name={`answers.${index}`} />
                          </div> */}
                          {errors.answers &&
                          touched.answers &&
                          errors.answers !==
                            'Duplicate answers are not allowed' ? (
                            <div style={quizFormErrMsg}>
                              <ErrorMessage name={`answers.${index}`} />
                            </div>
                          ) : null}
                        </div>
                      ))
                    : null}
                  {values.answers.length <= 3 ? (
                    <div
                      // style={addHover ? moreAnswerIconHover : moreAnswerIcon}
                      style={moreAnswerIcon}
                      onClick={() => arrayHelpers.push('')}
                      // onMouseEnter={() => setAddHover(true)}
                      // onMouseLeave={() => setAddHover(false)}
                    >
                      Add
                    </div>
                  ) : null}
                </div>
              )}
            />

            <div style={labelInputContainer}>
              <label htmlFor='correctAnswer' style={label}>
                Correct Answer
              </label>
              <Field
                min='1'
                max={values.answers.length}
                type='number'
                name='correctAnswer'
                onFocus={() => {
                  setFocused('ca');
                }}
                onBlur={() => {
                  setFocused('');
                }}
                style={focused === 'ca' ? focusStyle : quizFormInputText}
              />
              {errors.correctAnswer && touched.correctAnswer ? (
                <div style={quizFormErrMsg}>
                  Only 1 ~ {values.answers.length} are allowed.
                </div>
              ) : null}
            </div>

            <div style={labelInputContainer}>
              <label style={label}>Category</label>
              <Field
                as='select'
                name='category'
                onFocus={() => {
                  setFocused('category');
                }}
                onBlur={() => {
                  setFocused('');
                }}
                style={focused === 'category' ? focusStyle : quizFormInputText}
              >
                <option value='' disabled>
                  Select a category
                </option>
                <option value='Workout'>Workout</option>
                <option value='Muscle'>Muscle</option>
                <option value='Nutrition'>Nutrition</option>
                <option value='Other'>Other</option>
              </Field>
              {errors.category && touched.category ? (
                <div style={quizFormErrMsg}>{errors.category}</div>
              ) : null}
            </div>

            <button
              // disabled={!dirty && isValid}
              type='submit'
              style={submitBtnHover ? submitButtonHover : submitButton}
              onMouseEnter={() => setSubmitBtnHover(true)}
              onMouseLeave={() => setSubmitBtnHover(false)}
            >
              Submit
            </button>
          </Form>
        )}
      </Formik>
    </div>
  );
};

// ========== Styles =========
const labelInputContainer = {
  background: '',
  margin: '20px 0',
};

const label = {
  fontSize: '1.5rem',
  margin: '10px 0 5px',
  fontFamily: "'Cormorant Garamond', serif",
};

const quizFormInputText = {
  display: 'block',
  width: '100%',
  flexDirection: 'column',
  border: 'none',
  borderBottom: '1px solid rgb(200, 200, 200)',
  fontSize: '1.2rem',
  padding: '10px 15px',
  marginTop: '5px',
  transition: '.3s',
};
const focusStyle = {
  display: 'block',
  width: '100%',
  flexDirection: 'column',
  border: 'none',
  fontSize: '1.2rem',
  padding: '10px 15px',
  outline: 'none',
  borderBottom: '1px solid #005bbb',
  marginTop: '5px',
  background: '#ecf5ff',
  transition: '.3s',
};

const quizFormErrMsg = {
  color: 'red',
  fontSize: '.9rem',
};

// This includes an answer and error
const answerContainer = {
  display: 'flex',
  flexDirection: 'column',
};

// This includes an answer index, input, and a remove icon .
const indexAnswerIconContainer = {
  marginTop: '20px',
  display: 'flex',
  alignItems: 'center',
  gap: '10px',
  position: 'relative',
};

const answerIndex = {
  position: 'absolute',
  top: '-4px',
  left: '-4px',
  fontSize: '1rem',
};
const removeIcon = {
  position: 'absolute',
  top: '0px',
  right: '-8px',
  fontSize: '1.2rem',
  color: 'red',
  cursor: 'pointer',
  height: '25px',
};

const moreAnswerIcon = {
  fontSize: '1.2rem',
  fontFamily: "'Cormorant Garamond', serif",
  color: '#005bbb',
  padding: '5px 10px',
  marginTop: "20px",
  cursor: 'pointer',
  transition: '.3s',
  textAlign: 'center',
};

// const moreAnswerIconHover = {
//   fontSize: '1.2rem',
//   fontFamily: "'Cormorant Garamond', serif",
//   color: '#005bbb',
//   padding: '5px 10px',
//   marginTop: "20px",
//   cursor: 'pointer',
//   transition: '.3s',
//   textAlign: 'center',
//   opacity: ".7"
// };

const submitButton = {
  padding: '10px 18px',
  marginTop: '20px',
  borderRadius: '5px',
  border: 'none',
  width: '100%',
  cursor: 'pointer',
  transition: '.3s',
  background: 'none',
  color: '#005bbb',
  fontSize: '1.5rem',
  fontFamily: "'Cormorant Garamond', serif",
};

const submitButtonHover = {
  padding: '10px 18px',
  marginTop: '20px',
  borderRadius: '5px',
  border: 'none',
  width: '100%',
  cursor: 'pointer',
  transition: '.3s',
  background: '#ecf5ff',
  color: '#005bbb',
  fontSize: '1.5rem',
  fontFamily: "'Cormorant Garamond', serif",
};


export default QuizEdit;

第一次加载页面时状态值 qundefined,因为您在创建这样的状态时没有设置初始值

const [q, setQuiz] = useState();

要解决此问题,您必须像这样在状态定义期间传入初始值

const initialQuizValue = {} // whatever defaults you want

const [q, setQuiz] = useState(initialQuizValue);

或者在像这样呈现 Formik 组件之前检查 q 是否可用

const QuizEdit = () => {
  const { id } = useParams();
  const [focused, setFocused] = useState(false);
  const [submitBtnHover, setSubmitBtnHover] = useState(false);
  const [q, setQuiz] = useState()

  useEffect(() => {
    ...
  }, []);

  if (!q) {
    return 'Loading quiz data...'
  }

  return (
    <div className='formikNewQuiz'>
      <Formik
        initialValues={{
          question: q["question"],
          answers: ['', ''],
          correctAnswer: '',
          category: '',
          createdAt: new Date(),
          likes: 0,
        }}
      >
        ...
      </Formik>
  )
}