Я решил пересмотреть основы React, чтобы освежить свою память и укрепить свое понимание концепций и процедур. 🙂

Создать React-приложение

Во-первых, как инициализировать проект. (Для этого вам нужно иметь bash и npm)

$ npm create-react-app (NAME_OF_YOUR_PROJECT)

Теперь у вас будет инициализирован ваш проект.

Чтобы запустить свой локальный сервер, чтобы увидеть ваш проект в браузере.

$ npm run start

Теперь вы знаете, что реактивный проект инициализирован и готов к запуску.

Так откуда взялось это npm run start ? Как он был настроен на использование?

Проверьте файл package.json и просмотрите раздел script.

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

Здесь «старт» настроен на выполнение «запуска реактивных скриптов».

Когда вы клонируете GitHub, скорее всего, в вашем проекте нет каталога node_module. Это связано с тем, что в каталоге содержится много файлов, которые ненужно скачивать из репозитория GitHub, а напрямую из его исходников с помощью npm.

Вам просто нужно выполнить эту простую команду, чтобы установить все модули, определенные в файле package.json.

$ npm install

Компоненты и шаблоны

Компоненты — это строительные блоки для реагирующего приложения. Это автономный раздел контента, который содержит шаблон и логику. Имя компонента должно начинаться с заглавной буквы.

// If you are using an older version of React (<v16), 
// you need to add `import React from ‘react’` on the top.
import './App.css';
function App() {
  return (
    <div className="App">
      <p>HELLO WORLD</p>
    </div>
  );
}
export default App;

Код, который выглядит как HTML, называется JSX, который является расширением синтаксиса JavaScript.

Существует два типа компонентов: функциональный компонент и компонент класса. Проверьте этот пост в блоге, чтобы увидеть разницу между ними.

Динамические значения в шаблонах

Мы динамически выводим данные с переменными или кодом JS, встроенным между { }в подобные шаблоны.

function App(){
  const title = 'Hello World';
  const likes = 1000;
  const link = "http://www.google.com";
return (
    <div className="App">
      <div className="content">
         <h1>{ title }</h1>
         <p>Linked { likes } times </p>
         <p>{ 10 }</p>
         <p>{ "hello, ninjas" }</p>
         <p>{ [1,2,3,4,5] }</p>
         <p>{ Math.random() * 10 }</p>
         <a href={link}>Google Site</p>
      </div>
    </div>
  );
}

React может выводить строки, числа и массивы. Он не может выводить логические значения и объекты.

Несколько компонентов

Так как же собрать эти компоненты вместе? Во-первых, давайте создадим два компонента файлов «Navbar.js» и «Home.js».

// Navbar.js
const Navbar = () => {
  return (
    <nav className="navbar">
      <h1>The Blog</h1>
      <div className="links">
        <a href="/">Home</a>
        <a href="/create">Create</a>
      </div>
    </nav>  
  )
}
export default Navbar;
// Home.js
const Home = () => {
  return (
    <div className="home">
      <h2>Homepage</h2>
    </div>)}
export default Home;

Эти два файла можно импортировать в App.js, а затем добавить в оператор return следующим образом:

// index.js
import './App.css';
import Navbar from './Navbar.js';
import Home from './Home.js';
function App(){
  return(
    <div className="App>
       <Navbar />
       <div className="content">
          <Home />
       </div>
     </div>
  );
}

Добавление стилей

Как мы можем применить стили к компонентам? Ну, так же, как вы обычно делаете это с ванильным HTML и CSS, вы можете создать сценарий CSS, импортированный в компонент, или выполнить встроенный стиль, но вы должны использовать двойные фигурные скобки для атрибута стиля компонентов. ‘{{}}’ – это объект, заключенный в круглые скобки. Разница в том, что точно так же, как «класс» заменяется на «имя класса», вы должны сделать то же самое для имен свойств CSS.

// Navbar.js
const Navbar = () => {
  return (
    <nav className="navbar">
      <h1>The Blog</h1>
      <div className="links>
        <a href="/">Home</a>
        <a href="/create" style={{
          color:white,
          backgroundColor: '#f1356d'
        }}>New Blog</a>
      </div>
    </nav>  
  )
}
export default Navbar;

Нажмите События

Давайте добавим функцию, которая выполняется по клику. Вы должны определить функцию в своем компоненте и добавить атрибут «onClick», сделать его равным динамическому значению и сослаться на функцию:

const Home = () => {
  const handleClick = (e) => { 
    console.log('Hello', e)
  }
const handleClickAgain = (name, e) => { 
    console.log('Hello ' + name, e.target)
  }
return (
    <div className="home">
      <h2>Homepage</h2>
      <button onClick={handleClick}>Click Me</div>
      <button onClick={(e)=>{ 
        handleClickAgain('Sun', e)
      }}>Click Me Again</button>
    </div>
  );
}

