React — это, безусловно, прорывная технология, которая упрощает создание сложных интерфейсов, но, как у любой абстракции, у неё есть свои мелкие проблемки и особенности. Я в своей практике столкнулся с четырьмя не очень очевидными вещами. Багами это назвать сложно — это просто особенности работы библиотеки. О них сегодня и поговорим.
Момент первый — в 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" }); });
Спасибо за внимание, надеюсь, теперь ваши тесты будут гладкими и шелковистыми!
ссылка на оригинал статьи http://habrahabr.ru/post/273995/
Добавить комментарий