Use new createSlice to replace reducer
parent
3ac4494157
commit
0b0c7d0f46
|
@ -3,7 +3,7 @@ import { createStore } from "redux";
|
|||
import { Provider } from "react-redux";
|
||||
|
||||
import App from './App';
|
||||
import {todoReducer} from "../state/todo/reducers";
|
||||
import todoReducer from "../features/todo/todosSlice";
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const store = createStore(todoReducer);
|
||||
|
|
|
@ -2,23 +2,17 @@ import React, {useState} from 'react';
|
|||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import Todo from "../components/todo/Todo";
|
||||
import { TodoItem } from "../state/todo/types";
|
||||
import { ADD_TODO } from "../state/todo/reducers";
|
||||
import { addTodo } from "../features/todo/todosSlice";
|
||||
import { RootState } from "./store";
|
||||
|
||||
function App() {
|
||||
const [text, setText] = useState("");
|
||||
const todos = useSelector((state: { todos: TodoItem[] }) => state.todos);
|
||||
const todos = useSelector((state: RootState) => state.todos.todos);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleAddTodo = () => {
|
||||
if (text.trim() === "") return;
|
||||
const newTodo: TodoItem = {
|
||||
id: todos.length + 1,
|
||||
text: text,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
dispatch({ type: ADD_TODO, payload: newTodo});
|
||||
dispatch(addTodo(text));
|
||||
setText("");
|
||||
}
|
||||
|
||||
|
@ -33,12 +27,7 @@ function App() {
|
|||
<button onClick={handleAddTodo}>Add Todo</button>
|
||||
<ul>
|
||||
{todos.map((todo) => (
|
||||
<Todo
|
||||
key={todo.id}
|
||||
id={todo.id}
|
||||
text={todo.text}
|
||||
completed={todo.completed}
|
||||
/>
|
||||
<Todo key={todo.id} todo={todo}/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -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>;
|
|
@ -1,53 +1,56 @@
|
|||
import React from "react";
|
||||
import {screen, render, fireEvent, getByRole} from "@testing-library/react"
|
||||
import { createStore } from "redux";
|
||||
import {render, fireEvent} from "@testing-library/react"
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import Todo from "./Todo";
|
||||
import {todoReducer} from "../../state/todo/reducers";
|
||||
import {store} from "../../state/store";
|
||||
import todoReducer from '../../features/todo/todosSlice'
|
||||
import {configureStore} from "@reduxjs/toolkit";
|
||||
|
||||
describe("Todo", () => {
|
||||
|
||||
const props = {
|
||||
id: 1,
|
||||
text: "Buy milk",
|
||||
completed: false,
|
||||
const renderWithRedux = (
|
||||
ui: React.ReactElement,
|
||||
{ store = configureStore({ reducer: { todos: todoReducer } }) } = {}
|
||||
) => {
|
||||
return {
|
||||
...render(<Provider store={store}>{ui}</Provider>),
|
||||
store,
|
||||
};
|
||||
|
||||
it("should render todo text", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Todo {...props}/>
|
||||
</Provider>
|
||||
)
|
||||
expect(screen.getByText(props.text)).toBeInTheDocument();
|
||||
};
|
||||
describe("Todo", () => {
|
||||
it("renders todo text and toggle button", () => {
|
||||
const todo = {
|
||||
id: 1,
|
||||
text: "Test todo",
|
||||
completed: false,
|
||||
};
|
||||
const { getByText, getByRole } = renderWithRedux(<Todo todo={todo} />);
|
||||
const todoText = getByText("Test todo");
|
||||
expect(todoText).toBeInTheDocument();
|
||||
const toggleButton = getByRole("checkbox");
|
||||
expect(toggleButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onToggle when checkbox is clicked", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Todo {...props} />
|
||||
</Provider>
|
||||
)
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
it("toggles todo when toggle button is clicked", () => {
|
||||
const todo = {
|
||||
id: 1,
|
||||
text: "Test todo",
|
||||
completed: false,
|
||||
};
|
||||
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", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Todo {...props} />
|
||||
</Provider>
|
||||
)
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
})
|
||||
it("deletes todo when delete button is clicked", () => {
|
||||
const todo = {
|
||||
id: 1,
|
||||
text: "Test todo",
|
||||
completed: false,
|
||||
};
|
||||
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");
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import React from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import { DELETE_TODO, TOGGLE_TODO } from "../../state/todo/reducers";
|
||||
import { toggleTodo, deleteTodo, TodoItem } from "../../features/todo/todosSlice";
|
||||
|
||||
interface TodoProps {
|
||||
id: number;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
todo: TodoItem;
|
||||
}
|
||||
|
||||
const Todo = ( { id, text, completed }: TodoProps) => {
|
||||
const Todo = ( { todo }: TodoProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleToggle = () => {
|
||||
dispatch({ type: TOGGLE_TODO, payload: id });
|
||||
dispatch(toggleTodo(todo.id));
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch({ type: DELETE_TODO, payload: id });
|
||||
dispatch(deleteTodo(todo.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<li>
|
||||
<input type="checkbox" checked={completed} onChange={handleToggle} />
|
||||
<span style={{ textDecoration: completed ? "line-through" : "none"}}>
|
||||
{text}
|
||||
<input type="checkbox" checked={todo.completed} onChange={handleToggle} />
|
||||
<span style={{ textDecoration: todo.completed ? "line-through" : "none"}}>
|
||||
{todo.text}
|
||||
</span>
|
||||
<button onClick={handleDelete}>Delete</button>
|
||||
</li>
|
||||
|
|
|
@ -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;
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./state/store";
|
||||
|
||||
import { store } from "./app/store";
|
||||
import App from './app/App';
|
||||
|
||||
ReactDOM.render(
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { createStore } from "redux";
|
||||
|
||||
import {todoReducer} from "./todo/reducers";
|
||||
|
||||
export const store = createStore(todoReducer);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export interface TodoItem {
|
||||
id: number;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
}
|
Loading…
Reference in New Issue