Как использовать переменные среды в React

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

О различных способах доступа к переменным среды в React приложении

Если у вас нет опыта server side программирования, переменные среды могут показаться чем-то магическим. Этот недостаток знаний может поставить вас в тупик, когда вы закончите создавать приложения todo на localhost и попытаетесь создать продакшн сборку в первый раз. Если вы хотите узнать, как использовать переменные среды в ваших собственных инструментах, или глубоко погрузиться в то, как переменные среды работают в React, вы можете продолжить чтение этой статьи. Но если вы ищете быстрое решение и используете Create React App, ознакомьтесь с документацией здесь. Пользователи NextJS, ознакомьтесь с документацией здесь.

Проблема, которую мы решаем

Как объявить различные URL-адресов API для локальной разработки и для продакшн сборки.

Как решить эту проблему

Использовать переменные среды. При работе с React переменные среды — это переменные, доступные через глобальный объект process.env. Этот глобальный объект предоставляется вашей средой через NodeJS. И поскольку у нас нет NodeJS в браузере, нам понадобится webpack. В этой статье рассмотрим два способа установки и использования переменных среды для ваших React проектов с помощью webpack: с помощью скриптов npm и с помощью файла .env.

Способ 1: Использование скриптов npm для установки переменных среды

Во-первых, установите webpack и webpack-cli из npm:
npm install --save-dev webpack webpack-cli
Перейдите в файл package.json, проверьте поле scripts и найдите команды, которые запускают webpack. Вероятно, это будет выглядеть примерно так:
{
  // ...
  scripts: {
    "dev": "webpack --config webpack.config.dev.js",
    "build": "webpack --config webpack.config.build.js"
  }
}
Давайте добавим некоторые переменные окружения с флагом --env в scripts:
{
  // ...
  scripts: {
    "dev": "webpack --env.API_URL=http://localhost:8000 --config webpack.config.dev.js",
    "build": "webpack --env.API_URL=https://www.myapi.com --config webpack.config.build.js"
  }
}
Мы добавили --env.API_URL= часть в обоих скриптах. Теперь запустите команду npm run dev, перейдите к React компоненту и используйте process.env.API_URL:
const App = () => <h1>{process.env.API_URL}</h1>;
И тут проект должен сломаться.
Сломается он потому, что когда мы используем переменные окружения в клиентском коде, они на самом деле просто служат заполнителями, которые будут заменены при компиляции нашего кода. Проблема в том, что мы не сказали webpack скомпилировать эти переменные в реальные значения. Давайте сделаем это в нашем конфигурационном файле webpack с плагином DefinePlugin:
const webpack = require('webpack'); // DefinePlugin это часть webpack, поэтому это require обязателен

// возвращаем функцию из config файла
// переменная `env` будет просто объектом { API_URL: 'http://localhost:8000' }
// в ней будут содержаться все переменные среды, которые мы указали в package.json

module.exports = (env) => {
  // этот объект это сама конфигурация webpack
  return {
    plugins: [
      // добавим плагин в список плагинов
      new webpack.DefinePlugin({ `process.env.API_URL`: JSON.stringify(${env.API_URL}) })
    ]
  };
};
DefinePlugin требует, чтобы вы буквально определили свои «переменные среды». Вы также можете применить .reduce к переменным среды, чтобы получить объект:
module.exports = (env) => {
  // создаем объект из переменных среды
  const envKeys = Object.keys(env).reduce((prev, next) => {
    prev[`process.env.${next}`] = JSON.stringify(env[next]);
    return prev;
  }, {});

  return {
    plugins: [
      new webpack.DefinePlugin(envKeys)
    ]
  };
};
Если вы запустите команду сейчас, все скомпилируется, и ваш process.env.API_URL будет скомпилирован в правильный URL-адрес на основе переменной среды.

Способ 2: Использование файла .env для установки переменных среды