Обратите внимание, что для onClick функция handleClick() не используется, поскольку она вызовет функцию. Способ выполнения функции при нажатии кнопки состоит в том, чтобы обернуть ссылку на функцию анонимной функцией.

Кроме того, из этого примера следует сделать вывод о том, как используется объект события. Для первой кнопки handleClick используется без круглых скобок, но реакция по-прежнему будет автоматически предоставлять объект события в параметре.

Для анонимной функции e определяется как параметр, который будет использоваться React для размещения объекта события, когда handleClickAgain('Sun',e) выполняется при нажатии кнопки.

Использование состояния (хук useState)

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

// First you have to import useState
import {useState} from 'react'
export default function Home() {
  const [title, setTitle] = useState('untitled');
  const [count, setCount] = useState(0)
const changeTitle = (e) => {
    setTitle(e.target.value);
  };
const addCount = () => {
    setCount(count + 1);
  }
return (
    <div style={{border: '1px solid', padding: '3em'}}>
      <h3>Counter</h3>
      <div>
        <p>{title}</p>
        <p>{count}</p>
      </div>
      <input onChange={changeTitle}></input>
      <button onClick={addCount}>Add!</button>
    </div>
  );
}

Эта вещь, где у нас есть const [title, setTitle] = useState('untitled'); , называется «деструктурирование массива». Это дает вам способ получить доступ к текущему состоянию переменной и функции для изменения состояния, и вы должны следовать этому формату, чтобы изменить состояние переменной в реагирующем компоненте.

Вывод списков

Скажем, вы хотите вывести список сообщений в блоге, как бы вы это сделали? вы можете использовать метод .map() для перебора массива и возврата таким образом динамически сгенерированных DOM.

