Как работать с контекстом в React?

2 года назад·5 мин. на чтение

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

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

Проблема с передачей пропсов в React

Передача пропсов - отличный способ явно передать данные через UI дерево компонентам, которые его используют. Но передача пропса может стать многословным и неудобным, когда вам нужно передать какой-то проп глубоко через дерево, или если многим компонентам нужен один и тот же проп. Ближайший общий предок может быть далек от компонентов, которые нуждаются в данных, и поднятие состояния вверх может привести к ситуации, иногда называемой “prop drilling”. Было бы здорово, просто «телепортировать» данные к компонентам дерева, которые в них нуждаются.

Контекст: альтернатива передаче пропсов

Контекст позволяет родительскому компоненту предоставлять данные всему дереву под ним. Есть много применений для контекста. Вот один из примеров. Рассмотрим компонент Heading, который принимает level для своего размера:
// App.js

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Heading level={2}>Heading</Heading>
      <Heading level={3}>Sub-heading</Heading>
      <Heading level={4}>Sub-sub-heading</Heading>
      <Heading level={5}>Sub-sub-sub-heading</Heading>
      <Heading level={6}>Sub-sub-sub-sub-heading</Heading>
    </Section>
  );
}
// Section.js

export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
// Heading.js

export default function Heading({ level, children }) {
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
Допустим, вы хотите, чтобы несколько заголовков в одном разделе всегда имели одинаковый размер:
// App.js

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Section>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Section>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Section>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
В настоящее время вы передаете проп level каждому <Heading> отдельно:
<Section>
  <Heading level={3}>About</Heading>
  <Heading level={3}>Photos</Heading>
  <Heading level={3}>Videos</Heading>
</Section>
Было бы неплохо, если бы вы могли передать проп level в <Section> и удалить его из <Heading>. Таким образом, вы можете добиться того, чтобы все заголовки в одном разделе имели одинаковый размер:
<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>
Но как <Heading> может узнать уровень своего ближайшего <Section>? Для этого дочернему компоненту потребуется какой-то способ «попросить» данные откуда-то сверху на дереве. Вы не можете сделать это только с пропсами. Именно здесь в игру вступает контекст. Вы можете сделать это в три шага:
  1. Создайте контекст. (Вы можете назвать его LevelContext, так как он предназначен для уровня заголовка)
  2. Используйте этот контекст из компонента, которому требуются данные. (Heading будет использовать LevelContext)
  3. Укажите этот контекст из компонента, задающего данные. (Section будет содержать LevelContext.)
Контекст позволяет родителю предоставить некоторые данные всему дереву внутри него.

Шаг 1: Создайте контекст

Во-первых, вам нужно создать контекст. Вам нужно будет экспортировать его из файла, чтобы ваши компоненты могли использовать его:
// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(1);
Единственным аргументом createContext является значение по умолчанию. Здесь 1 относится к самому большому уровню заголовка, но вы можете передать любое значение (даже объект). Вы увидите значение значения по умолчанию на следующем шаге.

Шаг 2: Используйте контекст

Импортируйте хук useContext из React и импортируйте контекст, созданный на первом шаге:
// Heading.js

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Как вы помните компонент Heading считывает level из пропса:
export default function Heading({ level, children }) {
  // ...
}
Вместо этого удалите пропlevel и прочитайте значение из только что импортированного контекста LevelContext:
export default function Heading({ children }) {
 const level = useContext(LevelContext);
 // ...
}
useContext — это хук. Так же, как useState и useReducer, вы можете вызвать хук только на верхнем уровне компонента React. useContext сообщает React, что компонент Heading хочет прочитать LevelContext. Теперь, когда компонент Heading не имеет пропса level, вам больше не нужно передавать проп level в Heading в вашем JSX. Обновите JSX так, чтобы его получал Section:
<Section level={4}>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
</Section>
Обратите внимание, что этот пример еще не совсем работает. Все заголовки имеют одинаковый размер, потому что хоть мы и вызвали контекст, но еще не предоставили его. React не знает, где его взять. Если контекст не указан, React будет использовать значение по умолчанию, указанное на предыдущем шаге. В этом примере в качестве аргумента createContext мы передали 1, поэтому useContext(LevelContext) возвращает 1, установив для всех этих заголовков <h1>. Давайте исправим эту проблему, предоставив каждому Section свой собственный контекст.

Шаг 3: Предоставьте контекст

Компонент Section в настоящее время рендерит дочерние компоненты. Оберните их поставщиком контекста (context provider), чтобы предоставить им LevelContext:
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}
Это говорит React: «Если какой-либо компонент внутри этого <Section> просит LevelContext, дайте им этот level». Компонент будет использовать значение ближайшего <LevelContext.Provider> в UI дереве над ним. Этот код будет работать так же как и в самой первой реализации. Но вам не нужно было передавать проп level каждому компоненту Heading. Вместо этого он «выясняет» свой уровень заголовка, спрашивая ближайший Section выше:
  1. Вы передаете проп level в <Section>.
  2. Section оборачивает свои дочерние элементы в <LevelContext.Provider value={level}>.
  3. Heading запрашивает ближайшее значение LevelContext выше с useContext(LevelContext).

