Хотите узнать о том, что такое React, но вам всё никак не выпадает шанс изучить его? Или, может быть, вы уже пробовали освоить React, но не смогли толком понять? А может, вы разобрались с основами, но хотите привести в порядок знания? Эта статья написана специально для тех, кто положительно ответил хотя бы на один из этих вопросов. Сегодня мы создадим простой музыкальный проигрыватель, раскрывая основные концепции React по мере продвижения к цели.

image

Разобравшись с этим материалом, вы освоите следующее:

  • Компоненты React.
  • Рендеринг ReactDOM.
  • Классы компонентов и функциональных компоненты.
  • JSX.
  • Состояние (state).
  • Обработка событий.
  • Асинхронный метод setState.
  • Свойства (props).
  • Ссылки (refs).

Это — практически всё, что нужно знать для того, чтобы создавать и поддерживать React-приложения.

Предварительная подготовка


Рассмотрим такую ситуацию: к вам за помощью обращается маленький стартап. Они создали приятную страницу, пользуясь которой пользователи могут загружать в их сервис музыку и проигрывать её. Им хочется, чтобы вы сделали самое сложное — вдохнули в эту страницу жизнь.

Для начала создайте новую директорию проекта и добавьте туда три файла. Вот они на GitHub, а вот их код.

Файл app.css
body {
  background: #f9f9f9;
  font-family: 'Open Sans', sans-serif;
  text-align: center;
}

#container {
  position: relative;
  z-index: 2;
  padding-top: 100px;
}

.play {
  display: block;
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-bottom: 50px solid transparent;
  border-left: 60px solid #2c3e50;
  margin: 100px auto 50px auto;
  position: relative;
  z-index: 1;
  transition: all 0.3s;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
  left: 10px;
}

.play:before {
  content: '';
  position: absolute;
  top: -75px;
  left: -115px;
  bottom: -75px;
  right: -35px;
  border-radius: 50%;
  border: 10px solid #2c3e50;
  z-index: 2;
  transition: all 0.3s;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
}
.play:after {
  content: '';
  opacity: 0;
  transition: opacity 0.6s;
  -webkit-transition: opacity 0.6s;
  -moz-transition: opacity 0.6s;
}
.play:hover:before, .play:focus:before {
  transform: scale(1.1);
  -webkit-transform: scale(1.1);
  -moz-transform: scale(1.1);
}
.play.active {
  border-color: transparent;
}
.play.active:after {
  content: '';
  opacity: 1;
  width: 25px;
  height: 80px;
  position: absolute;
  right: 8px;
  top: -40px;
  border-right: 20px solid #2c3e50;
  border-left: 20px solid #2c3e50;
}

h1 {
  text-transform: uppercase;
  color: #34495e;
  letter-spacing: 2px;
  font-size: 2em;
  margin-bottom: 0;
}