import { useState } from 'react';
export default function Home(){
  const [blogs, setBlogs] = setState([
    {title: 'React101', body: 'lorem...', author: 'Sun, id: 1},
    {title: 'useState()', body: 'lorem...', author: 'Sun, id: 2},
    {title: 'Hooks', body: 'lorem...', author: 'Sun, id: 3}
  ]);
return (
    <div className="home">
      {blogs.map((blog) => (
        <div className="blog-preview" key={blog.id}>
          <h2>{ blog.title } </h2>
          <p>Written by {blog.author}</p>
        </div>
       ))}
    </div>
  );
}

Реквизит

Реквизиты предназначены для передачи данных дочернему компоненту. Для демонстрации взгляните на примеры здесь.

// Home.js
import {useState} from 'react';
import BlogList from './BlogList';
export default function Home() {
  const [blogs, setBlogs] = useState([
    {title: 'React 101', body: 'lorem...', author:'Sun', id: 1}, 
    {title: 'useState()', body: 'lorem...', author:'Moon', id: 2},
    {title: 'Hooks', body: 'lorem...', author:'Sun', id: 3},
  ])
return (
    <div className="Home">
      <BlogList blogs={blogs} title="All blog posts!"/>
    </div>
  )
}

BlogList.js импортируется, а затем компонент может использоваться в операторе return как JSX <BlogList />, и у него есть атрибуты. Атрибуты определены для создания свойств объекта реквизита, именно так вы передаете реквизит дочернему компоненту.

// blogList.js
export default function BlogList(props) {
  const blogs = props.blogs; 
  const title = props.title;
return (
    <div className="blog-list">
      {blogs.map(blog)=>(
        <div className="blog-post" key={blog.id}>
          <h2>{ blog.title }</h2>
          <p>Written by {blog.author} </p>
        </div>
      ))}
    </div>
  )
}

Этот дочерний компонент blogList.js имеет параметр под названием props. Этот реквизит — это объект, который содержит все реквизиты, переданные от родительского компонента. Вы также можете деструктурировать этот объект «реквизит», как показано ниже.

export default function BlogList({blogs, title}){
  return (
    <div className="blog-list">
      {blogs.map(blog)=>(
        <div className="blog-post" key={blog.id}>
          <h2>{ blog.title }</h2>
          <p>Written by {blog.author} </p>
        </div>
      ))}
    </div>
  )
}

Этот способ поможет вам сделать ваш код более кратким и простым в обслуживании.

Повторное использование компонентов

Итак, как бы вы повторно использовали свои компоненты с помощью реквизита? Один пример здесь покажет вам, как отфильтровать список на основе автора сообщения.

// Home.js
import {useState} from 'react';
import BlogList from './BlogList';
export default function Home() {
  const [blogs, setBlogs] = useState([
    {title: 'React 101', body: 'lorem...', author:'Sun', id: 1}, 
    {title: 'useState()', body: 'lorem...', author:'Moon', id: 2},
    {title: 'Hooks', body: 'lorem...', author:'Sun', id: 3},
  ])
return (
    <div className="Home">
      <BlogList blogs={blogs.filter(
        (blog) => blog.author === 'Sun')} 
        title="Sun's posts!"/>
      <BlogList blogs={blogs.filter(
        (blog) => blog.author === 'Moon')} 
        title="Moon's Post!"/>
    </div>
  )
}

Вы можете видеть, что .filter() используется для фильтрации массива блогов. Теперь имеет смысл создавать дочерние компоненты и использовать их для динамического создания контента. 😮

Функции как реквизит

Хорошо. Так что, если вы хотите добавить кнопку удаления для каждого сообщения в списке. Массив помещается в родительский компонент, но как вы доберетесь до него, чтобы использовать setState()?. Здесь вы добавляете функции в качестве реквизита.

// Home.js
import {useState} from 'react';
import BlogList from './BlogList';
export default function Home(){
  const [blogs, setBlogs] = useState([
    {title: 'React 101', body: 'lorem...', author:'Sun', id: 1}, 
    {title: 'useState()', body: 'lorem...', author:'Moon', id: 2},
    {title: 'Hooks', body: 'lorem...', author:'Sun', id: 3}
  ]);
const handleDelete = (id) => {
    const newBlogs = blogs.filter(blog => blog.id !== id);
    setBlogs(newBlogs)
  }
return (
    <div className="Home">
      <blogList blogs={blogs} handleDelete={handleDelete} />       
    </div>
  )
}

Вы видите handleDelete(), который фильтрует блог. Эта функция передается как реквизит handleDelete={handleDelete}.

Теперь нам нужна кнопка, которая выполняет эту функцию.

export default function BlogList({blogs, handleDelete}){
  return (
    <div className="blog-list">
      <div>Yo</div>
        <h2>{ blog.title }</h2>
        <p>Written by {blog.author} </p>
        <button onClick={()=> handleDelete(blog.id)}>
          Delete
        </button>
    </div>
  )
}

Оборачивая handleDelete(blog.id) в анонимную функцию, он запускает функцию при нажатии кнопки. поскольку функция была передана как реквизит, она получит доступ к функции в родительском компоненте, и setState(newBlogs) изменит состояние блогов на отфильтрованное.😀

использоватьЭффект Крюк

Функция useEffect запускается при отображении страницы. Всякий раз, когда состояние изменяется, оно будет выполнять функцию, определенную в первом аргументе.

// Home.js
import {useState, useEffect} from 'react';
import BlogList from './BlogList';
export default function Home(){
  const [blogs, setBlogs] = useState([
    {title: 'React 101', body: 'lorem...', author:'Sun', id: 1}, 
    {title: 'useState()', body: 'lorem...', author:'Moon', id: 2},
    {title: 'Hooks', body: 'lorem...', author:'Sun', id: 3}
  ]);
const handleDelete = (id) => {
      const newBlogs = blogs.filter(blog => blog.id !== id);
      setBlogs(newBlogs)
    }
useEffect(()=>{
    console.log('use effect ran');
  })
return (
    <div className="Home">
      <blogList blogs={blogs} handleDelete={handleDelete} />       
    </div>
  )
}

Зависимости useEffect

Мы можем установить зависимости для хука useEffect, чтобы функция, установленная в useEffect, запускалась только при изменении указанного состояния, что делает ее очень полезной. Эта условная зависимость может быть определена в массиве как второй аргумент хука useEffect.

Если нет второго аргумента, то нет и зависимостей, и он будет запускаться каждый раз при рендеринге DOM.

Если вы поместите пустой массив, он будет запущен один раз при первом рендеринге компонента.

// Home.js
import {useState} from 'react';
import BlogList from './BlogList';
export default function Home(){
  const [blogs, setBlogs] = useState([
    {title: 'React 101', body: 'lorem...', author:'Sun', id: 1}, 
    {title: 'useState()', body: 'lorem...', author:'Moon', id: 2},
    {title: 'Hooks', body: 'lorem...', author:'Sun', id: 3}
  ]);
const [name, setName] = useState('Sun')
const handleDelete = (id) => {
    const newBlogs = blogs.filter(blog => blog.id !== id);
    setState(newBlogs)
  }
useEffect(()=>{
    console.log('use effect ran');
    console.log(blogs);
  }, [name])
return (
    <div className="Home">
      <BlogList blogs={blogs} handleDelete={handleDelete} />
      <button onClick={()=> setName('Sean')}>change name</button>
      <p>{ name }</p>       
    </div>
  )
}

Использование JSON-сервера

Чтобы продемонстрировать, как useEffect можно использовать для извлечения данных, нам нужен тестовый сервер, который будет отправлять ответные данные JSON по запросу. Во-первых, давайте создадим поддельный файл JSON, который будет отправлен обратно.

{
  "blogs": [
    {
      "title": "ReactJS, how to use useEffect()",
      "body": "Why do we need useEffect hook and when do we use it?",
      "author": "Sun",
      "id": 1
    },
    {
      "title": "useState(), how do you use it?",
      "body": "How do you use useState? Lets find out...",
      "author": "Moon",
      "id": 2
    }
  ]
}

Создайте новый каталог «данные» в корневом каталоге и сохраните его в файле с именем «db.json».

Эта команда ниже установит JSON-сервер и одновременно запустит сервер.

$ npx json-server --watch data/db.json --port 8000

Итак, сервер готов.

Этот пакет предоставит 4 различных типа конечных точек.

/blogs        GET       Fetch all blogs
/blogs/{id}   GET       Fetch a single blog
/blogs        POST      Add a new blog
/blogs/{id}   DELETE    Delete a blog

Получение данных с помощью useEffect

Теперь давайте добавим fetch(), который будет запрашивать хук useEffect().

// Home.js
import {useState, useEffect} from 'react';
import BlogList from './BlogList';
export default function Home(){
  const [blogs, setBlogs] = useState(null);
const handleDelete = (id) => {
    const newBlogs = blogs.filter(blog => blog.id !== id);
    setState(newBlogs)
  }
useEffect(()=>{
    fetch('http://localhost:8000/blogs')
      .then(res=>{
        return res.json();
      })
      .then(data=>{
        setBlogs(data);
      })
   }, [])
return (
    <div className="Home">
    {blogs && 
      <BlogList blogs={blogs} handleDelete={handleDelete} />
    }
    </div>
  )
}

Теперь у useEffect есть функция fetch(), которая отправляет запрос на сервер JSON. Существует .then() методов для преобразования ответа в формат JSON, а setBlogs используется для изменения состояния блогов с нуля на ответ JSON.

В операторе return у нас есть blogs &&, чтобы убедиться, что компонент BlogList отображается только тогда, когда blogs не равен нулю.

Сообщение об условной загрузке

Давайте реализуем функцию загрузки сообщения, которое будет отображаться во время загрузки.

// Home.js
import {useState, useEffect} from 'react';
import BlogList from './BlogList';
export default function Home(){
  const [blogs, setBlogs] = useState(null);
  const [isPending, setIsPending] = useState(true);
const handleDelete = (id) => {
    const newBlogs = blogs.filter(blog => blog.id !== id);
    setState(newBlogs)
  }
useEffect(()=>{
    fetch('http://localhost:8000/blogs')
      .then(res=>{
        return res.json();
      })
      .then(data=>{
        setBlogs(data);
        setIsPending(false);
       })
   }, [])
return (
    <div className="Home">
    { isPending && <div>Loading...</div> }
    {blogs && 
      <BlogList blogs={blogs} handleDelete={handleDelete} />
    }
    </div>
  )
}

Теперь будет ожидающее сообщение, и когда данные будут получены, будет выполнено setIsPending(false), что приведет к исчезновению сообщения о загрузке, и будет отображаться <BlogList />.

Обработка ошибок выборки

Давайте реализуем catch() для обработки возможных ответов об ошибках от сервера.

// Home.js
import {useState, useEffect} from 'react';
import BlogList from './BlogList';
export default function Home(){
  const [blogs, setBlogs] = useState(null);
  const [isPending, setIsPending] = useState(true);
  const [error, setError] = useState(null);
const handleDelete = (id) => {
    const newBlogs = blogs.filter(blog => blog.id !== id);
    setState(newBlogs)
  }
useEffect(()=>{
    fetch('http://localhost:8000/blogs')
      .then(res=>{
        if(!res.ok){
          throw Error('Error: check your endpoint or request')
        }
        return res.json();
      })
      .then(data=>{
        setBlogs(data);
        setIsPending(false);
       })
      .catch(err=>{
        setIsPending(false)
        setError(err.message);
      }
}, [])
return (
    <div className="Home">
    { error && <div>{error}</div>}
    { isPending && <div>Loading...</div> }
    {blogs && 
      <BlogList blogs={blogs} handleDelete={handleDelete} />
    }
    </div>
  )
}

Вы можете проверить res.ok, чтобы увидеть логическое значение, и если оно неверно, будет выдано сообщение об ошибке. Затем эта выброшенная ошибка будет перехвачена методом .catch() и изменит состояние withsetError() на err.message. У нас также есть setIsPending(false), чтобы избавиться от сообщения о загрузке, поскольку в этот момент загрузка не происходит.

Изготовление пользовательского крючка

Теперь у нас есть хук useEffect() для извлечения данных при каждом рендеринге компонента, но было бы немного утомительно снова писать этот код всякий раз, когда нам нужно извлекать данные при написании нового компонента. Мы можем создать собственный хук в реакции, чтобы помочь вам писать повторяющиеся коды.

Все пользовательские хуки должны начинаться со слова «использовать». Давайте посмотрим на код и посмотрим, как мы можем создавать собственные хуки.

// useFetch.js
import {useState, useEffect} from 'react';
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isPending, setIsPending] = useState(true);
  const [error, setError] = useState(null);
useEffect(()=>{
    fetch(url)
      .then(res => {
        if(!res.ok){
          throw Error('could not fetch the data for that resource');
}
        return res.json();
      })
      .then(data => {
        setData(data);
        setIsPending(false);
        setError(null);
      })
      .catch(err=>{
        setIsPending(false);
        setError(err.message);
      })
  }, []);
return { data, isPending, error }
}
export default useFetch;

