Use new createSlice to replace reducer
parent
3ac4494157
commit
0b0c7d0f46
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 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";
|
||||||
|
|
||||||
describe("Todo", () => {
|
const renderWithRedux = (
|
||||||
|
ui: React.ReactElement,
|
||||||
const props = {
|
{ store = configureStore({ reducer: { todos: todoReducer } }) } = {}
|
||||||
id: 1,
|
) => {
|
||||||
text: "Buy milk",
|
return {
|
||||||
completed: false,
|
...render(<Provider store={store}>{ui}</Provider>),
|
||||||
|
store,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
it("should render todo text", () => {
|
describe("Todo", () => {
|
||||||
render(
|
it("renders todo text and toggle button", () => {
|
||||||
<Provider store={store}>
|
const todo = {
|
||||||
<Todo {...props}/>
|
id: 1,
|
||||||
</Provider>
|
text: "Test todo",
|
||||||
)
|
completed: false,
|
||||||
expect(screen.getByText(props.text)).toBeInTheDocument();
|
};
|
||||||
|
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", () => {
|
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");
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 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(
|
||||||
|
|
|
@ -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