You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React useReducer用户信息表单无法修改输入值的问题求助

问题:MERN栈表单无法手动修改已填充的用户信息

我是首次提问,感谢您的帮助。我接触MERN栈已有数周,在开发用户查看/修改个人信息表单时遇到问题:通过useEffect钩子结合axios的GET请求获取当前用户信息,调用reducer的dispatch更新状态后,表单能正确自动填充信息,但无法手动修改输入值。以下是我的代码:

import React, { useContext, useEffect, useReducer } from 'react';
import { Helmet } from 'react-helmet-async';
import Form from 'react-bootstrap/Form';
import { Resalia } from '../Resalia';
import Button from 'react-bootstrap/Button';
import { toast } from 'react-toastify';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { getError } from '../utils';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';

const reducer = (state, action) => {
  switch (action.type) {
    case 'FIELD':
      return { ...state, [action.fieldName]: action.payload };
    case 'FETCH_REQUEST':
      return { ...state, loading: true };
    case 'FETCH_SUCCESS':
      return { ...state, user: action.payload, loading: false };
    case 'FETCH_FAIL':
      return { ...state, error: action.payload, loading: false };
    default:
      return state;
  }
};

export default function AdminPersonalInfoScreen() {
  const navigate = useNavigate();
  const { state } = useContext(Resalia);
  const { userInfo } = state;
  const { email } = userInfo;

  const [{ user, error, loading }, dispatch] = useReducer(reducer, {
    loading: true,
    user: {},
    error: '',
  });

  const {
    name,
    surname,
    phone1,
    phone2,
    address1,
    address2,
    city,
    province,
    postcode,
  } = user;

  useEffect(() => {
    if (!userInfo) {
      navigate('/signin?redirect=/admin/personal-info');
    }
  }, [userInfo, navigate]);

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_REQUEST' });
      try {
        const result = await axios.get(
          `/api/users/personal-info/${userInfo._id}`
        );
        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (err) {
        dispatch({ type: 'FETCH_FAIL', payload: err.message });
      }
    };
    fetchData();
  }, [userInfo._id]);

  const submitHandler = async (e) => {
    e.preventDefault();

    try {
      const { data } = await axios.put(
        '/api/users/personal-info',
        {
          email,
          name,
          surname,
          phone1,
          phone2,
          address1,
          address2,
          city,
          province,
          postcode,
        },
        {
          headers: { Authorization: `Bearer ${userInfo.token}` },
        }
      );
      localStorage.setItem('userInfo', JSON.stringify(data));
      toast.success('Datos guardados con éxito');
    } catch (err) {
      toast.error(getError(err));
    }

  };

  return (
    <div>
      <Helmet>
        <title>Mis Datos Personales</title>
      </Helmet>
      <h2 className="mt-4 mb-5">Mis Datos Personales</h2>
      {loading ? (
        <LoadingBox />
      ) : error ? (
        <MessageBox variant="danger">{error}</MessageBox>
      ) : (
        <Form onSubmit={submitHandler}>
          <Form.Group className="mb-4" controlId="name">
            <Form.Label>Nombre</Form.Label>
            <Form.Control
              type="text"
              required
              value={user.name}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'name',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="surname">
            <Form.Label>Apellidos</Form.Label>
            <Form.Control
              type="text"
              value={surname}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'surname',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="email">
            <Form.Label>Email</Form.Label>
            <Form.Control type="email" value={email} required disabled />
          </Form.Group>
          <Form.Group className="mb-4" controlId="phone1">
            <Form.Label>Teléfono de Contacto</Form.Label>
            <Form.Control
              type="phone"
              value={phone1}
              required
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'phone1',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="phone2">
            <Form.Label>Teléfono Adicional</Form.Label>
            <Form.Control
              type="phone"
              value={phone2}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'phone2',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="address1">
            <Form.Label>Dirección (nombre de la vía y número)</Form.Label>
            <Form.Control
              type="text"
              value={address1}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'address1',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="address2">
            <Form.Label>Dirección 2 (piso y puerta si procede)</Form.Label>
            <Form.Control
              type="text"
              value={address2}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'address2',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="city">
            <Form.Label>Municipio</Form.Label>
            <Form.Control
              type="text"
              value={city}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'city',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="province">
            <Form.Label>Provincia</Form.Label>
            <Form.Control
              type="text"
              value={province}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'province',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <Form.Group className="mb-4" controlId="postcode">
            <Form.Label>Código Postal</Form.Label>
            <Form.Control
              type="text"
              value={postcode}
              onChange={(e) => {
                dispatch({
                  type: 'FIELD',
                  fieldName: 'postcode',
                  payload: e.currentTarget.value,
                });
              }}
            />
          </Form.Group>
          <div className="mb-3">
            <Button variant="primary" type="submit">
              Guardar Cambios
            </Button>
          </div>
        </Form>
      )}
    </div>
  );
}

请问如何实现既能获取用户当前信息,又允许手动修改输入值?我尝试自行修复并查找相关问题但未成功,恳请帮助。


解决方案

问题根源

你的reducer中FIELD类型的action是直接更新state的顶层属性(比如name、surname),但表单输入框绑定的是user对象下的属性(如user.name),两者没有关联。当你dispatch修改字段时,state的顶层属性变化了,但user对象里的属性并没有更新,所以输入框的值始终保持初始获取的用户信息,无法响应手动修改。

修复步骤

  1. 修改Reducer的FIELD处理逻辑
    更新FIELD action,让它修改user对象内部的对应字段,而不是state的顶层属性:

    const reducer = (state, action) => {
      switch (action.type) {
        case 'FIELD':
          return { 
            ...state, 
            user: { ...state.user, [action.fieldName]: action.payload } 
          };
        case 'FETCH_REQUEST':
          return { ...state, loading: true };
        case 'FETCH_SUCCESS':
          return { ...state, user: action.payload, loading: false };
        case 'FETCH_FAIL':
          return { ...state, error: action.payload, loading: false };
        default:
          return state;
      }
    };
    
  2. 确保表单输入值与更新后的user属性绑定
    你的输入框已经正确绑定了user.name、user.surname等属性,这一步不需要额外修改,但要确保解构的变量也是从user对象中获取的(你当前的代码已经做到了):

    const {
      name,
      surname,
      phone1,
      phone2,
      address1,
      address2,
      city,
      province,
      postcode,
    } = user;
    
  3. 可选:优化初始状态避免undefined
    可以给初始的user对象设置默认空字符串,避免输入框初始值为undefined:

    const [{ user, error, loading }, dispatch] = useReducer(reducer, {
      loading: true,
      user: {
        name: '',
        surname: '',
        phone1: '',
        phone2: '',
        address1: '',
        address2: '',
        city: '',
        province: '',
        postcode: ''
      },
      error: '',
    });
    

修复原理

修改后的FIELD action会先复制原有的user对象,再更新指定字段,保证user对象的引用发生变化,触发组件重新渲染,输入框就能正确显示手动修改后的值。同时,表单提交时使用的解构变量(name、surname等)也会同步更新为最新值,确保提交数据正确。


内容的提问来源于stack exchange,提问作者Ángel Castillo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 00:35:27