Файл useFetch.js готов. Как видите, все части useState() и useEffect() перенесены в этот скрипт из компонента Home.

Обратите внимание, что вместо статического URL-адреса, который мы использовали ранее, используется url. Это позволит повторно использовать этот хук всякий раз, когда нам нужно сделать запрос, и все, что нам нужно, это указать URL-адрес конечной точки в качестве аргумента.

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

// Home.js
import BlogList from './BlogList';
import useFetch from './useFetch';
export default function Home(){
const { data: blogs, isPending, error} =  useFetch('http://localhost:8000/blogs');
return (
    <div className="Home">
    { error && <div>{error}</div>}
    { isPending && <div>Loading...</div> }
    {blogs && 
      <BlogList blogs={blogs} handleDelete={handleDelete} />
    }
    </div>
  )
}

Во-первых, вам нужно импортировать пользовательский хук вверху скрипта. Затем деструктурируйте useFetch() и поместите URL-адрес в качестве аргумента для него. Это сделает реструктурированные значения доступными для использования возвращающими компонентами.

Реактивный маршрутизатор

Когда браузер делает запрос к серверу для вашего реагирующего сайта, сервер отвечает, отправляя обратно практически пустой HTML-файл и реагирующий js-файл. Когда делается запрос на переход на другую страницу, react перехватывает запрос и соответствующим образом вставляет запрошенную страницу, не отправляя ее на сервер, что делает работу в Интернете намного быстрее.

