Привет, ребята, в сегодняшнем посте мы собираемся создать приложение todo с операциями CRUD, используя React, Redux-Toolkit и Typescript. Как только мы завершим создание приложения, мы также разместим его на netlify, чтобы поделиться им с другими. И я также собираюсь бросить вызов вам, ребята, в конце поста. Итак, начнем.


1. Создайте папку

mkdir todo-crud
cd todo-crud

// this creates typescript app with all necessary configuration
npx create-react-app todo --template typescript
cd todo
npm i react-icons react-redux redux-persist @reduxjs/toolkit uuid
code .
Войти в полноэкранный режим

Выйти из полноэкранного режима


2. Очистите ненужные папки для нашего приложения.

Только следующие файлы должны оставаться в папке src. После того, как вы удалили все остальные файлы, также удалите их везде, где они используются.

  1. index.tsx
  2. App.tsx
  3. index.css


3. Создайте папку компонентов, резервную копию и файлы.

src
  /components
    /AddTodo
      AddTodo.tsx
    /EditTodo
      EditTodo.tsx
    /FilterTodo
      FilterTodo.tsx
    /TodoList
      /TodoItem
        TodoItem.tsx
      TodoList.tsx
  /redux
    todo.ts
    store.ts
Войти в полноэкранный режим

Выйти из полноэкранного режима


4. Заполните файлы папки компонентов функциональными компонентами.

В каждом созданном файле в папке компонентов на предыдущем шаге введите «rafce» или создайте функциональный компонент вручную.

// Example
const AddTodo = ()=>{
  return <div>AddTodo</div>
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


5. Импортируйте их все в файл приложения и вставьте следующий код.

В этом компоненте и во всех других компонентах мы собираемся добавить соответствующие имена классов, но мы будем стилизовать наши компоненты только после того, как вся логика написана и приложение работает без ошибок.

import React from "react";
import AddTodo from "./components/AddTodo/AddTodo";
import EditTodo from "./components/EditTodo/EditTodo";
import FilterTodo from "./components/FilterTodo/FilterTodo";
import TodoList from "./components/TodoList/TodoList";

// define the shape of the todo object and export it so that it'd be reused
export interface TodoInterface {
  id: string;
  task: string;
  completed: boolean;
}

const App = () => {
  const [editTodo, setEditTodo] = useState<TodoInterface | null>(null);


  return (
    <main className="app">
      <div className="app__wrapper">
        <div className="app__header">
          <h1 className="app__title">Todo App</h1>
        </div>
        <div className="app__inputs-box">
// display edit todo when todo is being edited else display add todo form
          {editTodo?.id ? <EditTodo /> : <AddTodo />}
          <FilterTodo/>
        </div>
        <TodoList/>
      </div>
    </main>
  );
};

export default App;
Войти в полноэкранный режим

Выйти из полноэкранного режима


6. Введите команду ниже

Введите npm start, он должен открыть новое окно браузера с пользовательским интерфейсом, который мы создали до сих пор.

npm start
Войти в полноэкранный режим

Выйти из полноэкранного режима

Он должен отображать следующий пользовательский интерфейс, если все работает на вашей стороне.
Описание изображения


7. Создайте редьюсер, вставив следующий код в файл «src/redux/todo.ts».

Теперь мы создаем редьюсер для обработки состояния нашего приложения.

import {createSlice} from "@reduxjs/toolkit";
import {TodoInterface} from "../App";

// shape of todos array
interface TodosListInterface {
    todos: TodoInterface[]
}


// initial todos state
const initialState: TodosListInterface = {
    todos: []
}


// todo slice with intial state and reducers to mutate state. They perform CRUD and also toggle todo. Redux-Toolkit uses Immutable.js which allows us to mutate state but on the background everything works as immutated state.
export const todoSlice = createSlice({
    name: "todo",
    initialState,
    reducers: {
        addTodo: (state, {payload: {task, id, completed}})=>{       
            state.todos.push({id, task, completed})
        },
        deleteTodo: (state, {payload: {todoId}})=>{
            state.todos = state.todos.filter(todo=> todo.id !== todoId)
        },
        editTodo: (state, {payload: {editedTodo}})=>{
            console.log(editedTodo)
            state.todos = state.todos.map(todo => todo.id === editedTodo.id ? editedTodo : todo);
        },
        toggleTodo: (state, {payload: {todoId}})=>{
            state.todos = state.todos.map(todo => todo.id === todoId ? {...todo, completed: !todo.completed} : todo);
        },
    }
})

// actions for telling reducer what to do with state, they can also include payload for changing state
export const {addTodo, deleteTodo, editTodo, toggleTodo} = todoSlice.actions;

// reducer to change the state
export default todoSlice.reducer;
Войти в полноэкранный режим

Выйти из полноэкранного режима


8. Настройте хранилище с помощью редуктора и вставьте приведенный ниже код.

По сути, мы используем redux-persist для сохранения состояния в локальном хранилище. redux-persist имеет такие параметры, как сохранение только разрешенных состояний и не сохранение неразрешенных состояний. вы можете посмотреть на это в здесь

import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import todoReducer from "./todo";


const persistConfig = {
  key: 'root',
  storage,
}

const persistedReducer = persistReducer(persistConfig, todoReducer)

// we are persisting todos on the local storage
export const store = configureStore({
  reducer: {
    todos: persistedReducer
  },
})

const persistor = persistStore(store)

export {persistor};
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Войти в полноэкранный режим

Выйти из полноэкранного режима


9. Откройте файл index.tsx и вставьте следующий код.

В этом файле мы соединяем реакцию и редукцию, и мы предоставляем все дерево состояний объектов как глобальное состояние и сохраняем указанное состояние в файле store.ts в локальном хранилище.

import React from "react";
import ReactDOM from "react-dom/client";
import { PersistGate } from "redux-persist/integration/react";
import { persistor } from "./redux/store";
import "./index.css";
import App from "./App";
import { store } from "./redux/store";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
// we are providing state as global and persisting specified state
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);
Войти в полноэкранный режим

