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

如何用TypeScript不可变更新Redux状态数组?TodoList开发问题

TypeScript + Redux TodoList 状态更新问题

我正在开发简易TodoList应用学习TypeScript,目前遇到Redux Slice中无法正确不可变更新状态数组的问题。以下是我的代码和报错信息:

主组件 Todolist.tsx

import React, { FC, ChangeEvent, FormEvent, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { update } from '../todoSlice';
import { RootState } from '../todoStore';

const TodoList: FC = () => {

    // React state array for the todos
    const [todos, setTodos] = useState<string[]>([]);
    // Get the input from this state
    const [input, changeInput] = useState<string>('');

    const dispatch = useDispatch()
    // Selecting the state of the todoSlice component
    const selector = useSelector((state: RootState) => { return state.todoCard })

    const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
        event.preventDefault();
        changeInput(event.target.value);
    }
    
    const addTodos = (event: FormEvent<HTMLFormElement>): void => {
        event.preventDefault();
        setTodos([...todos, input]);
        dispatch(update(todos));
        console.log("REACT: ", todos);
        console.log("REDUX: ", selector);
    }

    const removeTodos = (index: number): void => {
        let filteredArray = todos.filter((todo, todoIndex) => (
            todoIndex !== index
        ))
        setTodos(filteredArray);
    }

    return (
    <React.Fragment>
    <div className='bg-light d-inline-block text-center mt-4' style={{ padding: '10vh', marginLeft: '4vw' }}>
      <h1 className='mb-4'>Todo List</h1>
      <section className='text-wrap mb-4' style={{ overflowY: 'auto', height: '10rem' }}>
        {
          // Mapping thee todo-list items
          todos.map((todo, index) => (
            <React.Fragment key={index}>
              <div className='d-flex justify-content-between mb-3'>
                <li className='d-block'>{todo}</li>
                <button onClick={() => {removeTodos(index)}}>X</button>
              </div>
            </React.Fragment>
          ))
        }
      </section>
      <form onSubmit={addTodos}>
        <input type='text' name="input" className='p-1 rounded border-success' onChange={handleChange}></input>
        <button type="submit" className='ms-3 btn btn-success mb-1'>Enter</button>
      </form>
    </div>
    </React.Fragment>
    )
}

export default TodoList

Redux Slice todoSlice.ts

import { createSlice } from '@reduxjs/toolkit'

export const todoSlice = createSlice({
    name: 'todo',
    initialState: {
        todoCard: [],
    },
    reducers: {
        update: (state, action) => {
            return {
                // Return a copy of the array and SHOULD update the state array here "immutably"
                ...state,
                todoCard: [
                    ...state.todoCard,
                    action.payload
                ]
            }
        }
    }
})

export const { update } = todoSlice.actions

export default todoSlice.reducer

报错信息

TypeScript类型错误:

Type 'string[][]' is not assignable to type 'string[]'.
  Type 'string[]' is not assignable to type 'string'.

问题根源与解决方案

1. 传递错误的payload到Redux

你在addTodos中dispatch(update(todos))传递了整个todos数组,而非新添加的单个input值,导致Redux的todoCard变成数组嵌套数组,触发类型不匹配错误。

2. 冗余维护React状态与Redux状态

同时维护React本地状态和Redux状态会导致数据不一致,应以Redux作为单一数据源。

3. 未利用Redux Toolkit的Immer能力

Redux Toolkit内置Immer,可以直接"修改"状态,无需手动编写复杂的不可变更新逻辑。


修正后的代码

修正 todoSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// 明确定义状态类型
interface TodoState {
  todoCard: string[]
}

const initialState: TodoState = {
  todoCard: [],
}

export const todoSlice = createSlice({
    name: 'todo',
    initialState,
    reducers: {
        // 指定payload为单个字符串类型
        addTodo: (state, action: PayloadAction<string>) => {
            // Immer自动处理不可变更新,直接push即可
            state.todoCard.push(action.payload);
        },
        // 添加删除todo的reducer
        removeTodo: (state, action: PayloadAction<number>) => {
            state.todoCard = state.todoCard.filter((_, index) => index !== action.payload);
        }
    }
})

export const { addTodo, removeTodo } = todoSlice.actions

export default todoSlice.reducer

修正 Todolist.tsx

import React, { FC, ChangeEvent, FormEvent, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addTodo, removeTodo } from '../todoSlice';
import { RootState } from '../todoStore';

const TodoList: FC = () => {
    // 直接从Redux获取todos,不再维护本地状态
    const todos = useSelector((state: RootState) => state.todo.todoCard);
    const [input, changeInput] = useState<string>('');

    const dispatch = useDispatch()

    const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
        changeInput(event.target.value);
    }
    
    const addTodos = (event: FormEvent<HTMLFormElement>): void => {
        event.preventDefault();
        if (!input.trim()) return; // 过滤空输入
        // 传递单个input值到Redux
        dispatch(addTodo(input));
        changeInput(''); // 清空输入框
    }

    return (
    <React.Fragment>
    <div className='bg-light d-inline-block text-center mt-4' style={{ padding: '10vh', marginLeft: '4vw' }}>
      <h1 className='mb-4'>Todo List</h1>
      <section className='text-wrap mb-4' style={{ overflowY: 'auto', height: '10rem' }}>
        {
          todos.map((todo, index) => (
            <React.Fragment key={index}>
              <div className='d-flex justify-content-between mb-3'>
                <li className='d-block'>{todo}</li>
                {/* 直接dispatch删除动作 */}
                <button onClick={() => dispatch(removeTodo(index))}>X</button>
              </div>
            </React.Fragment>
          ))
        }
      </section>
      <form onSubmit={addTodos}>
        <input type='text' name="input" className='p-1 rounded border-success' onChange={handleChange} value={input}></input>
        <button type="submit" className='ms-3 btn btn-success mb-1'>Enter</button>
      </form>
    </div>
    </React.Fragment>
    )
}

export default TodoList

补充说明

确保你的RootState类型正确,包含todo切片:

// todoStore.ts
import { configureStore } from '@reduxjs/toolkit'
import todoReducer from './todoSlice'

export const store = configureStore({
  reducer: {
    todo: todoReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 13:10:24