跳到主要内容

Redux-Tookit

官网:https://redux-toolkit.js.org

实现 TodoList

技术栈:vite + typescript + redux-toolkit + tailwindcss

项目目录:

src
- components
- TodoList.tsx
- TodoItem.tsx
- store
- index.ts
- todoSlice.ts
- types
- todo.ts
- App.tsx
- index.css
- main.tsx

安装依赖:

pnpm add @reduxjs/toolkit react-redux

1、定义 store 和 reducer

store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice.ts';

export const store = configureStore({
reducer: {
todos: todoReducer
}
});

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

2、创建状态切片

创建切片需要一个名称来标识切片、一个初始状态值以及一个或多个 Reducer 函数来定义如何更新状态。

store/todoSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Todo } from '../types/todo.ts';

interface TodoState {
todos: Todo[];
}

const initialState: TodoState = {
todos: []
};

const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.todos.push({
id: Date.now().toString(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.todos.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action: PayloadAction<string>) => {
state.todos = state.todos.filter(todo => todo.id !== action.payload);
}
}
});

export const { addTodo, toggleTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;

定义通用的类型:

types/todo.ts
export interface Todo {
id: string;
text: string;
completed: boolean;
}

3、给 react 提供 store

使用<Provider>,并将 store 作为 props 传递:

main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { Provider } from 'react-redux';
import { store } from './store/index';

ReactDOM.createRoot(document.getElementById('root')!).render(
// <React.StrictMode>
<Provider store={store}>
<App />
</Provider>
// </React.StrictMode>
);

4、组件使用 store

  • useSelector:用于从 store 中获取数据
  • useDispatch:用于分发 action
components/TodoList.tsx
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Plus } from 'lucide-react';
import { RootState } from '../store/index.ts';
import { addTodo } from '../store/todoSlice.ts';
import TodoItem from './TodoItem.tsx';

const TodoList = () => {
const [newTodo, setNewTodo] = useState('');
const todos = useSelector((state: RootState) => state.todos.todos);
const dispatch = useDispatch();

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (newTodo.trim()) {
dispatch(addTodo(newTodo.trim()));
setNewTodo('');
}
};

return (
<div className="max-w-lg mx-auto p-6">
<h1 className="text-3xl font-bold text-gray-800 mb-8">Todo List</h1>
<form onSubmit={handleSubmit} className="mb-6">
<div className="flex gap-2">
<input
type="text"
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="添加一个新的 todo"
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center gap-2"
>
<Plus size={20} />
添加
</button>
</div>
</form>

<div className="space-y-2">
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>

<div className="mt-4 text-sm text-gray-500">总数: {todos.length}</div>
</div>
);
};

export default TodoList;
components/TodoItem.tsx
import React from 'react';
import { useDispatch } from 'react-redux';
import { Check, Trash2 } from 'lucide-react';
import { toggleTodo, deleteTodo } from '../store/todoSlice.ts';
import { Todo } from '../types/todo.ts';

interface TodoItemProps {
todo: Todo;
}

const TodoItem: React.FC<TodoItemProps> = ({ todo }) => {
const dispatch = useDispatch();

return (
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow-sm mb-2">
<div className="flex items-center gap-3">
<button
onClick={() => dispatch(toggleTodo(todo.id))}
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center
${todo.completed ? 'bg-green-500 border-green-500' : 'border-gray-300'}`}
>
{todo.completed && <Check size={14} className="text-white" />}
</button>
<span className={`${todo.completed ? 'line-through text-gray-500' : 'text-gray-700'}`}>{todo.text}</span>
</div>
<button
onClick={() => dispatch(deleteTodo(todo.id))}
className="text-gray-500 hover:text-red-500 transition-colors"
>
<Trash2 size={18} />
</button>
</div>
);
};

export default TodoItem;

在 index.css 中添加 tailwindcss 样式:

@import 'tailwindcss';

在 App.tsx 中使用 TodoList 组件:

App.tsx
import TodoList from './components/TodoList';

function App() {
return (
<div className="min-h-screen bg-gray-100 py-8">
<TodoList />
</div>
);
}

export default App;

持久化

redux-persist