Введение

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

В Stytch мы создали новую библиотеку визуализации с нуля, чтобы заменить нашу существующую реализацию на основе GWT/jqPlot, и в этом посте описаны некоторые проблемы, с которыми мы столкнулись, и то, как мы их решили.

Мы избегали использования каких-либо ранее существовавших библиотек визуализации, потому что знали, что нам потребуется обеспечить высокий уровень настройки наших визуализаций, а создание собственной позволило нам получить необходимый уровень контроля.

Мы выбрали React в качестве нашей библиотеки пользовательского интерфейса, потому что можно было встроить компоненты React в приложение GWT (а также приложение Angular, которое мы создавали), что позволило нам заменять пользовательский интерфейс по частям, а также потому, что его компонентная архитектура позволяла нам способствовать высокому уровню повторного использования кода. Мы также обнаружили, что React довольно легко понять и начать продуктивно работать.

Но этот пост не о React…

Как React и d3 дополняют и конфликтуют друг с другом

Проще говоря, они оба хотят контролировать DOM. К счастью, React предоставляет аварийные выходы, которые можно использовать для интеграции с другими библиотеками, такими как d3 или даже JQuery и тому подобное.

Есть два «крайних» решения конфликта, каждое из которых включает передачу (почти) полного контроля над DOM одной из двух библиотек.

Позвольте React управлять DOM

Давайте рассмотрим очень простой пример, рисуя красную линию в SVG. В качестве необработанного элемента SVG он выглядит примерно так:

<path stroke="red" d="M0,0V0H500V0" fill="none" class="my-line"/>

Мы можем легко превратить это в компонент React:

class RedLine extends React.Component {
  render() {
    return <path stroke="red" d="M0,0V0H500V0" className="my-line"/>;
  }
}

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

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

Пусть d3 управляет DOM

Вот наш компонент RedLine React с d3, используемым для управления DOM:

class RedLine extends React.Component {
  render() {
    return <g/>; // d3 will modify the DOM below here
  }
  componentDidMount() {
    let line = d3.line();
    let pathString = line(this.props.points); // array of x/y coords
    d3.select("g")
      .append("path")
      .attr("d", pathString)
      .style("stroke", "red");
  }
}

Вы заметите, что мы используем функцию жизненного цикла Reacts componentDidMount, чтобы позволить d3 изменять DOM после того, как React выполнил рендеринг. Это работает хорошо, но мы отказываемся от способности Reacts эффективно управлять DOM — этот компонент будет эффективно воссоздаваться каждый раз, когда мы повторно отрисовываем.

Еще одним недостатком этого метода является то, что componentDidMount не запускается на сервере. Если вы хотите воспользоваться рендерингом на стороне сервера, это не сработает.

Что-то между

Мы могли бы легко воспользоваться хотя бы некоторыми функциями d3 в React следующим образом:

class RedLine extends React.Component {
  render() {
    let line = d3.line();
    let pathString = line(this.props.points); // array of x/y coords
    return <path stroke="red" d={pathString} className="my-line"/>;
  }
}

Однако это не работает для всех функций d3. А как насчет d3.axis()?

Первоначально мы выбрали подход с использованием простого SVG в React в сочетании с функциями d3 для «сложной математики» (такие вещи, как масштабы, круговые диаграммы) и парой конкретных компонентов React, которые просто делегировались функции d3 (например, наша Axis компонент). Благодаря компонентной модели React было легко объединить два подхода в одном приложении.

Там, где это упало, был рендеринг на стороне сервера. Мне нужно было построить микросервис для экспорта визуализаций в файлы изображений (об этом я напишу отдельный пост) и вдруг ни на одном из моих графиков нет осей! Я мог бы переписать компонент Axis, но это было бы довольно много (оси могут быть довольно сложными!).

К счастью, есть решение этой проблемы в библиотеке под названием React Faux DOM. По сути, это обеспечивает структуру данных, имитирующую узел DOM (что-то вроде минимальной реализации jsdom). d3 может манипулировать этой структурой, как если бы это был обычный DOM, а затем в конце ее можно преобразовать в элементы React. Это позволило мне поместить весь мой код рендеринга, d3 или что-то еще, в метод render() и полностью избежать componentDidMount:

class Axis extends React.Component {
  render() {
    let node = ReactFauxDom.createElement("g");
    node.setAttribute("class", "axis");
    this._drawAxis(node); // What used to be componentDidMount()
    return node.toReact();
  }
}

Теперь у нас есть хороший, эффективный компонент, управляемый React, который использует d3, чтобы сэкономить нам время и включить рендеринг на стороне сервера!

Вывод

Я описал несколько разных способов смешивания React и d3 — конкретный метод, подходящий для вашего проекта, будет зависеть от ваших обстоятельств, но я надеюсь, что вы найдете здесь вдохновение.