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

React+TypeScript中用Redux Toolkit单Reducer更新表单多字段问题

问题描述

开发包含name、image、price、qty、info五个字段的更新表单时,未使用useState维护本地状态,直接从Redux Store取表单数据并尝试更新Store后提交。但调用updateProductState reducer更新字段时,Store数据未正确更新,反而被设置成类似"name: name"的错误值。

ProductSlice.ts

export interface ProductState {
  loading: boolean;
  products: ProductModel[];
  selectedProduct: ProductModel;
  error: string;
}

export let initialState: ProductState = {
  loading: false,
  products: [] as ProductModel[],
  selectedProduct: {} as ProductModel,
  error: ''
};

export const productSlice = createSlice( {
  name: 'product',
  initialState,
  reducers: {
    updateProductState: ( state, action ) => {
      const { id, name, image, price, qty, info } = action.payload;
      return {
        ...state,
        selectedProduct: {
          id,
          name,
          image,
          price,
          qty,
          info,
        } as ProductModel,
      };
    }
  }
});

UpdateProduct.ts

import React, { useEffect, useState } from 'react';
import { useDispatch,useSelector } from 'react-redux';
import { useParams} from 'react-router-dom';
import { AppDispatch } from '../../../../redux/store';
import * as productActions from '../../../../redux/productsSlice';
import { useNavigate } from 'react-router';

interface IProps{ }
interface IState {
  bigBasket: productActions.ProductState;
}

type URLParams= {
  productId: string;
}

const UpdateProduct: React.FC<IProps> = ({}) => {
  let dispatch = useDispatch<AppDispatch>();
  const { productId } = useParams<URLParams>();
  const navigate = useNavigate()

  let productState: productActions.ProductState = useSelector((store: IState) => store.bigBasket)

  let { error, loading, selectedProduct } = productState;

  useEffect( () => {
    dispatch(productActions.getSingleProductThunk(productId as string));
  }, [productId])

  let updateInput = (event:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    dispatch(productActions.updateProductState({
      name: event.target.name,
      value:event.target.value
    }))
  }

  let updateImage = async (event:React.ChangeEvent<HTMLInputElement | any>) => {
    let imageFile:Blob = event.target.files[0];
    let base64Image:string | ArrayBuffer = await convertBase64String(imageFile);
    dispatch(productActions.updateProductState({
      name: "image",
      value: base64Image
    }));
  };

  let convertBase64String = (imageFile:Blob):Promise<string | ArrayBuffer> => {
    return new Promise((resolve, reject) => {
      let fileReader = new FileReader();
      fileReader.readAsDataURL(imageFile);
      fileReader.addEventListener('load', () => {
        if (fileReader.result) {
          resolve(fileReader.result);
        } else {
          reject('Error Occurred');
        }
      })
    });
  };

  let submitUpdateProduct = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    dispatch(productActions.updateProductThunk({
      product: selectedProduct,
      productId: productId as string,
      navigate
    }));
  }

  return (
    <React.Fragment>
      {<pre>{JSON.stringify(selectedProduct)}</pre>}
      <section className='mt-3'>
        <div className="container">
          <div className="row">
            <div className="col-md-4">
              <div className="card">
                <div className="card-header bg-secondary bg-white">
                  <p className="h4 text-white">Update Product</p>
                </div>
                <div className="card-body rgba-green-light">
                  <form onSubmit={submitUpdateProduct}>
                    <div className="mb-3">
                      <input
                        required
                        name="name"
                        value={selectedProduct.name}
                        onChange={updateInput}
                        type="text"
                        className="form-control"
                        placeholder="Name"
                      />
                    </div>
                    <div className="mb-3">
                      <input
                        onChange={updateImage}
                        className="form-control"
                        type="file"
                        id="formFile"
                      />
                      {selectedProduct.image &&
                        <img
                          src={selectedProduct.image}
                          alt=""
                          width="25"
                          height="25"
                        />
                      }
                    </div>
                    <div className="mb-3">
                      <input
                        required
                        type="number"
                        name="price"
                        value={selectedProduct.price} 
                        onChange={updateInput}
                        className='form-control'
                        placeholder="Price"
                      />
                    </div>
                    <div className="mb-3">
                      <input
                        required
                        type="number"
                        name="qty"
                        value={selectedProduct.qty}
                        onChange={updateInput}
                        className='form-control'
                        placeholder='Available Quantity'
                      />
                    </div>
                    <div className="mb-3">
                      <textarea
                        required
                        name="info"
                        value={selectedProduct.info} 
                        onChange={updateInput}
                        rows={3}
                        className='form-control'
                        placeholder="General Info"
                      />
                    </div>
                    <div className="">
                      <input
                        type="submit"
                        className="btn btn-secondary btn-sm"
                        value="Update"
                      />
                    </div>
                  </form>
                </div>
              </div>
            </div>
          </div>
        </div>
      </section>
    </React.Fragment>
  )
}

export default UpdateProduct
解决方案

问题根源是reducer接收的payload格式和组件dispatch的payload不匹配,且更新时未保留selectedProduct原有字段,导致每次更新都会丢失其他字段数据。

1. 修正reducer逻辑

当前updateProductState reducer期望payload包含id、name、image等所有字段,但组件实际传递的是{name: 字段名, value: 字段值}格式,需改成动态更新单个字段并保留原有数据:

updateProductState: (state, action) => {
  const { field, value } = action.payload;
  return {
    ...state,
    selectedProduct: {
      ...state.selectedProduct, // 保留原有所有字段
      [field]: value // 动态更新目标字段
    } as ProductModel
  };
}

2. 修正组件dispatch的参数

组件调用updateProductState时,需将字段名称用field传递,替换原来的name:

  • 修改updateInput函数:
let updateInput = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  dispatch(productActions.updateProductState({
    field: event.target.name,
    value: event.target.value
  }))
}
  • 修改updateImage函数:
dispatch(productActions.updateProductState({
  field: "image",
  value: base64Image
}));

3. 额外提醒

  • 确保getSingleProductThunk能正确获取单条产品数据并更新selectedProduct,否则初始空对象会导致表单显示异常;
  • 若ProductModel包含id字段,更新过程中要保证selectedProduct始终保留id,避免提交时丢失主键信息。

内容的提问来源于stack exchange,提问作者Newbie2015

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:20:55