Идея заключается в развязывании, внедрении и изменении зависимостей в реакции с использованием контекста, сервисов и репозиториев.
(испанский) La idea es desacoplar y lograr una inyeccion e inversion de dependecias en react, usando contextos, servicios y repositorios.
Пойдем!
создать проект
yarn create react-app todo-app --template typescript yarn add styled-components @types/styled-components
очистить и создать папки:
src/services, src/repositories, src/contexts, src/providers, src/components, src/__tests__
Создание репозиториев и сервисов
источник/репозитории/todo.repository.ts
abstract class TodoRepository { abstract add(todo: string): void; abstract getAll(): string[]; abstract delete(todo: string): void; protected validateExistTodo(todos: string[], todo: string): boolean { const tempTodos = todos.map((todo) => todo.toLocaleLowerCase()); return tempTodos.includes(todo.toLocaleLowerCase()); } } export default TodoRepository;
источник/репозитории/todoInMemory.repository.ts
import TodoRepository from "./todo.repository"; class TodoInMemoryRepository extends TodoRepository { private todos: string[] = []; public add(todo: string) { if (!this.validateExistTodo(this.todos, todo)) { this.todos.unshift(todo); } } public delete(todo: string) { if (this.validateExistTodo(this.todos, todo)) { this.todos = this.todos.filter( (_todo) => _todo.toLocaleLowerCase() !== todo.toLocaleLowerCase() ); } } public getAll(): string[] { return this.todos; } } export default TodoInMemoryRepository;
src/repositories/todoInLocalStorage.repository.ts
import TodoRepository from "./todo.repository"; class TodoInLocalStorageRepository extends TodoRepository { private todos: string[] = []; private key: string = "todos"; public constructor() { super(); this.getFromLocalStorage(); } private getFromLocalStorage() { const tempTodos = window.localStorage.getItem(this.key); this.todos = tempTodos === null ? [] : JSON.parse(tempTodos); } private insertToLocalStorage() { window.localStorage.setItem(this.key, JSON.stringify(this.todos)); } public add(todo: string) { if (!this.validateExistTodo(this.todos, todo)) { this.todos.unshift(todo); this.insertToLocalStorage(); } } public delete(todo: string) { if (this.validateExistTodo(this.todos, todo)) { this.todos = this.todos.filter( (_todo) => _todo.toLocaleLowerCase() !== todo.toLocaleLowerCase() ); this.insertToLocalStorage(); } } public getAll(): string[] { return this.todos; } } export default TodoInLocalStorageRepository;
src/services/todo.service.ts
import TodoRepository from "../repositories/todo.repository"; class TodoService { private repository: TodoRepository; public constructor(repository: TodoRepository) { this.repository = repository; } public getAll(): string[] { return this.repository.getAll(); } public add(todo: string): void { this.repository.add(todo); } public delete(todo: string): void { this.repository.delete(todo); } } export default TodoService;
следите за тем, чтобы в конструкции мы внедряли абстрактный репозиторий, а не реализацию.
Создайте контекст
источник/контексты/todos.context.ts
import { createContext } from "react"; type TypeTodosContext = { todos: string[]; add: (todo: string) => void; delete: (todo: string) => void; }; const InitialTodosContext: TypeTodosContext = { todos: [], add: () => {}, delete: () => {}, }; const TodosContext = createContext<TypeTodosContext>(InitialTodosContext); export default TodosContext;
Напишите компоненты
Todo.tsx
import React, { useContext } from "react"; import styled from "styled-components"; import TodosContext from "../contexts/todos.context"; const TodoContainer = styled.div` display: flex; justify-content: space-between; padding: 20px; background-color: #f0f0f0; margin: 10px 0; `; type TodoProps = { name: string; }; const Todo: React.FC<TodoProps> = ({ name }): JSX.Element => { const ctx = useContext(TodosContext); const handlerClick = () => { ctx.delete(name); }; return ( <TodoContainer> <span>{name}</span> <button onClick={handlerClick}>❌</button> </TodoContainer> ); }; export default Todo;
TodoList.tsx
import React, { useContext } from "react"; import styled from "styled-components"; import TodosContext from "../contexts/todos.context"; import Todo from "./Todo"; const Ul = styled.ul` list-style: none; `; const TodoList: React.FC = (): JSX.Element => { const { todos } = useContext(TodosContext); return ( <Ul> {todos.map((todo: string, index: number) => ( <li key={index}> <Todo name={todo} /> </li> ))} </Ul> ); }; export default TodoList;
TodoForm.tsx
import React, { SyntheticEvent, useContext, useRef } from "react"; import styled from "styled-components"; import TodosContext from "../contexts/todos.context"; const Form = styled.form` display: flex; justify-content: center; `; const Input = styled.input` padding: 10px; &[type="submit"] { cursor: pointer; } `; const TodoForm = (): JSX.Element => { const inputText = useRef<HTMLInputElement>(null); const { add } = useContext(TodosContext); const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => { event.preventDefault(); if (inputText.current !== null) { add(inputText.current.value || ""); inputText.current.value = ""; } }; return ( <Form onSubmit={handleSubmit}> <Input ref={inputText} type="text" name="todo" placeholder="Add todo" required /> <Input type="submit" value="ADD" /> </Form> ); }; export default TodoForm;
TodoApp.tsx
import React from "react"; import styled from "styled-components"; import TodoForm from "./TodoForm"; import TodoList from "./TodoList"; const Div = styled.div` max-width: 800px; margin: 0 auto; `; const TodoApp = (): JSX.Element => { return ( <Div> <TodoForm /> <TodoList /> </Div> ); }; export default TodoApp;
Что ж, все компоненты у нас есть, теперь пишем провайдер для инжекта сервиса, репозиторий и определяем обработчики контекста.
Напишите поставщика
src/provides/todos.provider.tsx
import React, { ReactNode, useEffect, useState } from "react"; import TodosContext from "../contexts/todos.context"; import TodoService from "../services/todo.service"; type TypeTodoProvider = { service: TodoService; children?: ReactNode; }; const TodosProvider: React.FC<TypeTodoProvider> = ({ service, children, }): JSX.Element => { const [todos, setTodos] = useState<string[]>([]); useEffect(() => { getAll(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getAll = (): void => { setTodos([...service.getAll()]); }; const add = (todo: string): void => { service.add(todo); getAll(); }; const _delete = (todo: string): void => { service.delete(todo); getAll(); }; const contextValue = { todos, add, delete: _delete, }; return ( <TodosContext.Provider value={contextValue}> {children} </TodosContext.Provider> ); }; export default TodosProvider;
вот и все, перейдите в index.tsx и вызовите свое приложение внутри провайдера
import React from "react"; import ReactDOM from "react-dom"; import TodoApp from "./components/TodoApp"; import TodosProvider from "./providers/todos.provider"; import reportWebVitals from "./reportWebVitals"; import TodoInMemoryRepository from "./repositories/todoInMemory.repository"; import TodoService from "./services/todo.service"; const repository = new TodoInMemoryRepository(); const todoService = new TodoService(repository); ReactDOM.render( <React.StrictMode> <TodosProvider service={todoService}> <TodoApp /> </TodosProvider> </React.StrictMode>, document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
Посмотрите, как мы определяем службу и передаем ее провайдеру с помощью реквизитов. Службе нужен репозиторий, но у него может быть несколько реализаций, которые не зависят от службы.
если мы изменим репозиторий, мы не пострадаем от изменений в коде.
Тестирование:
const repository = new TodoInLocalStorageRepository(); const todoService = new TodoService(repository);
да все работает!.
теперь можно запускать приложение с localStorage и тестировать с данными в памяти без дополнительного кода или проблем.
Напишите тест:
import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import TodoService from "../services/todo.service"; import TodoApp from "../components/TodoApp"; import TodosProvider from "../providers/todos.provider"; import TodoInMemoryRepository from "../repositories/todoInMemory.repository"; const renderApp = (service: TodoService) => { return render( <TodosProvider service={service}> <TodoApp /> </TodosProvider> ); }; test("should add todo in todo app", () => { const repository = new TodoInMemoryRepository(); const service = new TodoService(repository); const { container } = renderApp(service); const textInput = screen.getByPlaceholderText(/add todo/i); const button = container.querySelectorAll("input[type=submit]")[0]; const todo = "test me"; fireEvent.change(textInput, { target: { value: todo } }); fireEvent.click(button); expect(screen.getByText(todo)).toBeInTheDocument(); });