
Момент первый — в 0.14 поменялся алгоритм, решающий перерисовывать ли root-элемент или нет
Есть вот такой тонкий момент, не описанный в документации. До версии 0.14 вызов
React.render()
всегда перерисовывал то, что в него передано. Можно было сохранить ссылку на корневой элемент…const element = <MyComponent />;
… и каждый вызов
React.render(element)
перерисовывал приложение.В 0.14 работа с
props
улучшена, и алгоритм стал «умнее». Теперь, если приходит тот же объект, проверяют соответствие props
и state
уже отрисованному. Иными словами, сохранив ссылку на элемент, нужно или менять его state
, или делать копию, или делать setProps()
перед отрисовкой.import React from "react";
import ReactDOM from "react-dom";
class MyComponent extends React.Component {
render() {
const date = Date.now();
return <div>The time is {date}</div>;
}
}
const app = document.getElementById("app");
const element = <MyComponent />;
const ref = ReactDOM.render(element, app);
ReactDOM.render(element, app); //повторный вызов не запустит render()
ref.forceUpdate(); //а так запустит
Альтернативный вариант — всегда создавать новый элемент:
const app = document.getElementById("app");
const ref = ReactDOM.render(<MyComponent />, app);
ReactDOM.render(<MyComponent />, app);
Момент второй — если вы работаете с контролами, вызов ReactDOM.render() должен идти синхронно с событиями контрола
Если вы используете
<input>
, <select>
и т. п., то после обработки событий изменения данных в них вы должны синхронно делать ReactDOM.render()
.Предположим, у нас есть такой компонент, это обычный
<select>
, вызывающий какую-то внешнюю бизнес-логику при переключении.import React from "react";
import ReactDOM from "react-dom";
class MyComponent extends React.Component {
handleChange(e) {
bizLogic1(e.currentTarget.value);
bizLogic2(e.currentTarget.value);
bizLogic3(e.currentTarget.value);
}
render() {
return (
<select size="3" value={this.props.selectedId} onChange={this.handleChange.bind(this)}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
);
}
}
… и есть типичный для FLUX-приложений код, когда то, какая опция выбрана хранится отдельно, в переменной
selectedId
, и некая бизнес-логика bizLogic1-3
, каждая требующая перерисовку приложения. Умный разработчик сделает перерисовку не три раза, на каждый вызов bizLogic*
, а один, игнорируя повторные запросы, и перерисовывая приложение асинхронно.let selectedId = 1;
const app = document.getElementById("app");
function bizLogic1(newValue) {
selectedId = newValue;
renderAfterwards();
}
function bizLogic2(newValue) {
//...
renderAfterwards();
}
function bizLogic3(newValue) {
//...
renderAfterwards();
}
let renderRequested = false;
function renderAfterwards() {
if (!renderRequested) {
//не смотря на то, что такой паттерн выглядит логичным, так делать нельзя
//асинхронный render() заставит <select> мигать и прыгать скроллом
window.setTimeout(() => {
ReactDOM.render(<MyComponent selectedId={selectedId} />, app, () => {
renderRequested = false;
});
}, 0);
}
}
//initial render
ReactDOM.render(<MyComponent selectedId={selectedId} />, app);
Так вот, при таком подходе, при переключении select'а начинается смешная чехарда — поскольку наш
<select>
не имеет собственного состояния, то при его переключении происходит запуск события 'onchange'
, которое вызовет bizLogic1-3
, но не поменяет props
компонента и не вызовет его перерисовку в процессе обработки события. Однако браузер покажет это переключение, синяя полоска выделения перепрыгнет. Дальше React вернёт обратно правильное (с его точки зрения) предыдущее состояние <select'а>
. Затем асинхронно сработает наш ReactDOM.render()
, который вызовет перерисовку компонента, и синяя полоска выделения снова прыгнет, на этот раз уже туда, куда нужно.Чтобы предотвратить такое поведение, перерисовывать UI с помощью
ReactDOM.render()
нужно сразу при обработке события.С этой задачей хорошо справляется код на подобие паттерна Dispatcher из FLUX:
class MyComponent extends React.Component {
handleChange(e) {
dispatch({action: "ACTION_OPTION_SELECT", value: e.currentTarget.value});
}
...
}
function dispatch(action) {
if (action.action === "ACTION_OPTION_SELECT") {
bizLogic1(action);
bizLogic2(action);
bizLogic3(action);
}
ReactDOM.render(<MyComponent selectedId={selectedId} />, app);
}
Две засады с тестами
Не все свойства объекта события можно подменить в
TestUtils.Simulate.change
Первая проблема заключается в том, что, читая документацию на React TestUtils, создаётся впечатление, что можно сгенерировать поддельное событие и передать его тестируемому компоненту. На самом деле это действительно можно сделать, но на базе переданного события ReactUtils сделает своё, заменяя некоторые свойства. Это не написано в документации и неочевидно, но подделать
target
и currentTarget
нельзя:describe("MyInput", function() {
it("refuses to accept DEF", function() {
var ref = ReactDOM.render(<MyComponent value="abc" />, app);
var rootNode = ReactDOM.findDOMNode(ref);
var fakeInput = {value: "DEF"};
TestUtils.Simulate.change(rootNode, {currentTarget: fakeInput}); //а вот не сработает, TestUtils выставит настоящий currentTarget
expect($(rootNode).val()).toEqual("abc"); //тест неверен, т.к. handleChange увидит настоящий <input> в currentTarget
});
});
Контролы (текстовые поля, чекбоксы итд) работают хитрее, чем вы думаете
Вторая частая засада с тестами близко связана с описанной выше проблемой номер 2 — при работе с контролами React после обработки событий сам восстанавливает значение, которое он считает текущим для контрола. Если вы где-то поменяли значение, но не вызвали перерисовку компонента, то после обработки события значение восстановится.
Поясняющий код:
import {$} from "commonjs-zepto";
import React from "react";
import ReactDOM from "react-dom";
class MyComponent extends React.Component {
handleChange(e) {
let value = e.currentTarget.value;
if (!value.match(/[0-9]/)) bizLogic(value);
}
render() {
return <input type="text" value={this.props.value} onChange={this.handleChange.bind(this)} />;
}
}
const app = document.getElementById("app");
describe("MyInput", function() {
it("refuses to accept digits", function() {
var ref = ReactDOM.render(<MyComponent value="abc" />, app);
var rootNode = ReactDOM.findDOMNode(ref);
$(rootNode).val("abc1"); //руками поменяем значение
TestUtils.Simulate.change(rootNode); //handleChange увидит <input value="abc1">
//здесь React сам вернет обратно значение "abc"
expect($(rootNode).val()).toEqual("abc"); //тест пройдет успешно, но он неверен, т.к. value перезаписан React'ом
//то есть при условии, что bizLogic не вызывает перерисовку компонента, впиши мы что угодно, все равно будет "abc"
});
});
Спасибо за внимание, надеюсь, теперь ваши тесты будут гладкими и шелковистыми!
Комментарии (8)
erlyvideo
30.12.2015 00:54Тесты это круто.
Мы ещё не дошли до них в яваскриптеavdept
30.12.2015 11:45-1Джаваскрипт, ява — это мотоцикл такой был.
Ashot
30.12.2015 15:48+1Ума не приложу, неужели ещё всем не надоело холиварить на тему произношения?
Название JavaScript было выбрано, т.к. язык Java был одним из, так сказать, вдохновителей при создании JavaScript. Название языку Java было выбрано в честь сорта кофе java, который по русски звучит как ява.
Лично я не вижу ничего криминального в использовании произношения «яваскрипт»(как и просто «ява») на равне с англоязычным произношением «джаваскрипт».
erlyvideo
30.12.2015 23:15если вы не в курсе, то для этого звука традиционно есть оба произношения: йокающее и джокающее.
Правильного нет, оба годятся.
KeepYourMind
30.12.2015 13:50XEK на встрече BeerJS мы обсуждали, что target переопределить можно. Ты точно уверен что нельзя?
YChebotaev
Статью можно назвать «как не стоит делать в react, если не хотите чтобы вам оторвал руки тимлид».