Для этого нам понадобится еще один пакет npm под названием react-router-dom. Мы установим версию 5 для демонстрации.

$ npm install react-router-dom@5

(Подробнее о react-router)

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

// App.js
import Navbar from './Navbar';
import Home from './Home';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
function App(){
  return (
    <Router>
      <div className="App">
        <Navbar />
        <div className="content">
          <Switch>
            <Route path="/">
              <Home>
            </Route>
          </Switch>
        </div>
      </div>
    </Router>
  );
}
export default App;

Во-первых, вам нужно импортировать BrowserRouter из «react-router-dom».

В этом примере импортируется { BrowserRouter as Router, Route, Switch }. Они используются для обертывания компонента.

Маршруты точного соответствия

Когда вы добавляете дополнительный маршрут, вы заметите, что реакция не направлена ​​на ваш предполагаемый маршрут.

<Switch>
  <Route path="/">
    <Home>
  </Route>
  <Route path="/create">
    <Create />
  </Route>
</Switch>

Когда вы пытаетесь направить свой браузер на «/create», вместо этого вы будете перенаправлены на «/». Это связано с тем, что react направит вас к первому маршруту, который соответствует маршруту назначения, а поскольку «/create» помещен в «/», он будет направлен на «/». Чтобы этого не произошло, нужно добавить exact в качестве атрибута Route.

<Switch>
  <Route exact path="/">
    <Home>
  </Route>
  <Route path="/create">
    <Create />
  </Route>
</Switch>

Добавив точно туда, он будет направлен на «/» только тогда, когда вы направите его на «/».

Странно привыкать, но это работает.

Ссылки на маршрутизаторы

Это наш текущий навигационный файл. Ссылки состоят из тегов привязки.

// Navbar.js
const Navbar = () => {
  return (
    <nav className="navbar">
      <h1>The Blog</h1>
      <div className="links>
        <a href="/">Home</a>
        <a href="/create">Home</a>
      </div>
    </nav>  
  )
}

Однако это не правильный способ создания ссылок. Чтобы заставить реагировать на ввод кода, мы должны использовать компонент ссылки

// Navbar.js
import {Link} from 'react-router-dom'
const Navbar = () => {
  return (
    <nav className="navbar">
      <h1>The Blog</h1>
      <div className="links">
        <Link to="/">Home</Link>
        <Link to="/create">Create</Link>
      </div>
    </nav>  
  )
}

использовать эффект очистки

С текущим кодом, когда вы нажимаете кнопку для просмотра компонента Create, useEffect, который извлекает данные, все еще работает. Это может снизить производительность вашего веб-приложения из-за утечки памяти.

Мы можем сделать это с помощью AbortController().

// useFetch.js
import {useState, useEffect} from 'react';
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isPending, setIsPending] = useState(true);
  const [error, setError] = useState(null);