Выйти из полноэкранного режима


10. Заполните файл App.tsx следующим кодом.

import React, { useState } from "react";
import { useSelector } from "react-redux";
import type { RootState } from "./redux/store";
import AddTodo from "./components/AddTodo/AddTodo";
import EditTodo from "./components/EditTodo/EditTodo";
import FilterTodo from "./components/FilterTodo/FilterTodo";
import TodoList from "./components/TodoList/TodoList";

export interface TodoInterface {
  id: string;
  task: string;
  completed: boolean;
}

const App = () => {
// here we are subsribed to todos state and read it on each time it changes
  const todos = useSelector((state: RootState) => state.todos.todos);
// editTodo used to get todo that to be edited
  const [editTodo, setEditTodo] = useState<TodoInterface | null>(null);
// todoFilterValue is used to filter out todos on select
  const [todoFilterValue, setTodoFilterValue] = useState("all");


// gets filterValue from select and sets it in the state
  const getTodoFilterValue = (filterValue: string) =>
    setTodoFilterValue(filterValue);

// gets todo that to be edited and sets it in the state
  const getEditTodo = (editTodo: TodoInterface) => setEditTodo(editTodo);


  return (
    <main className="app">
      <div className="app__wrapper">
        <div className="app__header">
          <h1 className="app__title">Todo App</h1>
        </div>
        <div className="app__inputs-box">
          {editTodo?.id ? (
            <EditTodo editTodo={editTodo} setEditTodo={setEditTodo} />
          ) : (
            <AddTodo />
          )}
          <FilterTodo getTodoFilterValue={getTodoFilterValue} />
        </div>
        <TodoList
          todos={todos}
          todoFilterValue={todoFilterValue}
          getEditTodo={getEditTodo}
          setEditTodo={setEditTodo}
          editTodo={editTodo}
        />
      </div>
    </main>
  );
};
export default App;
Войти в полноэкранный режим

Выйти из полноэкранного режима


11. Откройте компонент Todolist и установите типы для каждого переданного ему свойства.