Вся идея здесь состоит в том, чтобы создать файл (называемый просто .env), заполненный переменными среды. Чтобы защитить пароли и другие значения переменных среды, добавьте файл .env в .gitignore. Фронтенд код будет ссылаться на одну и ту же переменную среды (process.env.API_URL) в обеих средах (при локальной разработке и на продакшене), но поскольку вы определили разные значения в своих .env, скомпилированные значения будут отличаться.

Создадим файл .env

Этот файл должен находиться в корневом каталоге проекта и называться .env. Добавим переменную:
API_URL=http://localhost:8000

Обработка файла .env

Теперь нам нужен какой-то способ обработки файлов и их содержимого. Для этого мы собираемся использовать популярный npm пакет под названием dotenv. Dotenv широко используется (create-react-app использует его). Он будет получать переменные из нашего файла .env и добавлять их в глобальный process.env.
$ npm install --save-dev dotenv

Добавление переменных в проект React

Есть одна проблема. Dotenv работает только на стороне сервера. А мы хотим использовать переменные среды на стороне клиента, на фронтенде. В данном случае мы разрабатываем клиентскую часть. И dotenv нужна какая-то среда для фактического хранения переменных. Здесь поможет Webpack. Воспользуемся плагином DefinePlugin в нашей webpack конфигурации:
const webpack = require('webpack');
const dotenv = require('dotenv');

module.exports = () => {
  // dotenv вернет объект с полем parsed 
  const env = dotenv.config().parsed;
  
  // сделаем reduce, чтобы сделать объект
  const envKeys = Object.keys(env).reduce((prev, next) => {
    prev[`process.env.${next}`] = JSON.stringify(env[next]);
    return prev;
  }, {});

  return {
    plugins: [
      new webpack.DefinePlugin(envKeys)
    ]
  };
};
При необходимости проверьте параметры конфигурации dotenv в документации на github. Вызов .config() в dotenv вернет объект со всеми переменными среды, установленными в вашем файле .env через поле parsed. Теперь давайте проверим наш React код:
const App = () => <h1>{process.env.API_URL}</h1>;
И это работает! Он показывает значение переменной среды API_URL, определенной в .env. Осталась только одна проблема: нам все еще нужно определить различные API_URL для локальной разработки и продакшена.

Различные переменные среды для разных сред

Вся идея состоит в том, чтобы создать разные файлы .env для разных сред и позволить webpack выбрать правильный файл .env в зависимости от активной среды. Поэтому создайте два файла в корневом каталоге проекта:
  • .env (содержит все переменные среды для продакшн)
  • .env.development (содержит все переменные среды для локальной разработки)
Чтобы было ясно: мы добавляем к имени файла .env сопоставление имени среды. Общепринятой практикой является использование исходного файла .env для продакшн сборки, поэтому мы не будем добавлять постфикс для продакшн .env .

Настройка активной среды с помощью scripts в package.json

Мы собираемся использовать scripts (как мы это делали в методе 1), чтобы установить текущую среду в нашем package.json:
{
  "scripts": {
    "dev": "webpack --env.ENVIRONMENT=development --config webpack.config.dev.js",
    "build": "webpack --env.ENVIRONMENT=production --config webpack.config.build.js"
  }
}
Так как мы определили нашу среду в нашем package.json, теперь она доступна в нашей конфигурации webpack. Следующим шагом будет переход к webpack конфигурации и дать ему использовать файл .env, принадлежащий активной среде. Как и раньше, мы используем dotenv, но теперь мы указываем пользовательский path в параметрах.
const webpack = require('webpack');
const dotenv = require('dotenv');
const fs = require('fs'); // для проверки существования файла
const path = require('path'); // для получения текущего пути