Типичные варианты использования контекста

  • Темы: Если ваше приложение позволяет пользователю изменять его внешний вид (например, в темном режиме), вы можете поместить поставщика контекста в верхнюю часть приложения и использовать этот контекст в компонентах, которые должны настроить свой визуальный вид.
  • Текущий пользователь приложения: Многим компонентам может потребоваться знать текущего вошедшего в систему пользователя. Помещение его в контекст позволяет удобно читать его в любом месте дерева. Некоторые приложения также позволяют управлять несколькими учетными записями одновременно (например, оставлять комментарий как другой пользователь). В этих случаях может быть удобно обернуть часть пользовательского интерфейса во вложенного поставщика с другим значением текущего счета.
  • Маршрутизация: Большинство решений маршрутизации используют внутренний контекст для хранения текущего маршрута. Таким образом, каждая ссылка «знает», активна она или нет. Если вы создаете свой собственный маршрутизатор, вы тоже можете это сделать.
  • Управление состоянием: По мере роста вашего приложения вы можете получить большое состояние ближе к верхней части вашего приложения. Многие отдаленные компоненты ниже могут захотеть изменить его. Обычно используется редьюсер вместе с контекстом для управления сложным состоянием и передачи его удаленным компонентам без особых хлопот.
Контекст не ограничивается статическими значениями. Если вы передадите другое значение при следующем рендеринге, React обновит все нижележащие компоненты, которые его читают. Вот почему контекст часто используется в сочетании с состоянием. В общем, если какая-то информация нужна отдаленным компонентам в разных частях дерева, это хороший признак того, что контекст вам поможет.

Итоги

  • Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.
  • Чтобы передать контекст:
    1. Создайте и экспортируйте его с export const MyContext = createContext(defaultValue).
    2. Передайте его в хук useContext(MyContext) чтобы прочитать его в любом дочернем компоненте, независимо от глубины.
    3. Оберните дочерние комоненты в <MyContext.Provider value={...}> для представления контекста из родительского компонента.
  • Контекст проходит через любые компоненты посередине.
  • Контекст позволяет писать компоненты, которые «адаптируются к окружающей среде».
  • Прежде чем использовать контекст, попробуйте передать проп или передать JSX как children.

Как загружать файлы в React с помощью NodeJS и Express

2 года назад·3 мин. на чтение

В этой статье рассмотрим как загрузить файлы в React использованием Express в качестве бэкенда.

Создание бэкенда на Express

Во-первых, мы создаем POST API с использованием NodeJS и Express, который поможет нам загружать файлы (изображения, pdf и т. д.) на внутренний сервер.

Настройка серверного проекта

Давайте настроим бэкенд проект NodeJS, выполнив следующие команды одну за другой.
mkdir fileupload
cd fileupload
npm init -y

Установка пакетов

Теперь нам нужно установить четыре пакета: express, express-fileupload, cors и nodemon. Выполните приведенную ниже команду, чтобы установить пакеты.
npm i express express-fileupload cors nodemon
Теперь откройте папку fileupload в вашем любимом редакторе кода и создайте новый файл с именем server.js. Добавьте следующий код в файл server.js.
// server.js

const express = require('express');
const fileUpload = require('express-fileupload');
const cors = require('cors')

const app = express();

// middleware
app.use(express.static('public')); // для доступа к файлам в папке public
app.use(cors());
app.use(fileUpload());

// API для загрузки файлов
app.post('/upload', (req, res) => {
  if (!req.files) {
    return res.status(500).send({ msg: "file is not found" })
  }
   
  const myFile = req.files.file;
 
  // метод mv() помещает файл в папку public
  myFile.mv(`${__dirname}/public/${myFile.name}`,
    function (err) {
      if (err) {
        console.log(err)
        return res.status(500).send({ msg: "Error occurred" });
      }
      // возвращаем ответ с именем файла и его расположением
      return res.send({name: myFile.name, path: `/${myFile.name}`});
  });
})