import React from "react";
import TodoItem from "./TodoItem/TodoItem";
import { TodoInterface } from "../../App";

type TodoListProps = {
  todos: TodoInterface[];
  todoFilterValue: string;
  getEditTodo: (editTodo: TodoInterface) => void;
  setEditTodo: (editTodo: TodoInterface) => void;
  editTodo: TodoInterface | null;
};

const TodoList = ({
  todos,
  todoFilterValue,
  editTodo,
  getEditTodo,
  setEditTodo,
}: TodoListProps) => {
  return (
    <ul className="todo-list">
      {todos
        .filter((todo) => (todoFilterValue === "all" ? true : todo.completed))
        .map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
            editTodo={editTodo}
            getEditTodo={getEditTodo}
            setEditTodo={setEditTodo}
          />
        ))}
    </ul>
  );
};

export default TodoList;

Войти в полноэкранный режим

Выйти из полноэкранного режима


12. Откройте компонент TodoItem и установите типы для каждого переданного ему свойства.

import React from "react";
import { TodoInterface } from "../../../App";

type TodoItemProps = {
  todo: TodoInterface;
  editTodo: TodoInterface | null;
  getEditTodo: (editTodo: TodoInterface) => void;
  setEditTodo: (editTodo: TodoInterface) => void;
};

const TodoItem = ({
  todo,
  editTodo,
  getEditTodo,
  setEditTodo,
}: TodoItemProps) => {
  return <li>TodoItem</li>
};

export default TodoItem;

Войти в полноэкранный режим

Выйти из полноэкранного режима


13. Откройте компонент FilterTodo и установите типы для каждого свойства.

import React from "react";

type FilterTodoProps = {
  getTodoFilterValue: (filterValue: string) => void;
};

const FilterTodo = ({ getTodoFilterValue }: FilterTodoProps) => {
  return <div>FilterTodo</div>;
};

export default FilterTodo;

Войти в полноэкранный режим

Выйти из полноэкранного режима


14. Откройте компонент EditTodo и установите типы для каждого свойства.

import React from "react";
import { TodoInterface } from "../../App";

type EditTodoProps = {
  editTodo: TodoInterface;
  setEditTodo: (editTodo: TodoInterface);
};

const EditTodo = ({ editTodo, setEditTodo }: EditTodoProps) => {
  return <div>EditTodo</div>;
};

export default EditTodo;
Войти в полноэкранный режим

Выйти из полноэкранного режима


15. Добавьте форму в компонент AddTodo с логикой добавления todo

import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { addTodo } from "../../redux/todo";

const AddTodo = () => {
  const dispatch = useDispatch();
  const [task, setTask] = useState("");
  const [error, setError] = useState("");

/** 
this function prevents default behaviour page refresh on form submit and sets error to state if length of characters either less than 5 or greater than 50. 
Else if there'r no errors than it dispatches action to the reducer to add new task with unique id. And sets input to empty ""
*/
  const handleAddTaskSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (task.trim().length < 5) {
      setError("Minimum allowed task length is 5");
    } else if (task.trim().length > 50) {
      setError("Maximum allowed task length is 50");
    } else {
      dispatch(addTodo({ task, id: uuidv4(), completed: false }));
      setTask("");
    }
  };

/**
 this function removes error from the state if character length is greater than 5 and less than 50
*/
  const handleUpdateTodoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTask(e.target.value);
    if (task.trim().length > 5 && task.trim().length < 50) {
      setError("");
    }
  };

  return (
    <form onSubmit={handleAddTaskSubmit} className="form">
      <div className="form__control">
        <input
          onChange={handleUpdateTodoChange}
          value={task}
          type="text"
          className="form__input"
          placeholder="Add todo..."
        />
        {error && <p className="form__error-text">{error}</p>}
      </div>
      <button className="btn form__btn">Add Todo</button>
    </form>
  );
};

export default AddTodo;

Войти в полноэкранный режим

Выйти из полноэкранного режима


16. Тест: Добавить все новые

Если вы все сделали правильно, вы можете добавить новые задачи, и они останутся в магазине.
Добавить пример Todo
Описание изображения