canvas {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

audio {
  position: fixed;
  left: 10px;
  bottom: 10px;
  width: calc(100% - 20px);
}

Файл app.js
var ALPHA,
  AudioAnalyser,
  COLORS,
  MP3_PATH,
  NUM_BANDS,
  NUM_PARTICLES,
  Particle,
  SCALE,
  SIZE,
  SMOOTHING,
  SPEED,
  SPIN,
  TIMES_CALLED,
  ANALYSER;

NUM_PARTICLES = 150;

NUM_BANDS = 128;

TIMES_CALLED = 0;

SMOOTHING = 0.5;

MP3_PATH = 'music.mp3';

SCALE = {
  MIN: 5.0,
  MAX: 80.0
};

SPEED = {
  MIN: 0.2,
  MAX: 1.0
};

ALPHA = {
  MIN: 0.8,
  MAX: 0.9
};

SPIN = {
  MIN: 0.001,
  MAX: 0.005
};

SIZE = {
  MIN: 0.5,
  MAX: 1.25
};

COLORS = [
  '#69D2E7',
  '#1B676B',
  '#BEF202',
  '#EBE54D',
  '#00CDAC',
  '#1693A5',
  '#F9D423',
  '#FF4E50',
  '#E7204E',
  '#0CCABA',
  '#FF006F'
];
function getAnimation(file) {
  AudioAnalyser = (function() {
    AudioAnalyser.AudioContext = self.AudioContext || self.webkitAudioContext;

    AudioAnalyser.enabled = AudioAnalyser.AudioContext != null;

    function AudioAnalyser(audio, numBands, smoothing) {
      var src;
      this.audio = audio != null ? audio : new Audio();
      this.numBands = numBands != null ? numBands : 256;
      this.smoothing = smoothing != null ? smoothing : 0.3;
      this.audio = document.getElementById('audio');
      if (!this.audio) {
        return;
      }
      try {
        this.audio.src = window.URL.createObjectURL(file);
      } catch (err) {
        console.log(err);
      }
      this.context = new AudioAnalyser.AudioContext();
      this.jsNode = this.context.createScriptProcessor(2048, 1, 1);
      this.analyser = this.context.createAnalyser();
      this.analyser.smoothingTimeConstant = this.smoothing;
      this.analyser.fftSize = this.numBands * 2;
      this.bands = new Uint8Array(this.analyser.frequencyBinCount);
      this.audio.addEventListener(
        'play',
        (function(_this) {
          return function() {
            if (TIMES_CALLED === 1) {
              return;
            }
            ANALYSER.start();
            TIMES_CALLED++;
            _this.source = _this.context.createMediaElementSource(_this.audio);
            _this.source.connect(_this.analyser);
            _this.analyser.connect(_this.jsNode);
            _this.jsNode.connect(_this.context.destination);
            _this.source.connect(_this.context.destination);
            return (_this.jsNode.onaudioprocess = function() {
              _this.analyser.getByteFrequencyData(_this.bands);
              if (!_this.audio.paused) {
                return typeof _this.onUpdate === 'function'
                  ? _this.onUpdate(_this.bands)
                  : void 0;
              }
            });
          };
        })(this)
      );
    }

    AudioAnalyser.prototype.start = function() {
      return this.audio.play();
    };

    AudioAnalyser.prototype.stop = function() {
      return this.audio.pause();
    };

    return AudioAnalyser;
  })();

  Particle = (function() {
    function Particle(x1, y1) {
      this.x = x1 != null ? x1 : 0;
      this.y = y1 != null ? y1 : 0;
      this.reset();
    }

    Particle.prototype.reset = function() {
      this.level = 1 + floor(random(4));
      this.scale = random(SCALE.MIN, SCALE.MAX);
      this.alpha = random(ALPHA.MIN, ALPHA.MAX);
      this.speed = random(SPEED.MIN, SPEED.MAX);
      this.color = random(COLORS);
      this.size = random(SIZE.MIN, SIZE.MAX);
      this.spin = random(SPIN.MAX, SPIN.MAX);
      this.band = floor(random(NUM_BANDS));
      if (random() < 0.5) {
        this.spin = -this.spin;
      }
      this.smoothedScale = 0.0;
      this.smoothedAlpha = 0.0;
      this.decayScale = 0.0;
      this.decayAlpha = 0.0;
      this.rotation = random(TWO_PI);
      return (this.energy = 0.0);
    };

    Particle.prototype.move = function() {
      this.rotation += this.spin;
      return (this.y -= this.speed * this.level);
    };

    Particle.prototype.draw = function(ctx) {
      var alpha, power, scale;
      power = exp(this.energy);
      scale = this.scale * power;
      alpha = this.alpha * this.energy * 1.5;
      this.decayScale = max(this.decayScale, scale);
      this.decayAlpha = max(this.decayAlpha, alpha);
      this.smoothedScale += (this.decayScale - this.smoothedScale) * 0.3;
      this.smoothedAlpha += (this.decayAlpha - this.smoothedAlpha) * 0.3;
      this.decayScale *= 0.985;
      this.decayAlpha *= 0.975;
      ctx.save();
      ctx.beginPath();
      ctx.translate(this.x + cos(this.rotation * this.speed) * 250, this.y);
      ctx.rotate(this.rotation);
      ctx.scale(
        this.smoothedScale * this.level,
        this.smoothedScale * this.level
      );
      ctx.moveTo(this.size * 0.5, 0);
      ctx.lineTo(this.size * -0.5, 0);
      ctx.lineWidth = 1;
      ctx.lineCap = 'round';
      ctx.globalAlpha = this.smoothedAlpha / this.level;
      ctx.strokeStyle = this.color;
      ctx.stroke();
      return ctx.restore();
    };

    return Particle;
  })();

  Sketch.create({
    particles: [],
    setup: function() {
      var analyser, error, i, intro, j, particle, ref, warning, x, y;
      for (i = j = 0, ref = NUM_PARTICLES - 1; j <= ref; i = j += 1) {
        x = random(this.width);
        y = random(this.height * 2);
        particle = new Particle(x, y);
        particle.energy = random(particle.band / 256);
        this.particles.push(particle);
      }
      if (AudioAnalyser.enabled) {
        try {
          analyser = new AudioAnalyser(MP3_PATH, NUM_BANDS, SMOOTHING);
          analyser.onUpdate = (function(_this) {
            return function(bands) {
              var k, len, ref1, results;
              ref1 = _this.particles;
              results = [];
              for (k = 0, len = ref1.length; k < len; k++) {
                particle = ref1[k];
                results.push((particle.energy = bands[particle.band] / 256));
              }
              return results;
            };
          })(this);
          analyser.audio = window.audio;
          ANALYSER = analyser;
          intro = document.getElementById('intro');
          intro.style.display = 'none';
          if (
            /Safari/.test(navigator.userAgent) &&
            !/Chrome/.test(navigator.userAgent)
          ) {
            warning = document.getElementById('warning2');
            return (warning.style.display = 'block');
          }
        } catch (_error) {
          error = _error;
        }
      } else {
        warning = document.getElementById('warning1');
        return (warning.style.display = 'block');
      }
    },
    draw: function() {
      var j, len, particle, ref, results;
      this.globalCompositeOperation = 'lighter';
      ref = this.particles;
      results = [];
      for (j = 0, len = ref.length; j < len; j++) {
        particle = ref[j];
        if (particle.y < -particle.size * particle.level * particle.scale * 2) {
          particle.reset();
          particle.x = random(this.width);
          particle.y =
            this.height + particle.size * particle.scale * particle.level;
        }
        particle.move();
        results.push(particle.draw(this));
      }
      return results;
    }
  });
}

function handleFileSelect(evt) {
  var files = evt.target.files;
  getAnimation(files[0]);
}

getAnimation(null);

document
  .getElementById('files')
  .addEventListener('change', handleFileSelect, false);

Файл index.html
<link rel="stylesheet" href="app.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,800">
<div id="container">
  <div id="hook"></div>
  <h1>Play Music</h1>
  <input type="file" id="files" name="files[]" multiple />
</div>
<script crossorigin src="https://unpkg.com/react@15/dist/react.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script>
<script src="https://soulwire.github.io/sketch.js/js/sketch.min.js"></script>
<script src="app.js"></script>
<script type="text/babel">
  // Тут будет код на React.
</script>

Для успешного прохождения этого руководства вам понадобится свежая версия браузера Google Chrome, иначе не будут работать анимации. Выражаем благодарность Стивену Фабре за CSS для кнопки проигрывания и Джастину Виндлу за код визуализации (оригинал можно посмотреть здесь).

Откройте index.html в редакторе кода и в браузере. Пришло время познакомиться с React.

Что такое React?


React — это инструмент для создания пользовательских интерфейсов. Его главная задача — обеспечение вывода на экран того, что можно видеть на веб-страницах. React значительно облегчает создание интерфейсов благодаря разбиению каждой страницы на небольшие фрагменты. Мы называем эти фрагменты компонентами.

Вот пример разбивки страницы на компоненты:


Каждый выделенный фрагмент страницы, показанной на рисунке, считается компонентом. Но что это значит для разработчика?

Что такое компонент React?


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

Для формирования страницы мы вызываем эти функции в определённом порядке, собираем вместе результаты вызовов и показываем их пользователю.

Напишем компонент внутри тега <script> файла index.html с type, установленным в "text/babel":

<script type="text/babel">
  function OurFirstComponent() {
    return (
      // Тут будет код, представляющий элемент пользовательского интерфейса
    );
  }
</script>

Когда мы вызываем функцию OurFirstComponent(), в ответ приходит фрагмент страницы.

Функции можно писать и так:

const OurFirstComponent = () => {
  return (
    // То, что нужно для создания компонента, идёт сюда
  );
}

React использует язык программирования, называемый JSX, который похож на HTML, но работает внутри JavaScript, что отличает его от HTML.

Вы можете добавить сюда обычный HTML для того, чтобы он попал в пользовательский интерфейс:

<script type="text/babel">
  function OurFirstComponent() {
    return (
      <h1>Hello, I am a React Component!</h1>
    );
  }
</script>

Когда мы вызываем функцию OurFirstComponent(), она возвращает фрагмент JSX-кода. Мы можем использовать так называемый ReactDOM для вывода того, что представляет этот код, на страницу:

<script type="text/babel">
  function OurFirstComponent() {
    return (
      <h1>Hello, I am a React Component!</h1>
    );
  }
  const placeWeWantToPutComponent = document.getElementById('hook');
  ReactDOM.render(OurFirstComponent(), placeWeWantToPutComponent);
</script>

Теперь тег <h1> окажется внутри элемента с ID hook. Когда вы обновите страницу браузера, она должна выглядеть так:


Можно и написать собственный компонент на JSX. Делается это так:

ReactDOM.render(<OurFirstComponent />, placeWeWantToPutComponent);

Это — стандартный подход — вызывать компоненты так, будто вы работаете с HTML.

Сборка компонентов


Компоненты React можно помещать в другие компоненты.

<script type="text/babel">
  function OurFirstComponent() {
    return (
      <h1>I am the child!</h1>
    );
  }
  function Container() {
    return (
      <div>
        <h1>I am the parent!</h1>
        <OurFirstComponent />
      </div>
    );
  }
  const placeWeWantToPutComponent = document.getElementById('hook');
  ReactDOM.render(<Container />, placeWeWantToPutComponent);
</script>

Вот что выведет вышеприведённый код:


Именно так страницы собирают из фрагментов, написанных на React — вкладывая компоненты друг в друга.

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


До сих пор мы писали компоненты в виде функций. Их называют функциональными компонентами. Однако, компоненты можно писать и иначе, в виде классов JavaScript. Их называют классами компонентов.

class Container extends React.Component {
  render() {
    return (
      <div>
        <h1>I am the parent!</h1>
        <OurFirstComponent />
      </div>
    );
  }
}
const placeWeWantToPutComponent = document.getElementById('hook');
ReactDOM.render(<Container />, placeWeWantToPutComponent);

Классы компонентов должны содержать функцию, называемую render(). Эта функция возвращает JSX-код компонента. Их можно использовать так же, как функциональные компоненты, например, обращаясь к ним с помощью конструкции:

<AClassComponent />

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

JavaScript в JSX


В JSX-код можно помещать переменные JavaScript. Выглядит это так:

class Container extends React.Component {
  render() {
    const greeting = 'I am a string!';
    return (
      <div>
        <h1>{ greeting }</h1>
        <OurFirstComponent />
      </div>
    );
  }
}

Теперь текст «I am a string» окажется внутри тега <h1>.

Кроме того, тут можно делать и вещи посложнее, вроде вызовов функций:

class Container extends React.Component {
  render() {
    const addNumbers = (num1, num2) => {
      return num1 + num2;
    };
    return (
      <div>
        <h1>The sum is: { addNumbers(1, 2) }</h1>
        <OurFirstComponent />
      </div>
    );
  }
}

Вот как будет выглядеть страница после обработки вышеприведённого фрагмента кода:


Подводные камни JSX


Переименуйте OurFirstComponent() в PlayButton. Нам надо, чтобы этот компонент возвращал следующее:

<a href="#" title="Play video" class="play" />

Однако, тут мы сталкиваемся с проблемой: class — это ключевое слово JavaScript, поэтому использовать его мы не можем. Как же назначить класс play элементу <a>?

Для того, чтобы этого добиться, нужно воспользоваться свойством className:

<script type="text/babel">
  function PlayButton() {
    return <a href="#" title="Play video" className="play" />;
  }
  class Container extends React.Component {
    render() {
      return (
        <div>
          <PlayButton />
        </div>
      );
    }
  }
  const placeWeWantToPutComponent = document.getElementById('hook');
  ReactDOM.render(<Container />, placeWeWantToPutComponent);
</script>

Особенности создаваемого компонента


Компоненты, основанные на классах, могут хранить информацию о текущей ситуации. Эта информация называется состоянием (state), она хранится в JS-объекте. В нижеприведённом коде показан объект, представляющий состояние нашего компонента. Его ключ — это isMusicPlaying, с ним связано значение false. Этот объект назначен this.state в методе constructor, который вызывается при первом использовании класса.

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  
  render() {
    return (
      <div>
        <PlayButton />
      </div>
    );
  }
}