useEffect(()=>{
    const abortCont = new AbortController();
setTimeout(()=>{   
      // AbortController is associated with this fetch by
      // adding this as a second argument
     fetch(url, { signal: abortCont.signal })
        .then(res => {
          if(!res.ok){
          throw Error('could not fetch the data for that resource');         
          }
          return res.json();
        })
        .then(data => {
          setData(data);
          setIsPending(false);
          setError(null);
        })
        .catch(err=>{
          // If its aborted, it will print the error message
          // Else, it sets pending to false and prints error message
          if(err.name === 'AbortError'){
            console.log('fetch aborted');
          }else{
            setIsPending(false);
            setError(err.message);
          }
        })
      }, 1000);
      return () => abortCont.abort();
}, [url]);
return { data, isPending, error }
}
export default useFetch;

Если вы используете React 18, вы можете заметить, что эта ошибка дважды выводится в консоли из-за изменений, внесенных в эту версию. Я нашел это видео, в котором объясняется, как работать с этим изменением. (Перейти по ссылке)

Самый простой способ — просто для этого урока закомментировать ваш ‹React.StrictMode› в index.js, и вы не увидите, что сообщение будет напечатано дважды.

Параметр маршрута

Создайте новый файл «BlogDetails.js» в каталоге src.

// BlogDetails.js
export default function BlogDetails(){
  return (
    <div className="blog-details">
      <h2>Blog Details</h2>
    </div>
  )
}

Теперь давайте добавим это в «app.js».

// App.js
import Navbar from './Navbar';
import Home from './Home';
import Create from './Create';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import BlogDetails from './BlogDetails';
function App(){
return (
    <Router>
      <div className="App">
        <Navbar />
        <div className="content">
          <Switch>
            <Route exact path="/">
              <Home />
            </Route>
            <Route path="/create">
              <Create />
            </Route>
            <Route path="/blogs/:id">
              <BlogDetails />
            </Route>
          </Switch>
        </div>
      </div>
    </Router>
  );
}
export default App;

Обратите внимание на символ двоеточия перед «id». Вот как вы сообщаете, что этот идентификатор является параметром.

Теперь наш файл JSON имел свойство с именем «id» для каждого объекта записи. Нам нужно использовать его, чтобы компонент «BlogDetail» знал, какой идентификатор ему нужно использовать, чтобы узнать, какую статью в блоге ему нужно отобразить.

import {useParams} from 'react-router-dom'
export default function BlogDetails () {
  const { id } = useParams();
  return (
     <div className="blog-details>
       <h2>Blog Details - { id }</h2>
     </div>
  )
}

Используя хук «useParams()», мы можем реструктурировать его, чтобы получить идентификатор из URL-адреса. Поэтому, когда вы находитесь на «localhost/blogs/1», компонент знает, что идентификатор равен 1.

Давайте отредактируем компонент «BlogList», чтобы в заголовке всех сообщений были ссылки, а не статический текст.

import {Link} from 'react-router-dom';
export default function BlogList({blog}) {
  return(
    <div className="blog-list">
    {
      blogs.map((blog)=>(
        <div className="blog-preview" key={blog.id}>
          <Link to={`/blogs/:${blog.id}`}>
            <h2>{blog.title}</h2>
            <p>Writte by {blog.author}</p>
          </Link>
        </div>
      ))
    }
    </div>
  )
}

Теперь список будет ссылкой, которая ведет к сообщению при нажатии.

Повторное использование пользовательских хуков

Теперь давайте воспользуемся созданными ранее пользовательскими хуками и используем их в компоненте «BlogDetails».

import {useParams} from 'react-router-dom';
import useFetch from './useFetch';
export default function BlogDetails(){
  const { id } = useParams();
  const { data: blog, error, isPending } = useFetch('http://localhost:8000/blogs/' + id);
  return(
    <div className="blog-details">
      { isPending && <div>Loading...</div> }
      { error && <div>{ error }</div> }
      { blog && (
        <article>
          <h2>{blog.title}</h2>
          <p>Written by { blog.author }</p>
          <div>
            {blog.body}
          </div>
        </article>
      )}
    </div>
  )
}

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

Контролируемые вводы (формы)

Давайте поработаем над реализацией форм в Create.js.

Обновим таблицу стилей.

