Тонкие места в React.js

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

c40c12057f4043f58329c25c077dd69f.jpg


Момент первый — в 0.14 поменялся алгоритм, решающий перерисовывать ли root-элемент или нет


Есть вот такой тонкий момент, не описанный в документации. До версии 0.14 вызов React.render() всегда перерисовывал то, что в него передано. Можно было сохранить ссылку на корневой элемент…

const element = ;


… и каждый вызов 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 
The time is {date}
; } } const app = document.getElementById("app"); const element = ; const ref = ReactDOM.render(element, app); ReactDOM.render(element, app); //повторный вызов не запустит render() ref.forceUpdate(); //а так запустит


Альтернативный вариант — всегда создавать новый элемент:

const app = document.getElementById("app");

const ref = ReactDOM.render(, app);
ReactDOM.render(, app);

Момент второй — если вы работаете с контролами, вызов ReactDOM.render () должен идти синхронно с событиями контрола


Если вы используете , , вызывающий какую-то внешнюю бизнес-логику при переключении.

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 (
            
        );
    }
}


… и есть типичный для 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() заставит  не имеет собственного состояния, то при его переключении происходит запуск события 'onchange', которое вызовет bizLogic1-3, но не поменяет props компонента и не вызовет его перерисовку в процессе обработки события. Однако браузер покажет это переключение, синяя полоска выделения перепрыгнет. Дальше React вернёт обратно правильное (с его точки зрения) предыдущее состояние . Затем асинхронно сработает наш 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(, app);
}


Две засады с тестами


Не все свойства объекта события можно подменить в TestUtils.Simulate.change

Первая проблема заключается в том, что, читая документацию на React TestUtils, создаётся впечатление, что можно сгенерировать поддельное событие и передать его тестируемому компоненту. На самом деле это действительно можно сделать, но на базе переданного события ReactUtils сделает своё, заменяя некоторые свойства. Это не написано в документации и неочевидно, но подделать target и currentTarget нельзя:

describe("MyInput", function() {

    it("refuses to accept DEF", function() {
        var ref = ReactDOM.render(, app);
        var rootNode = ReactDOM.findDOMNode(ref);

        var fakeInput = {value: "DEF"};
        TestUtils.Simulate.change(rootNode, {currentTarget: fakeInput});  //а вот не сработает, TestUtils выставит настоящий currentTarget
        expect($(rootNode).val()).toEqual("abc");  //тест неверен, т.к. handleChange увидит настоящий  в 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 ;
    }
}

const app = document.getElementById("app");

describe("MyInput", function() {

    it("refuses to accept digits", function() {
        var ref = ReactDOM.render(, app);
        var rootNode = ReactDOM.findDOMNode(ref);

        $(rootNode).val("abc1");  //руками поменяем значение
        TestUtils.Simulate.change(rootNode);  //handleChange увидит 
        //здесь React сам вернет обратно значение "abc"
        expect($(rootNode).val()).toEqual("abc");  //тест пройдет успешно, но он неверен, т.к. value перезаписан React'ом
        //то есть при условии, что bizLogic не вызывает перерисовку компонента, впиши мы что угодно, все равно будет "abc"
    });
});


Спасибо за внимание, надеюсь, теперь ваши тесты будут гладкими и шелковистыми!

© Habrahabr.ru