Метод constructor компонента React всегда должен вызвать super(props) прежде чем выполнять что угодно другое.
Итак, а что нам делать с этим «состоянием»? Зачем оно придумано?

Изменение компонента React на основе его состояния


Состояние — это инструмент, позволяющий обновлять пользовательский интерфейс, основываясь на событиях. Тут мы будем использовать состояние для изменения внешнего вида кнопки проигрывания музыки, основываясь на щелчке по ней. Кнопка может отображаться в одном из двух вариантов. Первый указывает на возможность запуска проигрывания, второй — на то, что музыка проигрывается, и этот процесс можно приостановить. Когда пользователь щёлкает по кнопке, меняется состояние, а затем обновляется пользовательский интерфейс.

Вот с чего мы начнём. Узнаем состояние компонента с помощью конструкции this.state. В следующем коде мы проверяем состояние и используем его для принятия решения о том, какой текст показать пользователю.

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  render() {
    const status = this.state.isMusicPlaying ? 'Playing' : 'Not playing';
    return (
      <div>
        <h1>{ status }</h1>
        <PlayButton />
      </div>
    );
  }
}

В функции render ключевое слово this всегда ссылается на компонент, внутри которого она находится.


Всё это не особенно полезно, если у нас нет способа изменять this.state.isMusicPlaying.

