[Перевод] Основы React: всё, что нужно знать для начала работы

Хотите узнать о том, что такое 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


 
 

Play Music

 


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

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

Что такое React?


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

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

820b7e9a18ac8fc6ec5aa6ad9b89ff84.png


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

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


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

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

Напишем компонент внутри тега


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

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

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


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

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


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


Теперь тег

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

c05c588e6198989108217058f4c48cd3.png


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

ReactDOM.render(, placeWeWantToPutComponent);


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

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


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


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

c0b99ec12676b92ca0e0f585f2d6a2f9.png


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

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


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

class Container extends React.Component {
  render() {
    return (
      
       

I am the parent!

             
   );  } } const placeWeWantToPutComponent = document.getElementById('hook'); ReactDOM.render(, placeWeWantToPutComponent);


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


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

JavaScript в JSX


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

class Container extends React.Component {
  render() {
    const greeting = 'I am a string!';
    return (
      
       

{ greeting }

             
   );  } }


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

.

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

class Container extends React.Component {
  render() {
    const addNumbers = (num1, num2) => {
      return num1 + num2;
    };
    return (
      
       

The sum is: { addNumbers(1, 2) }

             
   );  } }


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

0d10dee96c930592af66fb463ed14f23.png


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


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


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

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


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


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

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


Метод 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 (
      
       

{ status }

             
   );  } }


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

35a836e7aaa5ad0da9cf8f22eebc2ae9.png


Всё это не особенно полезно, если у нас нет способа изменять 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 (
      
       

{ status }

             
   );  } }


Когда пользователь щёлкает по тексту, представленному тегом

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

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

.

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


Когда меняется состояние компонента, он снова вызовет функцию 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 (
      
             
   );  } }


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

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

function PlayButton(props) {
  const className = props.isMusicPlaying ? 'play active' : 'play';
  return ;
}


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

7cee94cad0462e17a3dd394da47ce4f3.png


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


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

function PlayButton(props) {
  const className = props.isMusicPlaying ? 'play active' : 'play';
  return ;
}
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 (
      
             
   );  } }


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

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


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

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

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


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

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

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


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

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


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

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isMusicPlaying: false };
  }
  handleClick() {
    this.setState(prevState => {
      return { 
        isMusicPlaying: !prevState.isMusicPlaying   
      };
    });
  };
  render() {
    return (
      
               
   );  } }


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

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


Эта функция будет вызываться каждый раз, когда выводится Container, то есть, this.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-код не должен «жить» исключительно внутри тега