Как обрабатывать события в React приложении

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

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

В JSX нам нужно обернуть выражения JavaScript фигурными скобками { }. Обычный JavaScript:
<button onclick="hitMe()">hit me</button>
В ReactJS:
<button onClick={hitMe}>hitme</button>
Рассмотрим подробнее, как присоединить обработчики событий в функциональных и классовых компонентах.

Функциональные компоненты

function Button() {
  function handleClick() {
    alert('some one clicked me')
  }
  
  return (
    <button onClick={handleClick}>Click me</button>
  )
}
В React нам нужно передать функцию обработчика событий.

Передача аргументов

Мы также можем передавать аргументы в функции обработчика событий, подобные этой.
function Button() {
  function handleClick(name) {
     alert(name)
  }

  return (
    <button onClick={()=>handleClick('react')}>Click me</button>
  )
}

Классовые компоненты

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: 0
    }
  }

  handleInc(){
    this.setState({
      num: this.state.num+1
    })
  }

  render(){
    return (
       <div>
        <h1>Counter</h1>
        <h3>{this.state.num}</h3>
        <button onClick={this.handleInc.bind(this)}>Increment</button>
      </div>
    )
  }
}
В компонентах на основе классов нам нужно привязать this к классу, иначе this будет привязано к global window object или в строгом режиме this будет undefined. Мы можем решить эту проблему с помощью стрелочных функций, потому что в стрелочных функциях this будет привязано к его внешнему контексту выполнения. Давайте модифицируем приведенный выше компонент с помощью стрелочных функций.
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: 0
    }
  }

  handleInc() {
    this.setState({
      num: this.state.num+1
    })
  }

  render(){
    return (
       <div>
        <h1>Counter</h1>
        <h3>{this.state.num}</h3>
        <button onClick={()=>this.handleInc()}>Increment</button>
      </div>
    )
  }
}

Второй способ использования class fields syntax

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: 0
    }
  }

  // стрелочная функция
  handleInc = () => {
    this.setState({
      num: this.state.num+1
    })
  }

  render() {
    return (
       <div>
        <h1>Counter</h1>
        <h3>{this.state.num}</h3>
        <button onClick={this.handleInc}>Increment</button>
      </div>
    )
  }
}

Объект события

Мы также можем использовать объект события таким образом в react.
function Link(){

  function handleClick(e){
     console.log(e);
     e.preventDefault();
  }

  return (
      <a href="#" onClick={handleClick} >Click me</a>
  )
}

Как работать с контекстом в 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.