module.exports = (env) => {
  // получаем корневой путь (предполагаем, что webpack config лежит в корне проекта)
  const currentPath = path.join(__dirname);
  
  // путь по умолчанию (будет использован для продакшена - `.env`)
  const basePath = currentPath + '/.env';

  // склеиваем имя среды с именем файла для получения имени env файла
  const envPath = basePath + '.' + env.ENVIRONMENT;

  // проверяем существует ли env файл, если нет используем имя по умолчанию
  const finalPath = fs.existsSync(envPath) ? envPath : basePath;

  // устанавливаем параметр path в dotenv
  const fileEnv = dotenv.config({ path: finalPath }).parsed;
  
  // сделаем reduce, чтобы получить объект
  const envKeys = Object.keys(fileEnv).reduce((prev, next) => {
    prev[`process.env.${next}`] = JSON.stringify(fileEnv[next]);
    return prev;
  }, {});

  return {
    plugins: [
      new webpack.DefinePlugin(envKeys)
    ]
  };
Эта вся необходимая настройка, но вы можете создать больше .env файлов для большего количества сред (например, .env.staging) по аналогии.

11 ошибок, которых следует избегать при создании React приложений

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

Некоторые распространенные ошибки в разработке React приложений, которых следует избегать.

Поскольку React становится все более и более популярным, все больше и больше React разработчиков сталкиваются с различными проблемами в процессе разработки. В этой статье, мы обобщим некоторые распространенные ошибки в разработке React приложений, чтобы помочь вам их избежать. Если вы только начинаете использовать React, рекомендуется внимательно ознакомиться с этой статьей. Если вы уже используете React для разработки проектов, также рекомендуется проверить и заполнить пробелы.
Прочитав эту статью, вы узнаете, как избежать эти 11 ошибок React:
  • При рендеринге списка не используется key
  • Изменение значения состояния прямым присваиванием
  • Привязка значения состояния непосредственно к свойству value инпута
  • Использование состояния сразу после выполнения setState
  • Появление бесконечного цикла при использовании useState + useEffect
  • Отсутствие очистки побочных эффектов в useEffect
  • Неправильное использование логических операторов
  • Тип пропсов компонента не типизирован
  • Передача строк в качестве значений компонентам
  • Имя компонента не начинается с заглавной буквы
  • Неверная привязка события к элементу

Ошибка: При рендеринге списка не используется key

Проблема Когда мы впервые изучали React, мы отображали список следующим образом:
const items = [
  { id: 1, value: 'item1' },
  { id: 2, value: 'item2' },
  { id: 3, value: 'item3' },
  { id: 4, value: 'item4' },
  { id: 5, value: 'item5' }
];

const listItems = items.map((item) => {
  return <li>{item.value}</li>
});
После рендеринга консоль выдаст предупреждение, что для элементов списка необходимо указать ключ. Решение Вам просто нужно последовать этой подсказке и добавить key к каждому элементу:
const items = [
  { id: 1, value: ‘item1’ },
  { id: 2, value: ‘item2’ },
  { id: 3, value: ‘item3’ },
  { id: 4, value: ‘item4’ },
  { id: 5, value: ‘item5’ }
];

const listItems = items.map((item) => {
  return <li key={item.id}>{item.value}</li>
});
key помогает React определить, какие элементы были изменены, например, добавлены или удалены. Поэтому нам нужно установить уникальное значение ключа для каждого элемента в массиве. Для значения ключа лучше всего установить уникальное значение. В приведенном выше примере используется id. Можно использовать индекс массива, но такой подход не рекомендуется. Уникальный ключ помогает React следить за изменениями списка - какой элемент удалился или переместился.

Ошибка: Изменение значения состояния прямым присваиванием

Проблема В React нельзя назначать состояние и изменять напрямую, иначе это вызовет проблемы.
// классовый компонент

handleChange = () => {
   this.state.name = "John";
};
В этот момент будет выдано предупреждение не изменять состояние напрямую, а использовать setState(). Решение Классовые компоненты могут быть изменены с помощью setState(), а функциональные компоненты могут быть изменены с помощью useState():
// Классовые компоненты: используйте setState()
this.setState({ name: "John" });

// Функциональные компоненты:используйте useState()
const [name, setName] = useState("");
setName("John");

Ошибка: Привязка значения состояния непосредственно к свойству value инпута

Проблема Когда мы напрямую привязываем значение состояния к свойству value инпута, мы обнаружим, что независимо от того, что мы вводим в поле ввода, содержимое поля ввода не изменится.
export default function App() {
  const [count, setCount] = useState(0);
  return <input type="text" value={count} />;
}
Это связано с тем, что мы используем переменную состояния в качестве значения по умолчанию для присвоения значения <input>, а состояние в функциональном компоненте может быть изменено только функцией set*, возвращаемым useState. Таким образом, решение также очень простое, просто используйте функцию set* при изменении. Подробнее о том как работать с инпутом в React можно прочитать в этой статье. Решение Просто привяжите событие onChange к <input> и измените его, вызвав setCount:
export default function App() {
  const [count, setCount] = useState(0);
  const handleChange= (event) => setCount(event.target.value);

  return <input type="text" value={count} onChange={handleChange} />;
}

Ошибка: Использование состояния сразу после выполнения setState

Проблема Когда мы изменяем данные через setState() и сразу же хотим получить новые данные, возникнет ситуация, что возвращаются старые данные:
// Классовые компоненты

// инициализация состояния
this.state = { name: "John" };

// обновление состояния
this.setState({ name: "Hello, John!" });
console.log(this.state.name); // => John
Это связано с тем, что setState() является асинхронным. Когда setState() выполняется, реальная операция обновления будет помещена в асинхронную очередь для выполнения, а код, который будет выполняться следующим (т.е. console.log в примере), выполняется синхронно, поэтому выводимое в консоль состояние не является последним значением. Решение Просто передайте последующую операцию, которая будет выполняться как функция, в качестве второго параметра setState(), эта функция обратного вызова будет выполнена после завершения обновления.
this.setState({ name: "Hello, John!" }, () => {
  console.log(this.state.name); // => Hello, John!
});
Теперь обновленное значение выводится правильно.

Ошибка: Появление бесконечного цикла при использовании useState + useEffect

Проблема Когда мы напрямую вызываем метод set*(), возвращаемый useState() внутри useEffect(), и не устанавливаем второй параметр в useEffect(), мы столкнемся с бесконечным циклом:
export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  });
  return <div className="App">{count}</div>;
}
После этого можно увидеть, что данные на странице обновляются, и функция useEffect() вызывается бесконечно, входя в состояние бесконечного цикла. Решение Это распространенная проблема неправильного использования useEffect(). useEffect() можно рассматривать как комбинацию трех функций жизненного цикла: componentDidMount, componentDidUpdate и componentWillUnmount в классовых компонентах. useEffect(effect, deps) принимает 2 аргумента:
  • effect функция, которая должна выполниться (побочный эффект)
  • deps массив зависимостей