17. Добавьте логику для удаления, переключения todo. А также возможность получать текущие задачи

Мы также включили значки, указывающие на удаление и редактирование задач. Теперь сосредоточьтесь только на handleDeleteTodoClick, который отправляет действие для удаления задачи по его идентификатору. Вскоре будут использованы другие обработчики. Теперь он должен быть в состоянии удалить todo.

import React from "react";
import { MdModeEditOutline } from "react-icons/md";
import { FaTrashAlt } from "react-icons/fa";
import { useDispatch } from "react-redux";
import { deleteTodo, toggleTodo } from "../../../redux/todo";
import { TodoInterface } from "../../../App";

type TodoItemProps = {
  todo: TodoInterface;
  editTodo: TodoInterface | null;
  getEditTodo: (editTodo: TodoInterface) => void;
  setEditTodo: (editTodo: TodoInterface) => void;
};

const TodoItem = ({
  todo,
  editTodo,
  getEditTodo,
  setEditTodo,
}: TodoItemProps) => {
  const dispatch = useDispatch();

// This event handler toggles checkbox on and off
const handleToggleTodoChange = () =>
    dispatch(toggleTodo({ todoId: todo.id }));

/** This event handler deletes current todo on delete button click
It also resets editTodo state if it's deleted
*/
  const handleDeleteTodoClick = () => {
    dispatch(deleteTodo({ todoId: todo.id }));
    if (todo.id === editTodo?.id) {
      setEditTodo({ id: "", task: "", completed: false });
    }
  };

// This event handler gets current todo which is to be edited
  const handleGetEditTodoClick = () => getEditTodo(todo);


  return (
    <li className="todo-list__item">
      <label
        htmlFor={todo.id}
        style={
          todo.completed
            ? { textDecoration: "line-through" }
            : { textDecoration: "none" }
        }
        className="todo-list__label"
      >
        <input
          onChange={handleToggleTodoChange}
          checked={todo.completed ? true : false}
          type="checkbox"
          id={todo.id}
          className="todo-list__checkbox"
        />
        {todo.task}
      </label>
      <div className="todo-list__btns-box">
        <button 
onClick={handleGetEditTodoClick}
className="todo-list__btn todo-list__edit-btn">
<MdModeEditOutline />
</button>
        <button
          onClick={handleDeleteTodoClick}
          className="todo-list__btn todo-list__delete-btn"
        >
<FaTrashAlt />
</button>
      </div>
    </li>
  );
};

export default TodoItem;

Войти в полноэкранный режим

Выйти из полноэкранного режима


18. Откройте компонент EditTodo, чтобы добавить логику редактирования todo.

Он очень похож на компонент AddTodo, но мы используем useEffect. Как только вы вставите код, проверьте его в браузере. Если все работает правильно, то сейчас можно будет редактировать todo.

import React, { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { editTodo as updateTodo } from "../../redux/todo";
import { TodoInterface } from "../../App";

type EditTodoProps = {
  editTodo: TodoInterface;
};

const EditTodo = ({ editTodo }: EditTodoProps) => {
  const dispatch = useDispatch();
  const [task, setTask] = useState("");
  const [error, setError] = useState("");

// effect hook is going to set new task on each time user click todo edit button
  useEffect(() => {
    setTask(editTodo.task);
  }, [editTodo]);

// This event handler dispatches action to update edited todo and resets editTodo state so that form switches from edit todo to add todo 
  const handleEditTaskSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (task.trim().length < 5) {
      setError("Minimum allowed task length is 5");
    } else if (task.trim().length > 50) {
      setError("Maximum allowed task length is 50");
    } else {
      dispatch(updateTodo({ editedTodo: { ...editTodo, task } }));
      setEditTodo({ id: "", task: "", completed: false });
      setTask("");
    }
  };

// this event handler removes error if character length greater than 5 and less than 50
  const handleUpdateTodoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTask(e.target.value);
    if (task.trim().length > 5 && task.trim().length < 50) {
      setError("");
    }
  };

  console.log(editTodo);
  return (
    <form onSubmit={handleEditTaskSubmit} className="form">
      <div className="form__control">
        <input
          onChange={handleUpdateTodoChange}
          value={task}
          type="text"
          className="form__input"
          placeholder="Edit todo..."
        />
        {error && <p className="form__error-text">{error}</p>}
      </div>
      <button className="btn form__btn">Edit Todo</button>
    </form>
  );
};

