Use new createSlice to replace reducer

master
Jason Zhu 2023-03-02 21:27:34 +11:00
parent 3ac4494157
commit 0b0c7d0f46
10 changed files with 117 additions and 139 deletions

View File

@ -3,7 +3,7 @@ import { createStore } from "redux";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import App from './App'; import App from './App';
import {todoReducer} from "../state/todo/reducers"; import todoReducer from "../features/todo/todosSlice";
test('renders learn react link', () => { test('renders learn react link', () => {
const store = createStore(todoReducer); const store = createStore(todoReducer);

View File

@ -2,23 +2,17 @@ import React, {useState} from 'react';
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import Todo from "../components/todo/Todo"; import Todo from "../components/todo/Todo";
import { TodoItem } from "../state/todo/types"; import { addTodo } from "../features/todo/todosSlice";
import { ADD_TODO } from "../state/todo/reducers"; import { RootState } from "./store";
function App() { function App() {
const [text, setText] = useState(""); const [text, setText] = useState("");
const todos = useSelector((state: { todos: TodoItem[] }) => state.todos); const todos = useSelector((state: RootState) => state.todos.todos);
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleAddTodo = () => { const handleAddTodo = () => {
if (text.trim() === "") return; if (text.trim() === "") return;
const newTodo: TodoItem = { dispatch(addTodo(text));
id: todos.length + 1,
text: text,
completed: false,
};
dispatch({ type: ADD_TODO, payload: newTodo});
setText(""); setText("");
} }
@ -33,12 +27,7 @@ function App() {
<button onClick={handleAddTodo}>Add Todo</button> <button onClick={handleAddTodo}>Add Todo</button>
<ul> <ul>
{todos.map((todo) => ( {todos.map((todo) => (
<Todo <Todo key={todo.id} todo={todo}/>
key={todo.id}
id={todo.id}
text={todo.text}
completed={todo.completed}
/>
))} ))}
</ul> </ul>
</div> </div>

11
src/app/store.ts 100644
View File

@ -0,0 +1,11 @@
import { configureStore } from "@reduxjs/toolkit";
import todosReducer from '../features/todo/todosSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;

View File

@ -1,53 +1,56 @@
import React from "react"; import React from "react";
import {screen, render, fireEvent, getByRole} from "@testing-library/react" import {render, fireEvent} from "@testing-library/react"
import { createStore } from "redux";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import Todo from "./Todo"; import Todo from "./Todo";
import {todoReducer} from "../../state/todo/reducers"; import todoReducer from '../../features/todo/todosSlice'
import {store} from "../../state/store"; import {configureStore} from "@reduxjs/toolkit";
const renderWithRedux = (
ui: React.ReactElement,
{ store = configureStore({ reducer: { todos: todoReducer } }) } = {}
) => {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
};
};
describe("Todo", () => { describe("Todo", () => {
it("renders todo text and toggle button", () => {
const props = { const todo = {
id: 1, id: 1,
text: "Buy milk", text: "Test todo",
completed: false, completed: false,
}; };
const { getByText, getByRole } = renderWithRedux(<Todo todo={todo} />);
it("should render todo text", () => { const todoText = getByText("Test todo");
render( expect(todoText).toBeInTheDocument();
<Provider store={store}> const toggleButton = getByRole("checkbox");
<Todo {...props}/> expect(toggleButton).toBeInTheDocument();
</Provider>
)
expect(screen.getByText(props.text)).toBeInTheDocument();
}); });
it("should call onToggle when checkbox is clicked", () => { it("toggles todo when toggle button is clicked", () => {
render( const todo = {
<Provider store={store}> id: 1,
<Todo {...props} /> text: "Test todo",
</Provider> completed: false,
) };
fireEvent.click(screen.getByRole("checkbox")); const { getByRole, store } = renderWithRedux(<Todo todo={todo} />);
const toggleButton = getByRole("checkbox");
fireEvent.click(toggleButton);
expect(store.getState().todos.todos[0].completed).toBe(true);
}); });
it("should call onDelete when delete button is clicked", () => { it("deletes todo when delete button is clicked", () => {
render( const todo = {
<Provider store={store}> id: 1,
<Todo {...props} /> text: "Test todo",
</Provider> completed: false,
) };
fireEvent.click(screen.getByRole("button")); const { getByRole, store } = renderWithRedux(<Todo todo={todo} />);
}) const deleteButton = getByRole("button");
fireEvent.click(deleteButton);
expect(store.getState().todos.todos).toHaveLength(0);
});
it("should have line-through style when completed is true", () => {
render(
<Provider store={store}>
<Todo {...props} completed={true} />
</Provider>
)
expect(screen.getByText(props.text)).toHaveStyle("text-decoration: line-through");
})
}) })

View File

@ -1,28 +1,26 @@
import React from "react"; import React from "react";
import {useDispatch} from "react-redux"; import {useDispatch} from "react-redux";
import { DELETE_TODO, TOGGLE_TODO } from "../../state/todo/reducers"; import { toggleTodo, deleteTodo, TodoItem } from "../../features/todo/todosSlice";
interface TodoProps { interface TodoProps {
id: number; todo: TodoItem;
text: string;
completed: boolean;
} }
const Todo = ( { id, text, completed }: TodoProps) => { const Todo = ( { todo }: TodoProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleToggle = () => { const handleToggle = () => {
dispatch({ type: TOGGLE_TODO, payload: id }); dispatch(toggleTodo(todo.id));
}; };
const handleDelete = () => { const handleDelete = () => {
dispatch({ type: DELETE_TODO, payload: id }); dispatch(deleteTodo(todo.id));
}; };
return ( return (
<li> <li>
<input type="checkbox" checked={completed} onChange={handleToggle} /> <input type="checkbox" checked={todo.completed} onChange={handleToggle} />
<span style={{ textDecoration: completed ? "line-through" : "none"}}> <span style={{ textDecoration: todo.completed ? "line-through" : "none"}}>
{text} {todo.text}
</span> </span>
<button onClick={handleDelete}>Delete</button> <button onClick={handleDelete}>Delete</button>
</li> </li>

View File

@ -0,0 +1,46 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: TodoItem[];
}
const initialState: TodoState = {
todos: []
}
export const todosSlice = createSlice({
name: 'todo',
initialState,
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<TodoItem>) => {
state.todos.push(action.payload);
},
prepare: (text: string) => ({
payload: {
id: new Date().getTime(),
text,
completed: false,
},
}),
},
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.todos.find((todo) => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action: PayloadAction<number>) => {
state.todos = state.todos.filter((todo) => todo.id !== action.payload);
},
},
})
export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "./state/store";
import { store } from "./app/store";
import App from './app/App'; import App from './app/App';
ReactDOM.render( ReactDOM.render(

View File

@ -1,5 +0,0 @@
import { createStore } from "redux";
import {todoReducer} from "./todo/reducers";
export const store = createStore(todoReducer);

View File

@ -1,60 +0,0 @@
import { TodoItem } from "./types";
interface TodoState {
todos: TodoItem[];
}
const initialState: TodoState = {
todos: [],
}
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const DELETE_TODO = "DELETE_TODO";
interface AddTodoAction {
type: typeof ADD_TODO;
payload: TodoItem;
}
interface ToggleTodoAction {
type: typeof TOGGLE_TODO;
payload: number;
}
interface DeleteTodoAction {
type: typeof DELETE_TODO;
payload: number;
}
type TodoActionTypes = AddTodoAction | ToggleTodoAction | DeleteTodoAction;
export function todoReducer(
state = initialState,
action: TodoActionTypes
): TodoState {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload],
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map((todo) => {
if (todo.id === action.payload) {
return { ...todo, completed: !todo.completed };
}
return todo;
}),
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
default:
return state;
}
}

View File

@ -1,5 +0,0 @@
export interface TodoItem {
id: number;
text: string;
completed: boolean;
}