При изменении массива deps выполняется функция эффекта. Чтобы изменить метод, вам нужно всего лишь передать [] в качестве второго аргумента useEffect() :
export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  }, []);

  return <div className="App">{count}</div>;
}
Приведем 4 случая использования useEffect:
  • Если второй параметр не передан: при обновлении любого состояния будет запущена функция эффекта useEffect.
useEffect(() => {
  setCount(count + 1);
});
  • Если второй параметр - это пустой массив: функция эффекта useEffect срабатывает только при монтировании и размонтировании.
useEffect(() => {
  setCount(count + 1);
}, []);
  • Если второй параметр представляет собой массив с одним значением: функция эффекта useEffect будет запускаться только при изменении значения.
useEffect(() => {
  setCount(count + 1);
}, [name]);
  • Если второй параметр представляет собой массив c несколькими значениями: функция эффекта useEffect будет запускаться при изменении хотя бы одного из значений из списка зависимостей.
useEffect(() => {
  setCount(count + 1);
}, [name, age]);

Ошибка: Отсутствие очистки побочных эффектов в useEffect

Проблема В классовых компонентах мы используем метод жизненного цикла componentWillUnmount() для очистки некоторых побочных эффектов, таких как таймеры, слушатели событий и т. д. Решение Из функции эффекта useEffect() может быть возвращена функция очистки, которая аналогична роли метода жизненного цикла componentWillUnmount():
useEffect(() => {
  // ...
  return () => clearInterval(id);
}, [name, age]);