// index.css
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');
/* base styles */
* {
  margin: 0;
  font-family: "Quicksand";
  color: #333;
}
.navbar {
  padding: 20px;
  display: flex;
  align-items: center;
  max-width: 600px; 
  margin: 0 auto;
  border-bottom: 1px solid #f2f2f2;
}
.navbar h1 {
  color: #f1356d;
}
.navbar .links {
  margin-left: auto;
}
.navbar a {
  margin-left: 16px;
  text-decoration: none;
  padding: 6px;
}
.navbar a:hover {
  color: #f1356d;
}
.content {
  max-width: 600px;
  margin: 40px auto;
  padding: 20px;
}
/* blog previews / list */
.blog-preview {
  padding: 10px 16px;
  margin: 20px 0;
  border-bottom: 1px solid #fafafa;
}
.blog-preview:hover {
  box-shadow: 1px 3px 5px rgba(0,0,0,0.1);
}
.blog-preview h2 {
  font-size: 20px;
  color: #f1356d;
  margin-bottom: 8px;
}
.blog-preview a{
  text-decoration: none;
}
/* blog details page */
.blog-details h2 {
  font-size: 20px;
  color: #f1356d;
  margin-bottom: 10px;
}
.blog-details div {
  margin: 20px 0;
}
.blog-details button {
  background: #f1356d;
  color: #fff;
  border: 0;
  padding: 8px;
  border-radius: 8px;
  cursor: pointer;
}
/* create new blog form */
.create {
  max-width: 400px;
  margin: 0 auto;
  text-align: center;
}
.create label {
  text-align: left;
  display: block;
}
.create h2 {
  font-size: 20px;
  color: #f1356d;
  margin-bottom: 30px;
}
.create input, .create textarea, .create select {
  width: 100%;
  padding: 6px 10px;
  margin: 10px 0;
  border: 1px solid #ddd;
  box-sizing: border-box;
  display: block;
}
.create button {
  background: #f1356d;
  color: #fff;
  border: 0;
  padding: 8px;
  border-radius: 8px;
  cursor: pointer;
}

Теперь давайте поработаем над Create.js.

//Create.js
import {useState} from "react";
export default function Create(){
  const [title, setTitle] = useState('hello');
  const [body, setBody] = useState('');
  const [author, setAuthor] = useState('Sun');
  return (
    <div className="create">
    <h2>Add a New Blog</h2>
    <form>
      <label>Blog title: </label>
      <input
        type="text"
        required 
        value={title}
        onChange={(e)=> setTitle(e.target.value)}
      />
      <label>Blog body:</label>
      <textarea
        required
        value={body}
        onChange={(e)=> setBody(e.target.value)}
      ></textarea>
      <label>Blog author:</label>
      <select
        value={author}
        onChange={(e)=> setAuthor(e.target.value)}
      >
        <option value="Sun">Sun</option>
        <option value="Moon">Moon</option>
      </select>
      <button>Add Blog</button>
        <p>{ title }</p>
        <p>{ body }</p>
        <p>{ author }</p>
      </form>
    </div>
  )
}

Теперь у нас есть форма с useState, в которой будут храниться состояния.

Представить форму

У нас есть форма, теперь пришло время добавить функцию отправки.

import {useState} from "react";
export default function Create(){
  const [title, setTitle] = useState('hello');
  const [body, setBody] = useState('');
  const [author, setAuthor] = useState('Sun');
  const handleSubmit = (e) => {
    e.preventDefault();
    const blog = {title, body, author};
    console.log(blog);
  }
  return (
    <div className="create">
    <h2>Add a New Blog</h2>
    <form onSubmit={handleSubmit}>
      <label>Blog title: </label>
      <input
        type="text"
        required 
        value={title}
        onChange={(e)=> setTitle(e.target.value)}
      />
      <label>Blog body:</label>
      <textarea
        required
        value={body}
        onChange={(e)=> setBody(e.target.value)}
      ></textarea>
      <label>Blog author:</label>
      <select
        value={author}
        onChange={(e)=> setAuthor(e.target.value)}
      >
        <option value="Sun">Sun</option>
        <option value="Moon">Moon</option>
      </select>
      <button>Add Blog</button>
        <p>{ title }</p>
        <p>{ body }</p>
        <p>{ author }</p>
      </form>
    </div>
  )
}

Здесь в форму добавляется атрибут onSubmit, который будет запускать функцию handleSubmit. Объект блога будет содержать все состояния, которые будут загружены на сервер.

Выполнение POST-запроса

Теперь нам нужно закончить функцию handleSubmit, чтобы сделать почтовый запрос для загрузки отправки на сервер JSON.