Как компонент реагирует на события?


Пользователь может взаимодействовать с компонентом, щёлкая по кнопке проигрывания музыки. Мы хотим реагировать на эти события. Делается это посредством функции, которая занимается обработкой событий. Эти функции так и называются — обработчики событий.

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  handleClick(event) {
    // Отреагировать на щелчок мышью
  };
  render() {
    let status = this.state.isMusicPlaying 
    ? 'Playing :)' 
    : 'Not playing :(';
    return (
      <div>
        <h1 onClick={this.handleClick.bind(this)}>{ status }</h1>
        <PlayButton />
      </div>
    );
  }
}

Когда пользователь щёлкает по тексту, представленному тегом <h1>, компонент вызывает функцию handleClick. Функция получает объект события в качестве аргумента, а это значит, что она может, при необходимости, им пользоваться.

Мы используем метод .bind функции handleClick для того, чтобы ключевое слово this ссылалось на весь компонент, а не только на <h1>.

Как должен работать компонент


Когда меняется состояние компонента, он снова вызовет функцию render. Мы можем изменить состояние с помощью this.setState(), если передадим этой функции объект, представляющий новое состояние. Компонент на странице всегда будет представлять своё текущее состояние. React самостоятельно обеспечивать такое поведение компонентов.

handleClick() {
    if (this.state.isMusicPlaying) {
      this.setState({ isMusicPlaying: false });
    } else {
      this.setState({ isMusicPlaying: true });
    }
  };

Теперь, разобравшись с этим механизмом, займёмся обработкой щелчка по кнопке.

Обмен данными между компонентами


Компоненты могут «общаться» друг с другом. Посмотрим, как это работает. Мы можем сообщить PlayButton, проигрывается музыка или нет, используя так называемые свойства (props). Свойства — это информация, коллективно используемая родительским компонентом и компонентами-потомками.

Свойств в JSX выглядят так же, как HTML-свойства. Мы назначаем PlayButton свойство, называемое isMusicPlaying, которое является тем же самым, что и isMusicPlaying в this.state.

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  handleClick() {
    if (this.state.isMusicPlaying) {
      this.setState({ isMusicPlaying: false });
    } else {
      this.setState({ isMusicPlaying: true });
    }
  };
  render() {
    return (
      <div>
        <PlayButton isMusicPlaying={this.state.isMusicPlaying} />
      </div>
    );
  }
}

Когда состояние Container меняется, свойство PlayButton также меняется, и функция PlayButton вызывается снова. Это означает, что вид компонента на экране обновится.

Внутри PlayButton мы можем реагировать на изменения, так как PlayButton принимает свойства как аргумент:

function PlayButton(props) {
  const className = props.isMusicPlaying ? 'play active' : 'play';
  return <a href="#" title="Play video" className={className} />;
}

Если мы поменяем состояние на this.state = { isMusicPlaying: true }; и перезагрузим страницу, на ней должна появиться кнопка паузы:


События как свойства


Свойства необязательно должны представлять собой какие-то данные. Они могут быть и функциями.

function PlayButton(props) {
  const className = props.isMusicPlaying ? 'play active' : 'play';
  return <a onClick={props.onClick} href="#" title="Play video" className={className} />;
}
class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  handleClick() {
    if (this.state.isMusicPlaying) {
      this.setState({ isMusicPlaying: false });
    } else {
      this.setState({ isMusicPlaying: true });
    }
  };
  render() {
    return (
      <div>
        <PlayButton 
          onClick={this.handleClick.bind(this)} 
          isMusicPlaying={this.state.isMusicPlaying} 
        />
      </div>
    );
  }
}

Теперь, когда мы щёлкаем по кнопке PlayButton, она меняет состояние Container, которое изменит props PlayButton, что приведёт к обновлению кнопки на странице.

Неприятная особенность setState


При вызове setState изменение состояния не производится мгновенно. React ждёт немного для того, чтобы увидеть, не нужно ли внести ещё какие-то изменения, и только потом производит изменение состояния. Это означает, что нельзя точно знать, каким будет состояние компонента после вызова setState.

Поэтому вот так поступать не следует:

handleClick() {
  this.setState({ isMusicPlaying: !this.state.isMusicPlaying });
};

Если вы изменяете состояние, основываясь на предыдущем состоянии, нужно делать это по-другому. А именно, следует передать setState функцию, а не объект. Эта функция принимает старое состояние как аргумент и возвращает объект, представляющий новое состояние.

Выглядит это так:

handleClick() {
  this.setState(prevState => {
    return { 
      isMusicPlaying: !prevState.isMusicPlaying   
    };
  });
};

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

Что такое ссылки?


Пришло время включить музыку. Для начала добавим тег <audio>:

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  handleClick() {
    this.setState(prevState => {
      return { 
        isMusicPlaying: !prevState.isMusicPlaying   
      };
    });
  };
  render() {
    return (
      <div>
        <PlayButton 
          onClick={this.handleClick.bind(this)} 
          isMusicPlaying={this.state.isMusicPlaying} 
        />
        <audio id="audio" />
      </div>
    );
  }
}