app.listen(4500, () => {
  console.log('server is running at port 4500');
})
В приведенном выше коде мы сначала импортировали три пакета: express, express-fileupload и cors, а затем создали приложение express вызвав функцию express() Наш маршрут POST API - /upload. Мы помещаем файлы в общую папку public. Для этого нам нужно создать папку public внутри нашего бэкенд-проекта.

Добавление скриптов

Чтобы запускать и перезапускать сервер мы используем nodemon. Откройте файл package.json и добавьте следующий код в scripts.
 "server": "nodemon server.js"
Теперь запустите сервер, выполнив команду npm run server в терминале.

Создание приложения React

Давайте создадим новое React приложение, выполнив следующую команду.
npx create-react-app react-fileupload
Теперь измените текущий рабочий каталог, выполнив приведенную ниже команду.
cd react-fileupload

Установка библиотеки Axios

Нам также необходимо установить клиентскую библиотеку axios, которая используется для выполнения http-запросов.
npm i axios

Создание компонента загрузки файлов

Откройте папку react-fileupload в своем любимом редакторе кода и создайте новый файл с именем fileupload.js внутри папки src. Теперь добавьте следующий код.
// fileupload.js

import React, { useRef, useState } from 'react';
import axios from 'axios';

function FileUpload() {

  // для хранения загруженного файла
  const [file, setFile] = useState('');
  
  // для хранения ответа от бекенда
  const [data, getFile] = useState({ name: "", path: "" });

  const [progress, setProgess] = useState(0); // progessbar
  const el = useRef(); // для доступа к инпуту

  const handleChange = (e) => {
    setProgess(0)
    const file = e.target.files[0]; // доступ к файлу
    console.log(file);
    setFile(file); // сохранение файла
  }

  const uploadFile = () => {
    const formData = new FormData();
    formData.append('file', file); // добавление файла
    axios.post('http://localhost:4500/upload', formData, {
      onUploadProgress: (ProgressEvent) => {
        let progress = Math.round(
          ProgressEvent.loaded / ProgressEvent.total * 100
        ) + '%';
        setProgess(progress);
      }
    }).then(res => {
      console.log(res);
      getFile({
        name: res.data.name,
        path: 'http://localhost:4500' + res.data.path
      })
    }).catch(err => console.log(err))
  }

  return (
    <div>
      <div className="file-upload">
        <input type="file" ref={el} onChange={handleChange} />
        <div className="progessBar" style={{ width: progress }}>
          {progress}
        </div>
        <button onClick={uploadFile} className="upbutton">
          Upload
        </button>
        <hr />
        {/* для показа полученного изображения */}
        {data.path && <img src={data.path} alt={data.name} />}
      </div>
    </div>
  );
}

export default FileUpload;
В приведенном выше коде мы использовали хуки react для управления состоянием, и у нас есть две функции: handleChange и uploadFile. handleChange вызывается после того, как пользователь выбрал файл. Функция uploadFile() используется для загрузки файла в наш /upload api. Существует также индикатор выполнения, который показывает прогресс загрузки файла на сервер, а также мы отображаем изображение после получения ответа от сервера.

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

Добавьте следующие стили в файл App.css.
.App {
    margin: 2rem auto;
    max-width: 800px;
}

img {
  width: 500px;
  height: 500px;
  object-fit: contain;
}

.progessBar {
  height: 1rem;
  width: 0%;
  background-color: rgb(68, 212, 231);
  color: white;
  padding:2px
}

.file-upload {
  box-shadow: 2px 2px 2px 2px #ccc;
  padding: 2rem;
  display: flex;
  flex-direction: column;

  justify-content: space-between;
  font-size: 1rem;
}

input , div , button{
  margin-top: 2rem;
}

.upbutton {
   width: 5rem;
   padding: .5rem;
   background-color: #2767e9;
   color: aliceblue;
   font-size: 1rem;
   cursor: pointer;
}
Теперь импортируйте компонент FileUpload в файл App.js.
// App.js

import React from 'react';
import FileUpload from './fileupload';
import './App.css';

function App() {
  return (
    <div className="App">
      <FileUpload />
    </div >
  );
}

export default App;
Запустите приложение React, запустив npm start.