import {useState} from "react";
import {useHistory} from 'react-router-dom';
export default function Create(){
  const [title, setTitle] = useState('hello');
  const [body, setBody] = useState('');
  const [author, setAuthor] = useState('Sun');
  const [isPending, setIsPending ] = useState(false);
  const handleSubmit = (e) => {
    e.preventDefault();
    const blog = {title, body, author};
    setIsPending(true)
    fetch('http://localhost:8000/blogs',{
      method:'POST',
      headers: {"Content-type": "application/json},
      body: JSON.stringify(blog)
    }).then(()=>{
      console.log('New blog added');
      setIsPending(false);
    })
  }
  return (
    <div className="create">
    <h2>Add a New Blog</h2>
    <form onSubmit={handleSubmit}>
      <label>Blog title: </label>
      <input
        type="text"
        required 
        value={title}
        onChange={(e)=> setTitle(e.target.value)}
      />
      <label>Blog body:</label>
      <textarea
        required
        value={body}
        onChange={(e)=> setBody(e.target.value)}
      ></textarea>
    <label>Blog author:</label>
      <select
        value={author}
        onChange={(e)=> setAuthor(e.target.value)}
      >
        <option value="Sun">Sun</option>
        <option value="Moon">Moon</option>
      </select>
      {!isPending && <button>Add Blog</button>}
      {isPending && <button disabled>Adding blog...</button>}
        <p>{ title }</p>
        <p>{ body }</p>
        <p>{ author }</p>
      </form>
    </div>
  )
}

Используя fetch(), вы можете установить первый аргумент в конечную точку, а затем заголовки и тело HTTP-запроса, которые будут запрошены в конечной точке для второго аргумента. У нас также есть .then(), чтобы указать, был ли запрос успешным или нет.

Программная переадресация

Давайте реализуем useHistory() для перенаправления пользователя на домашнюю страницу после отправки сообщения.

// Create.js
import {useState} from "react";
import {useHistory} from 'react-router-dom';
export default function Create(){
  const [title, setTitle] = useState('hello');
  const [body, setBody] = useState('');
  const [author, setAuthor] = useState('Sun');
  const [isPending, setIsPending ] = useState(false);
  const handleSubmit = (e) => {
    e.preventDefault();
    const blog = {title, body, author};
    setIsPending(true)
    fetch('http://localhost:8000/blogs',{
      method:'POST',
      headers: {"Content-type": "application/json},
      body: JSON.stringify(blog)
    }).then(()=>{
      console.log('New blog added');
      setIsPending(false);
      // If you want to make it redirect to just one step back
      // history.go(1)
      // To redirect to home
      history.push('/');
    })
  }
return (
    <div className="create">
    <h2>Add a New Blog</h2>
    <form onSubmit={handleSubmit}>
      <label>Blog title: </label>
      <input
        type="text"
        required 
        value={title}
        onChange={(e)=> setTitle(e.target.value)}
      />
      <label>Blog body:</label>
      <textarea
        required
        value={body}
        onChange={(e)=> setBody(e.target.value)}
      ></textarea>
<label>Blog author:</label>
      <select
        value={author}
        onChange={(e)=> setAuthor(e.target.value)}
      >
        <option value="Sun">Sun</option>
        <option value="Moon">Moon</option>
      </select>
      {!isPending && <button>Add Blog</button>}
      {isPending && <button disabled>Adding blog...</button>}
        <p>{ title }</p>
        <p>{ body }</p>
        <p>{ author }</p>
      </form>
    </div>
  )
}

Удаление блогов

Теперь давайте реализуем функцию удаления.

// BlogDetails.js
import {useParams, useHistory} from 'react-router-dom';
import useFetch from './useFetch';
export default function BlogDetail(){
  const { id } = useParams();
  const {data: blog, error, isPending } = useFetch('http://localhost:8000/blogs/' + id)
  const history = useHistory();
  const handleClick = () => {
    fetch('http://localhost:8000/blogs/', + blog.id, {
      method: 'DELETE',
    }).then(()=>{
      history.push('/');
    })
  }
  return(
    <div className="blog-details">
      { isPending && <div>Loading...</div> }
      { error && <div>{ error }</div> }
      { blog && (
        <article>
          <h2>{ blog.title }</h2>
          <p>Written by {blog.author}</p>
          <div>{ blog.body }</div>
          <button onClick={handleClick}>delete</button>
        </article>
      )}
   </div>
  )
}

Опять же, вы используете fetch(), но на этот раз с помощью метода DELETE.

404 страницы

Давайте создадим страницу 404. Создайте файл «NotFound.js» и введите коды, как показано ниже.

// NotFound.js
import { Link } from "react-router-dom";
export default function NotFound () {
  return(
    <div className="not-found">
      <h2>Sorry</h2>
      <p>That page cannot be found</p>
      <Link to="/">Back to the homepage..</Link>
    </div>
  )
}

Теперь нам нужно отредактировать App.js и добавить маршрут для этой страницы.

// App.js
import Navbar from './Navbar';
import Home from './Home';
import Create from './Create';
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';
import BlogDetails from './BlogDetails';
import NotFound from './NotFound';
function App() {
  return(
    <Router>
      <div className="App">
        <Navbar />
        <div className="content">
          <switch>
            <Route exact path="/">
              <Home />
            </Route>
            <Route path="/create">
              <Create />
            </Route>
            <Route path="/blogs/:id">
              <BlogDetails />
            </Route>
            <Route path="*">
              <NotFound />
            </Route>
          </Switch>
        </div>
      </div>
    </Router>
  )
}
export default App;

Теперь, когда вы попытаетесь перейти на несуществующую страницу, вас перенаправят на эту страницу 404.

Поздравляем, вы прошли курс 101 по основам React! 😊