Нам нужен способ обратиться к тегу <audio> и вызвать либо его метод play(), либо pause(). Сделать это можно с помощью конструкции document.getElementById('audio').play(), но React предлагает кое-что получше.

Мы назначаем элементу атрибут, называемый ref, который принимает функцию. Эта функция, в качестве первого аргумента, принимает элемент <audio>, и присваивает его this.audio.

<audio id="audio" ref={(audioTag) => { this.audio = audioTag }} />
Эта функция будет вызываться каждый раз, когда выводится Container, то есть, this.audio всегда будет в актуальном состоянии и будет указывать на тег <audio>.
Теперь мы можем запускать и приостанавливать воспроизведение музыки:

handleClick() {
  if (this.state.isMusicPlaying) {
    this.audio.pause();
  } else {
    this.audio.play();
  }
  this.setState(prevState => {
    return { 
      isMusicPlaying: !prevState.isMusicPlaying   
    };
  });
};

Выгрузим на страницу музыкальный файл (лучше — в формате .mp3) с использованием кнопки Choose files, нажмём на кнопку запуска воспроизведения и послушаем музыку.

React за пределами index.html


Как вы, возможно, догадались, React-код не должен «жить» исключительно внутри тега <script>. React поддерживает множество конфигураций сборки. К счастью, с помощью инструментов вроде Create React App всю рутинную работу по формированию каркаса приложения можно автоматизировать. Установите create-react-app, создайте новый проект, посмотрите руководство и приступайте к работе с JS-файлами в папке проекта src, применяя все те знания по React, которые вы сегодня получили.

Итоги


Примите поздравления! Вы сделали первые шаги в сфере разработки React-приложений, освоили основы, позволяющие приступать к созданию собственных проектов и продуктивно учиться дальше.

Уважаемые читатели! Если сегодня состоялось ваше первое знакомство с React — просим поделиться впечатлениями.