Ошибка: Неправильное использование логических операторов

Проблема В синтаксисе JSX/TSX мы часто используем логические значения для управления отображаемыми элементами, и во многих случаях мы используем оператор && для обработки этой логики:
const count = 0;
const Comp = () => count && <h1>Chris1993</h1>;
Мы думаем, что в это время страница будет отображать пустой контент, но на самом деле на ней отобразится 0. Решение Причина в том, что ложное выражение приводит к тому, что элементы после && пропускаются, и будет возвращено значение ложного выражения. Поэтому нужно стараться написать условие оценки как можно более полным, не полагаясь на истинное и ложное логическое значение JavaScript для сравнения:
const count = 0;
const Comp = () => count > 0 && <h1>Chris1993</h1>;
Теперь страница будет отображать пустой контент, как и ожидается.

Ошибка: Типы просов компонента не типизированы

Проблема Если компоненты, разработанные разными членами команды, не имеют четко определенных типов для просов, то для коллег будет не очевидно, как использовать компоненты, например:
const UserInfo = (props) => {
  return (
    <div>
      {props.name} : {props.age}
    </div>
  );
};
Решение
  • Определить типы пропсов компонента, используя TypeScript.
// Классовые компоненты
interface AppProps {
  value: string;
}
interface AppState {
  count: number;
}
class App extends React.Component<AppProps, AppStore> {
  // ...
}

// Функциональные компоненты
interface AppProps {
  value?: string;
}
const App: React.FC<AppProps> = ({ value = "", children }) => {
  //...
};
  • Без использования TypeScript типы пропсов могут быть определены с помощью propTypes.
const UserInfo = (props) => {
  return (
    <div>
      {props.name} : {props.age}
    </div>
  );
};
UserInfo.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
};

Ошибка: Передача строк в качестве значений компонентам

Проблема Так как React имеет шаблонный синтаксис, очень похожий на HTML, часто бывает так, что числа передаются напрямую компонентам в пропсы, что приводит к неожиданному результату:
<MyComp count="99"></MyComp>
Сравнение props.count === 99 в компоненте MyComp вернет false. Решение Правильный способ должен заключаться в использовании фигурных скобок для передачи пропсов:
<MyComp count={99}></MyComp>
Передача строковых просов будет выглядеть следующим образом:
<MyComp count={"99"}></MyComp>

Ошибка: Имя компонента не начинается с заглавной буквы

Проблема Начинающие разработчики часто забывают называть свои компоненты с заглавной буквы. Компоненты, начинающиеся со строчной буквы в JSX/TSX, компилируются в элементы HTML, такие как <div /> для тегов HTML.
class myComponent extends React.component {}
Решение Просто измените первую букву на заглавную:
class MyComponent extends React.component {}

Ошибка: Неверная привязка события к элементу в классовых компонентах

Проблема
import { Component } from "react";

export default class HelloComponent extends Component {
  constructor() {
    super();
    this.state = {
      name: "John",
    };
  }
  update() {
    this.setState({ name: "Hello John!" });
  }

  render() {
    return (
      <div>
        <button onClick={this.update}>update</button>
      </div>
    );
  }
}
При нажатии на кнопку update консоль сообщит об ошибке, что невозможно прочитать свойства undefined (чтение setState) Решение Это происходит потому, что this не привязан к тому контексту, который мы ожидаем. Есть несколько решений:
  • Привязать контекст в конструкторе при помощи метода bind
constructor() {
  super();
  this.state = {
    name: "John"
  };
  this.update = this.update.bind(this);
}
  • Использовать стрелочные функции
update = () => {
  this.setState({ name: "Hello John!" });
};
  • Привязать прямо в функции рендеринга
<button onClick={this.update.bind(this)}>update</button>
  • Использовать стрелочные функции в функции рендеринга (не рекомендуется, т.к. это создает новую функцию каждый раз при рендеринге компонента, что влияет на производительность)
<button onClick={() => this.update()}>update</button>