Как загружать файлы в 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.

Хук useContext - как использовать контекст в React?

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

useContext - это React хук, который позволяет вам читать и подписываться на контекст из вашего компонента.

API хука useContext

const value = useContext(SomeContext)

useContext(SomeContext)

Вызовите useContext на верхнем уровне вашего компонента для чтения и подписки на контекст.
import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  // ...

Параметры

  • SomeContext: Контекст, который вы ранее создали с помощью createContext. Сам контекст не содержит информации, он только представляет тип информации, которую вы можете предоставить или прочитать из компонентов.

Что возвращает useContext?

useContext возвращает значение контекста для вызывающего компонента. Оно определяется как значение, переданное ближайшему SomeContext.Provider, расположенному выше вызывающего компонента в дереве. Если такого провайдера нет, то возвращаемое значение будет значением по умолчанию (defaultValue), которое вы передали в createContext для данного контекста. React автоматически перерисовывает компоненты, которые читают некоторый контекст, если он изменяется.

Использование контекста

Передача данных вглубь дерева

Вызовите useContext на верхнем уровне вашего компонента для чтения и подписки на контекст.
import { useContext } from 'react';

function Button() {
  const theme = useContext(ThemeContext);
  // ...
useContext возвращает значение контекста для переданного вами контекста. Чтобы определить значение контекста, React просматривает дерево компонентов и находит ближайший вышеуказанный провайдер контекста для данного контекста. Чтобы передать контекст кнопке, оберните ее или один из ее родительских компонентов в соответствующий провайдер контекста:
function MyPage() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  );
}

function Form() {
  // ... отрисовывает кнопки внутри себя ...
}
Не имеет значения, сколько слоев компонентов находится между провайдером и кнопкой. Когда кнопка в любом месте формы вызывает useContext(ThemeContext), она получит значение "dark".
import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}

function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

Обновление данных, переданных через контекст

Часто бывает необходимо, чтобы контекст менялся с течением времени. Чтобы обновить контекст, вам нужно объединить его с состоянием. Объявите переменную state в родительском компоненте и передайте текущее состояние в качестве значения контекста провайдеру.
unction MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext.Provider>
  );
}
Теперь любая кнопка внутри провайдера будет получать текущее значение темы. Если вы вызовете setTheme для обновления значения темы, которое вы передаете провайдеру, все компоненты Button будут заново отображаться с новым значением "light".

Указание значения по умолчанию

Если React не может найти ни одного провайдера данного контекста в родительском дереве, значение контекста, возвращаемое функцией useContext(), будет равно значению по умолчанию, которое вы указали при создании контекста:
const ThemeContext = createContext(null);
Значение по умолчанию никогда не изменяется. Если вы хотите обновить контекст, используйте его вместе с состоянием, как описано выше. Часто вместо null можно использовать какое-то более значимое значение по умолчанию, например:
const ThemeContext = createContext('light');
Таким образом, если вы случайно отобразите какой-то компонент без соответствующего провайдера, он не сломается. Это также поможет вашим компонентам хорошо работать в тестовой среде без установки большого количества провайдеров в тестах. В приведенном ниже примере кнопка "Toggle theme" всегда светлая, потому что она находится вне любого провайдера контекста темы, а значение контекстной темы по умолчанию - 'light'.
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <>
      <ThemeContext.Provider value={theme}>
        <Form />
      </ThemeContext.Provider>
      <Button onClick={() => {
        setTheme(theme === 'dark' ? 'light' : 'dark');
      }}>
        Toggle theme
      </Button>
    </>
  )
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  );
}

Переопределение контекста для части дерева

Вы можете переопределить контекст для части дерева, обернув эту часть в провайдер с другим значением.
<ThemeContext.Provider value="dark">
  ...
  <ThemeContext.Provider value="light">
    <Footer />
  </ThemeContext.Provider>
  ...
</ThemeContext.Provider>
Вы можете вложить и переопределить провайдеров столько раз, сколько вам нужно.

Оптимизация повторных рендерингов при передаче объектов и функций

Вы можете передавать любые значения через контекст, включая объекты и функции.
function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  function login(response) {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }

  return (
    <AuthContext.Provider value={{ currentUser, login }}>
      <Page />
    </AuthContext.Provider>
  );
}
Здесь значение контекста - это JavaScript объект с двумя свойствами, одно из которых - функция. Всякий раз, когда MyApp ререндерится (например, при обновлении маршрута), это будет другой объект, указывающий на другую функцию, поэтому React также придется перерендерить все компоненты в глубине дерева, которые вызывают useContext(AuthContext). В небольших приложениях это не является проблемой. Однако нет необходимости перерисовывать их, если базовые данные, такие как currentUser, не изменились. Чтобы помочь React воспользоваться этим фактом, вы можете обернуть функцию входа в систему в useCallback и обернуть создание объекта в useMemo.
Это оптимизация производительности будет выглядеть следующим образом:
import { useCallback, useMemo } from 'react';

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }, []);

  const contextValue = useMemo(() => ({
    currentUser,
    login
  }), [currentUser, login]);

  return (
    <AuthContext.Provider value={contextValue}>
      <Page />
    </AuthContext.Provider>
  );
}
В результате этого изменения, даже если MyApp потребуется повторный рендеринг, компонентам, вызывающим useContext(AuthContext), не потребуется повторный рендеринг, если только currentUser не изменился.