Комментарии (55)


  1. sinneren
    23.11.2017 14:31

    Ну, для начала JSX не «язык программирования». Реакт знаю плохо, прав ли я, что ref плохой способ обратиться к элементу, и имеет место лишь в определенных случаях? (я понимаю, что тут пример для ньюби и это просто пример, потому он и указан). Если прав, то было бы круто услышать об иных способах. Ну и ждём-с статью про подробный разбор состояний, поднятие, редакс и т.п.


    1. mayorovp
      23.11.2017 14:41

      ref — канонический способ обратиться к элементу. Но, конечно же, число случаев когда его следует использовать — ограничено.

      Просто потому что в большинстве случаев нет никакого смысла обращаться напрямую к элементу.


    1. VolCh
      26.11.2017 10:36

      Чем JSX не язык программирования?


      1. sinneren
        26.11.2017 11:53
        -1

        Тем же, чем и тайпскрипт. В общем-то вы и сами на свой вопрос отвечали ниже…


      1. samizdam
        26.11.2017 19:06

        Тем, что язык разметки?


        1. VolCh
          27.11.2017 10:22

          JSX — надмножество JS, это не XML-подобный язык, а JS c возможностью включать XML-подобный синтаксис для некоторых целей, например с целью трансляции в вызовы React.createElement() или любые другие на ваше усмотрение.


  1. BOM
    23.11.2017 16:06

    Единственное, что меня смущает в туториалах реакта, это тотальное смешивание html и js кода. Разве это не считается bad practice? Можно ли обойтись совсем без смешивания одного с другим или это неотъемлемая часть JSX?


    1. mayorovp
      23.11.2017 16:38

      Как бы JSX — это и есть почти-html внутри js.


      1. zorn_v
        23.11.2017 17:04

        Да срань это господня, простите.


    1. defaultvoice
      23.11.2017 18:40

      JSX это НЕ html, он только притворяется им (с помощью babel). Это просто удобное представление для createElement(component, props, ...children).


      1. mayorovp
        24.11.2017 09:04

        Тем не менее, он:

        1. синтаксически похож на html;
        2. предназначен для описания желаемого DOM — то есть для того же самого для чего предназначен html.


        1. faiwer
          24.11.2017 09:26

          1. Не только DOM. Вложенные компоненты тоже описываются как XML-элементы. При желании можно вообще никакого DOM не создавать, а делать всякие sideEffect-ы.


        1. VolCh
          26.11.2017 10:42

          Формально, JSX — это надмножество, расширение JavaScript, дополняющего его возможностью использовать XML-подобный синтаксис для некоторых целей.


    1. faiwer
      23.11.2017 19:04

      Если вы писали на knockout или angular, то вспомните все эти большие шаблоны с data-bind, v-
      & angular attributes. Где таки довольно много логики, бывают фильтры и пр… В запущенном случае там находятся даже двух-трёх этажные js-выражения. И не всегда удаётся провести удачную грань, что тащить в класс, а что оставить в шаблоне. В случае React и JSX, то этот самый шаблон и есть React класс с JSX. Можно воспринимать это как язык шаблонов с расширением js, jsx. JS-шаблонизатор, если хотите.


      В то время как логика работы с данными лежит, как правило, отдельно. И там нет JSX. В случае redux это action-ы, actionCreator-ы, reducer-ы, connect-классы, селекторы и пр…
      Если вам правда интересно, то вот суть подхода.


      1. leschenko
        23.11.2017 23:38

        Мы используем knockout. Да, data-bind'ов очень много. Я бы даже сказал что на этом все и построено. Но! Мы можем отделить ViewModel от View и от Model. У нас одна и та же ViewModel может быть привязана к разным View. И это работает.

        Как быть в случае с спагетти от React?
        Как отделить этот HTML-не HTML от бизнес логики? Как сделать так, чтобы одни и те же данные отображались по разному в разных частях общей View (компонента если хотите)?


        1. faiwer
          24.11.2017 07:19

          В большинстве больших приложений с React используется внешний state manager. Например Redux или MobX. Данные хранятся там. Взаимодействие с ними хранится где либо ещё. В случае redux работа с данными и вообще вся не UI логика лежит во множестве слоёв: action-ы, actionCreator-ы, reducer-ы, selector-ы, container-компоненты. И там нет JSX. Как правильно это самостоятельные JS файлы без единой XML строчки.


          А React компонент представляет из себя уже UI часть для этих данных. Он их получает свыше. Менять он их не умеет, но умеет, гхм, дёргать рычаги, которые приведут к их изменению. Опять же, рычаги эти проброшены сверху. В общем никакой прямой обработки данных. Что-то вроде шаблонизатора.


          ^ всё это немного упрощённо, лишь по сути. И да, перейдите по моей ссылке. Многое станет понятнее, я думаю.


          1. leschenko
            24.11.2017 10:10

            Не спора ради, а разобраться для…
            Правильно ли я понимаю что React это инструмент для создания только View. За остальное (если здесь уместно говорить про MVVM, то VM и M) отвечают другие инструменты. Есть ли здесь (или около React) что-то напоминающее Binding'и и если есть, то каким инструментом организовано?


            1. mayorovp
              24.11.2017 10:27

              Есть ли здесь (или около React) что-то напоминающее Binding'и и если есть, то каким инструментом организовано?

              Так JSX же...


              1. leschenko
                24.11.2017 10:29

                Мне показалось что это очень отдаленный односторонний биндинг. Как быть с друсторонними?


                1. mayorovp
                  24.11.2017 10:32

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

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


                1. VolCh
                  26.11.2017 10:45

                  Двустороннего нет и обычно попытки его как-то неявно реализовать считаются антипаттерном в мире реакта.


            1. faiwer
              24.11.2017 10:40

              Есть ли здесь (или около React) что-то напоминающее Binding'и и если есть, то каким инструментом организовано?

              Да. Просто пишем что-то вроде onClick="this.props.open". В финале это будет addEventListener, а не аттрибут. Если скажем нужно onChange на <input/> или <textarea/>, то тут начинаются сложности, т.к. никаких удобных механизмов работы с этим из коробки нет. Более того onChange ведёт себя как onInput (oO). Есть два подхода для работы с контролами. Оба мне не нравятся. Но, что есть, то есть. В общем дву-направленных bind-ов для работы с контролами тут нет. Из коробки. Люди пишут свои абстракции, либо используют что-нибудь типа redux-form.


        1. faiwer
          24.11.2017 07:49

          Попробую на примере. Вот есть у вас скажем список чего-то, и есть снизу кнопка "добавить запись". И есть кнопки "удалить" на панели каждого элемента списка. Типичный такой TODO, только ещё проще. В итоге у нас:


          1. есть презентационные компоненты: Item, List, AddButton. Они с JSX, т.е. там внутри есть XML. Они на входе попросят данные для отображения и 2 функции: для удаления и для добавления. Им "не интересно" ни откуда данные пришли, ни что это за функции. Они просто внутри своего XML пропишут onClick={this.props.add}, аналог data-bind="click: add".
          2. у нас есть state. Это древо всех данных приложения. И в нём есть тот самый список. Данные просто данные, т.е. сериализуемые. Никаких функций, хитрых геттеров и сеттеров. Нет, просто тупо данные.
          3. у нас есть action-ы (опустим для простоты actionCreator-ы). Это такие простые объекты типа { type: 'ADD_NEW_ITEM' } & { type 'DEL_ITEM', idx: 12 }. Они сами на данные не влияют. Они просто объекты. Они высказывают намерения. Или как это сказать… Это что-то вроде "эй store, держи мой ADD_NEW_ITEM. обработай его как надо". Причём о store они не знают. Это просто тупо сериализуемые объекты-намерения. Их можно подготовить, к примеру, но не использовать. Просто объекты.
          4. у нас есть reducer-ы. Это чистые функции, которые возьмут старый store, посмотрят на переданный туда action и сделают новый store, но уже с запрошенными изменениями. Вот тут как раз вся бизнес логика по изменению данных приложения. Тут не только JSX нет и в помине, тут вообще весь код… своеобразный. Иммутабельность же.
          5. у нас есть container-компоненты. Они имеют связь со store. По сути store это ko.observable. И container-компоненты на него subscribe-ны. Если store меняется, то они получают новые данные store из него. Их задача — получить данные из store, выдрать из них только то, что нужно конкретно взятому компоненту из пункта 1, и подготовить все эти add и delItem методы для него же. Эти add и delItem-ы опять же, будут просто толкать в store (в его reducer) все те action-ы из пункта 3. Т.е. и тут бизнес логики практически нуль. Подготовив все эти данные container-компонент передаёт их своему приемнику, т.е. презентационному компоненту.

          Наверное сильно запутал. Ну там всё не настолько просто, чтобы в паре предложений описать. В общем суть — нет в больших React приложениях никакого нагромождения XML внутри JS, так, чтобы бизнес-логика и вьюхи были рядом. Ещё суть — если вы бросите knockout и возьмёте стандартную связку react + redux, то последнее, что станет для вас проблемой, это этот несчастный JSX. Мат трёхэтажный у вас будет стоять от огромного количества бойлерплейта (мне кажется порой раз в 15 больше суеты чем в knockout-е), и вырвиглазной (имхо) работы с immutable значениями (ну не haskell у нас, не haskell, как умеем, как могём). А когда что-нибудь начнёт тормозить, то пойдут и нормализация данных в store, и selector-ы (мемоизированные функции) и прочая чертовщина. В общем прямолинейный в knockout.js js-код будет разбросан по десяткам файлов и вообще даже близко не будет напоминать обычное словесное описание того, что вам нужно. Тут конвеерная несколькозвенная обработка изменения иммутабельного состояния. Никаких больше: this.list.add(newItem). Теперь это будет через карусель: actionCreator -> action -> reducer -> mapStateToProps -> view.


          Из плюшек же: всё работает очень предсказуемо и отслеживаемо. Вот прямо до предела. Вплоть до того, что можно перезагружать страницу целиком и на экране ничего не поменяется, т.к. данные в store те же (скажем localStorage) и всё остальное лишь чистые функции по работе с ними. Тестируется опять же на раз два.


          У меня бывало уходили часы работы на поиск хитрого бага в сложном knockout приложении (3-4 года работы в одно рыло, большая кодовая база, много legacy). Потому что stack-трейсы для асинхронных observable бесполезны. А если там 1 computed зависит от 2-го, а 2-й от 3-го и они ещё разных типов, то во-первых — это всё работает непредсказуемо и ну очень непонятно. Последовательность пересчёта всего этого клубка (а knockout располагает к сложным клубкам) может быть разной в зависимости от нюансов. Сами эти связи могут быть совершенно не очевидными, стоит только отвлечься и забыть контекст. Никогда нельзя сказать, что вот тут у нас сложный баг, давайте просто посмотрим что от чего зависит… потому что это может быть ну очень непросто и в каждом модуле устроено по своему. Вспоминаю сейчас и испарина на лбу.


          Поработав 1 год на redux+react и 3 на knockout, я бы для больших проектов выбирал react+redux, т.к. такой проект спустя годы будет иметь меньше технических долгов и куда проще поддерживаться. А vue (вместо knockout)-а для мелких и средних проектов, чтобы не утонуть в этом бойлерплейте.


          Всё вышеописанное лишь моё ИМХО. Никому свою точку зрения не навязываю )


          1. leschenko
            24.11.2017 10:14

            Спасибо. Этот комментарий рассказал больше чем сама статья.


            1. VolCh
              26.11.2017 10:48

              Комментарий рассказывает в основном не о реакте, а о редаксе :)


        1. mayorovp
          24.11.2017 09:06

          React — это библиотека для построения View и только View. Для других частей предполагается использовать другие библиотеки.


    1. VolCh
      26.11.2017 11:01

      JSX и есть возможность смешивать JS и XML. Вы сами определяете, где смешивать, а где нет. Это с одной стороны. С другой, в props и context (что-то вроде DI-контейнера) компоненту можно передавать любое JS значение, в том числе полноценные объекты и функции, реализующие любые сайд-эффекты, используя компоненты в качестве тупых шаблонизаторов. .state не является обязательным для использования, в некоторых практиках его использование вообще запрещено. Но ответственность за своевременный ререндеринг тогда полностью ложится на вас.


  1. zorn_v
    23.11.2017 16:26
    +1

    Честно говоря не понимаю хайпа вокруг реакта. Для меня мешанина хтмл с жс, называемая «правильным подходом» как то отталкивает.
    И очень удивлен почему НИ РАЗУ не видел статью на хабре по emberjs. Даже грешным делом сам хотел написать, но все времени нет и т.п.

    Vue с компонентным подходом мне больше импонирует (может потому что фарш не устраивает), чем эта модная «jQuery лапша» именуемая гордо РЕАКТ и JSX.


    1. ru_vds Автор
      23.11.2017 16:27

      Была статья 27 октября в нашем блоге Ember.js: отличный фреймворк для веб-приложений


      1. zorn_v
        23.11.2017 16:59

        Пропустил. Просто в глазах РЯБИТ от этого реакта на хабре


      1. zorn_v
        23.11.2017 17:22

        К сожалению не могу комментить там.
        Первое — там обычный «хеллоу ворлд» который можно почитать и на самом сайте (ну может у вас чуть другой).
        Я планировал статью немного другого плана. Типа как у Symfony раньше была — «Зачем нужен фреймфорк».
        Ну и самое главное не упомянули про emberobserver.com
        А я например через него узнал про такую замечательную штуку как PouchDB (искал адаптеры для баз).


    1. mayorovp
      23.11.2017 16:37

      Во Vue тоже мешанина, только в другую сторону: код на js пишется прямо в html-файлах. :-(


      1. zorn_v
        23.11.2017 16:57

        Не, там хотя бы разделяешь <template> <script> и <style>
        И этот подход по мне правильный по части компонентов.


      1. zorn_v
        23.11.2017 20:21

        Хотя может мы о разных подходах говорим. Я про однофайловые компоненты ru.vuejs.org/v2/guide/single-file-components.html


        1. faiwer
          24.11.2017 07:57

          О разном вы говорите. Я думаю mayorovp говорит про все "v-" аттрибуты. Типичный презентационный React компонент с JSX это то, что во vue лежит внутри template.


          1. mayorovp
            24.11.2017 09:09

            Нет, я как раз об однофайловых компонентах и говорил. Казалось бы, столько усилий потратили чтобы вынести js в отдельные файлы… и вот на тебе.


    1. justboris
      23.11.2017 20:56

      мешанина хтмл с жс

      Легко обозвать незнакомую технологию мешаниной и тем самым оправдать своё невежество.


      Гораздо сложнее понять, как так вышло, что 3 года назад такой подход появился и до сих пор успешно используется.


      1. zorn_v
        23.11.2017 21:49

        Легко обозвать незнакомого человека невеждой, познав одну технологию и проповедовать все что она дает (своих мыслей то нет).

        Гораздо сложнее понять что существуют другие, более удобные технологии.
        Но это же реакт и фасебук да?


        1. justboris
          24.11.2017 12:56

          Легко обозвать незнакомого человека невеждой, познав одну технологию

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


          Но это же реакт и фасебук да?

          А Angular это Google. Но почему-то не так успешно летит Angular, несмотря на поддержку большой компании.


        1. VolCh
          26.11.2017 11:05

          Я использовал многие технологии на фронте за 20 лет в веб-разработке. Лично мне React кажется наиболее удобным.


  1. romy4
    23.11.2017 18:34

    плохой совет:

    onClick={this.handleClick.bind(this)}
    

    бинд внутри рендера делать категорически не стоит.


    1. Ungla
      26.11.2017 15:57

      «категорически не стоит», было бы хорошо прокомментировать почему, или дать ссылку на man, но я предпочёл бы комментарий от живого опытного человека. Я во многих уроках встречал такую строчку, но первый раз слышу почему этого не стоит делать. Не сочтите за труд, напишите пожалуйста почему. Спасибо.


      1. faiwer
        26.11.2017 16:09

        .bind порождает новую функцию. Это всегда будет новый экземпляр. В данном случае это будет происходить при каждом вызове .render(). И тот компонент которому этот onClick= назначен всегда будет получать новый экземпляр onClick в своих props. И если такой компонент это pureComputed то он будет всегда заного рендерить свой virtualDom. Без разбору, всегда. Надо не надо… будет. Потому что shallow-проверка будет сверять по ===.


        1. Ungla
          26.11.2017 16:28

          Спасибо. По реакту немного основательных статей, а бестпрактисес статей ещё меньше. После Python где всё зарегламентировано по самое небалуйся жутко непривычно, постоянно кажется, и часто так и есть, что позоришься в коде.)


  1. Writerim
    24.11.2017 06:22

    Я толерантный ко всем фреймворкам: хорошим, не очень, React.


  1. skyline405
    24.11.2017 08:27

    Функции можно писать и так:

    const OurFirstComponent = () => {
      return (
        // То, что нужно для создания компонента, идёт сюда
      );
    }
    



    stateless компоненты можно писать с еще чуть меньшей вложенностью)

    const OurFirstComponent = () => (
        <h1>Hello world</h1>
    );
    


    1. faiwer
      24.11.2017 09:30

      Ваш случай можно ещё больше упростить и слегка ускорить :)


      const helloWorld = <h1>Hello, World</h1>; // declare
      // ...
      {helloWorld} // use


  1. MAXHO
    24.11.2017 09:28

    Уважаемые читатели! Если сегодня состоялось ваше первое знакомство с React — просим поделиться впечатлениями.

    Оптимистичное заявление на Хабре ;)

    Чтобы не разочаровывать топикпарсера отвечаю…
    Я не профессиональный программист. Я педагог преподающий в кружке информатику. И когда встал вопрос про выбор между Angular, React и Vue, то я остановился на Vue.
    Почему? Как мне показалось Vue код ближе всего к JavaScript и HTML (с уклоном в HTML). Причем не смешивает оно и другое. Меньше нужно какие то места объяснять «так принято», «они решили сделать по другому», «это МАГИЯ» и пр.

    Хотя React, бесспорно, тоже просмотрю. Говорят с React 16 там лицензия MIT. А это было одним из минусов, которые подтолкнули меня делать курс не на нем.
    Как плюс React могу отметить большое количество руководств как сделать ToDo


  1. Salexer
    24.11.2017 11:12

    Классы компонентов и функциональных компоненты

    читая хабр я знаю, что тут умные люди. Поэтому вчитываясь в очепятку я думаю, что так и задумано, перепрочитывая фразу несколько раз


  1. babylon
    24.11.2017 11:44

    Очередной качественный перевод от RUVDS.com. Как пирожки пекут:))) Молодцы!


  1. CynepHy6
    24.11.2017 13:03

    Вчера только начал реакт тыкать. И тут прям подарок — статья на хабре. Спасибо
    vue — это хорошо и приятно, но вакансий пока больше на реакте


  1. Vmist
    24.11.2017 13:06

    Уважаемые читатели! Если сегодня состоялось ваше первое знакомство с React — просим поделиться впечатлениями.

    1е знакомство. Вроде и понятно, но недосказанности.
    Как селекторы под css добавлять после по такому коду jsx — удобно?
    Отсюда: получается это нечто temlate-ора для одной-пары уникальных страниц на сайте?
    В чем глобальное преимущество над js или jquery?
    Совсем непонятно
    Эта конструкция сложнее, но она необходима только в том случае, если вы используете старое состояние для формирования нового состояния. Если нет — можно просто передавать setState объект.
    это про (if) a = !a;?


    1. mayorovp
      24.11.2017 13:50

      Отличия view-библиотеки от шаблонизатора — работа на уровне объектов, а не на уровне строк. Соответственно, нет необходимости бояться инъекций и задумываться над экранированием. Плюс обработчики событий можно привязывать напрямую, а не после рендера с поиском элемента по какому-нибудь id. Но если все это отбросить — то да, это по сути шаблонизатор.


      это про (if) a = !a;?

      Да, именно эта операция тут так делается. Но есть библиотеки которые позволяют делать ее проще (mobx-react).


    1. VolCh
      26.11.2017 11:14

      Для стилизации селекторы обычные, но лучше, имхо, избегать вложенных типа .block .element, поскольку от блока до элемента может оказаться десяток компонентов.

      В получении дом-элемента по селектору обычно необходимости нет, собственно когда кажется, что она есть, то велика вероятность, что что-то сильно не так делаешь.


  1. search
    24.11.2017 14:00
    +1

    Если сегодня состоялось ваше первое знакомство с React — просим поделиться впечатлениями.

    Несколько месяцев назад моя компания оплатила мне курсы реакта. Чтож, фреймворк создаёт очень приятные впечатления возможностью невероятно быстро стартануть/запрототипировать проект и наличием огромного комьюнити. А редакс оказался настолько просто и хорош собой что мы стали использовать его на Angular 2 проекте (ngrx/store, если быть точным).


    Многие в комментариях жалуются на смешение HTML и JS кода в одной функции. Но глядя на любой проект в котором я участвовал, я вижу либо пилёж логики в самом шаблоне, либо ад из дата-атрибутов. Для себя я решил относиться к JSX коду как как к view-слою, который легко протестировать и понять и меня отпустило. Даже понравилось, если честно.


    Такие дела.