export default EditTodo;

Войти в полноэкранный режим

Выйти из полноэкранного режима


19. Добавьте функциональность фильтра todo

Откройте компонент FilterTodo и добавьте следующий код, чтобы включить фильтр для выполнения функций. Поскольку мы уже добавили логику в компонент TodoList, мы должны иметь возможность фильтровать задачи на основе завершенных и незавершенных. Теперь идите и проверьте, добавив 2 завершенных и 2 незавершенных задачи, и отфильтруйте выбор для всех и завершенных. Все должно отображать все 4, а завершенное должно отображать только 2 элемента.

import React, { useState } from "react";

type FilterTodoProps = {
  getTodoFilterValue: (filterValue: string) => void;
};

const FilterTodo = ({ getTodoFilterValue }: FilterTodoProps) => {
  const [filterTodoVal, setFilterTodoVal] = useState("all");

// This event handler updates current select option and passes currently selected option value to App component  
const handleFilterTodoChanges = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setFilterTodoVal(e.target.value);
    getTodoFilterValue(e.target.value);
  };
  return (
    <select
      onChange={handleFilterTodoChanges}
      value={filterTodoVal}
      className="filter-todo"
    >
      <option value="all">All</option>
      <option value="completed">Completed</option>
    </select>
  );
};

export default FilterTodo;

Войти в полноэкранный режим

Выйти из полноэкранного режима


20. Примените стиль.

Наконец мы закончили создание приложения. Теперь включите следующий код в index.css, и после этого ваше приложение должно выглядеть так.
Описание изображения


21. Хостинг

  1. Войдите в github и создайте новый репозиторий
  2. В вашем терминале введите следующее в каталоге «todo»
    1. git добавить .
    2. git commit -m «init: начальная фиксация»
    3. Добавьте удаленный источник созданного вами репозитория, который выглядит, например, так: git remote add origin
    4. git push -u мастер происхождения
  3. Это отправляет код реакции на Github, который вы должны увидеть в своей учетной записи github.
  4. Войдите в Netlify, если у вас есть еще один, создайте новую учетную запись, что очень просто сделать.
  5. Делайте подписки
    1. Нажмите кнопку «Добавить новый сайт», чтобы открыть меню с 3 вариантами.
    2. Выберите опцию «Импортировать существующий проект».
    3. Подключите Netlify к своей учетной записи Github, нажав кнопку Github.
    4. Щелкните над репозиторием todo, который вы создали.
    5. Введите «CI = npm run build» в поле ввода команды «Сборка».
    6. Нажмите кнопку «Развернуть сайт», которая развернет веб-сайт. Это займет несколько минут. Если все идет хорошо, приложение размещается в Интернете.
    7. Это в прямом эфире сейчас. Поделись с друзьями, а еще лучше сделай лучше


22. Бонус: вызов

  1. Добавьте пагинацию с помощью реагировать-разбивать на страницы. Должен быть выбран для отображения 5/10/20/50 задач на странице.
  2. Добавьте дату начала и дату окончания для каждой задачи, и она должна быть оформлена по-разному, если задача просрочена.
  3. Добавить опцию поиска дел
  4. Если вы являетесь разработчиком пользовательского стека, добавьте аутентификацию: зарегистрируйтесь, войдите в систему и выполните вход через социальные сети с помощью google, linkedin, facebook, чтобы ea. Todos нельзя передавать другим пользователям, прошедшим проверку подлинности.



Исходный код


Резюме

Так что спасибо всем, кто завершил проект со мной, я уверен, что вы получили некоторые полезные знания, выполнив это руководство. Если вы амбициозны и завершили проект, пожалуйста, поделитесь им со мной, я хотел бы увидеть, что вы сделали, чтобы сделать его лучше.