Компоненты: Дизайн-система на React

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

Перед началом разработки сервиса, на стороне разработки надо ответить на три базовых вопроса: где данные хранить, какой дизайн и какова логика обработки данных. Это классическая модель MVC (Model-View-Controller), в которой Model про хранение, View про вид, Controller про логику управления данными. Так как именно дизайнер отвечает за внешний вид сайта, то в парадигме MVC наша часть работы это View. Оставшиеся Controller и Model могут присутствовать в React в зависимости от архитектуры.

Немного разберемся со структурой сущностей. Что мы можем завести в проекте:

Дизайн-токены — некие переменные данные, свойства дизайн-системы, вроде цвета, гарнитуры шрифта, пользовательских данных, значения в браузерной строке и так далее. Это любые свойства, такие как отдельная палитра для отступов (спейсеры), высота строки, размеры шрифтов. Если при выравнивании чек-бокса относительно кнопки приходится прописывать align-items: center, то это проблема на уровне токенов. Ничего меньше токена в природе существовать не может, отдельные переменные для тона, насыщенности и светлоты в HSL это тоже токены, позволяющие гибко управлять тимизацией.

--main-brand-h: 130deg;
--main-brand-s: 75%;
--main-brand-l: 65%;
--main-brand: hsl(var(--brand-h), var(--brand-s), var(--brand-l));

Атомы — простые компоненты, из которых строятся более сложные компоненты. Не могут включать в себя другие атомы и не могут иметь логику отображения (состояния). Заголовок H1 с выравниванием по левому краю это атом, иконка, ячейка таблицы тоже атомы. Цвет, толщина и радиусы скругления у иконки меняются за счет токенов.

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

Организмы — компоненты, имеющие сложную логику, обычно это конкретная фича. Под фичей подразумевается некая полезная переиспользуемая функциональность, как эпики в разработке. Компоненты типа «организм» самые частотные в зрелом проекте. Могут содержать в себе атомы, молекулы, бизнес-логику, абстрактную логику и могут управляться извне. Например, карта яндекса может влиять на содержимое карточки объекта на карте, и таким образом организм < Map/> будет включать в себя организм <Card />, и рядом может жить виджет чата поддержки, который будет переиспользован на множестве других страниц. Можно импортировать организм внутрь организма, но изначальный дизайн организма не должен подразумевать такой вложенности. Не может содержать собственных стилей. Организмами являются формы регистрации, карточки товаров, большие и сложные таблицы.

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

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

Если рассматривать таблицу, то это организм. У которого есть атомы-ячейки таблицы, и молекулы — строчки или колонки. Если таблица состоит только из атомов, то таблица будет молекулой. Не может существовать точного определения, что таблица или редактор тэгов всегда организм, это зависит от дизайна и структуры.

Это описание View, для реализации которого потребуется уверенное знание ES6, который мы будем преобразовывать в ES5 с помощью транспайлера. Не надо путать с компилятором. Компилятор переводит ваш код в машинный код, а транспайлер преобразует понятный и читаемый код JS в другой код JS, также читаемый и понятный человеку.

Среда разработки

Немного теории. Классический способ подключения библиотек это script src="URL"…/script, но у такого способа есть проблема, что нужно указывать порядок подключения библиотек правильно, и каждая библиотека это отдельный запрос на сервер. Модуль/библиотека — обычный файл .js с готовой логикой.

В синтаксисе ES6 появился удобный способ экспорта и импорта компонентов. Достаточно дописать export перед переменной. Такие переменные или функции в дальнейшем можно импортировать через import {} from '/.filename'.

И удобный способ работы с этим носит имя webpack. Webpack это сборщик модулей. Тонкости настройки и понимание процесса развертывания всего, что нужно для работы с современным front-end это отдельная дисциплина, и мы должны понимать весь процесс от начала и до конца. Возьмем систему управления модулями CommonJS, для этого понадобится node.js. Заходим на nodejs и скачиваем LTS версию. Устанавливаем. Выбираем LTS версию, так как она стабильная и все библиотеки, с которыми мы захотим работать, ее поддерживают.

Итак, после установки node.js вам станет доступен менеджер пакетов npmjs. Следующим шагом в терминале прописываем npm init, следуем инструкциям внутри терминала, прописывания или игнорируя имя проекта, версия, описание, основной файл программы, команду для теста, репозиторий, ключевые слова, лицензия, и, как результат выполнения команды, должен появиться файл package.json. В этом файле хранятся настройки, команды для webpack, основные пакеты. Посмотреть все зависимости пакетов можно по команде npm list.

Идем дальше вглубь webpack. Вбиваем в терминал команду npm i webpack и ждем, пока скачаются все пакеты, по ходу скачивания будут созданы новые файлы package-lock.json для фиксации версий пакетов и папка node_modules, в которой и лежит webpack. Чтобы проверить корректность установки, эту папку можно удалить, почистить кэш npm cache clean и по команде npm i эта папка с правильным содержимым будет создана заново.

Webpack нам нужен для объединения файлов, в результате чего мы получаем один файл-модуль .js, и сможем обойтись всего одним запросом на сервер. Мы отдаем Webpack’у на вход один файл (App.js), он проходится по всем прописанным import и берет нужный код со всех файлов, куда доберется по цепочкам разных import -> import -> import -> …. В результате генерируется один единственный файл .js. Файл .css тоже просто модуль, который после минификации подключается к проекту.

Попробуем на практике. Устанавливаем npm i webpack-cli, можно установить конкретную версию npm i -D webpack@4.44.2 webpack-cli@3.3.12. Создаем в корне проекта папку src и внутри папки создаем файл index.js, запускаем команду node_modules/.bin/webpack. Появится папка dist и внутри нее файл main.js, это и есть наша сборка. Более быстрый способ для сборки проекта это команда npx webpack.

Теперь мы умеем компилировать максимально сжатую сборку для продакшена. Попросту пишем любой JS-код в файле index.js, запускаем node_modules/.bin/webpack, все изменения отразятся в файле main.js. Компилировать код после каждого изменения дело весьма непродуктивное, поэтому упростим себе жизнь. В package.json можно дописать скрипт для автоматической компиляции, вроде "watch": "webpack --mode development --watch", и теперь по команде npm run watch мы запускаем режим, когда любые изменения вносятся налету. Вот мой набор команд:

"scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1",
    "dev": "webpack --mode development",
    "dev:watch": "webpack --mode development --watch",
    "build": "webpack --mode production",
    "start": "webpack-dev-server --mode development --open"
  },

Установка React

Для лучшего соответствия общепринятым практикам я переименую index.js в App.js. Для установки React достаточно лаконичной команды npm i react react-dom. React-dom позволяет добавлять компоненты на HTML-страницу. И сразу пишем свой первый компонент:

import React from 'react';
import ReactDOM from 'react-dom';
 
class App extends React.Component {
    render() {
        return
        <div>
        <h3>А вот мы и начали</h3>
        </div>
    }
}
ReactDOM.render(<App />, document.querySelector("#root"))

Все, компонент есть, простой заголовок H3. В классе компонента обязательно должна быть реализована функция render(). То, что возвращает эта функция, как раз и выводится на экран. Следующий шаг: разместить компонент на HTML-страничке. Создаем самую типовую веб-страницу внутри папки dist. Прописываем <script src="main.js"></script>, это тот файлик, который у нас получается после команды node_modules/.bin/webpack. И для размещения компонента внутри HTML-странички надо дописать тэг <div id ="root"></div>, к которому мы будем привязываться уже прописанным JSX-кодом: 
ReactDOM.render(<App />, document.querySelector("#root")), он будет преобразован в JS-код. JSX это расширенный код JS с возможность вставлять html-код внутрь JS. Код вида <div></div> преобразовывается в React.createElement("div");. В результате вызова метода должен быть получен объект. Мы пишем компоненты на JSX (Java Script Extended), это JS с возможностью писать HTML-код. Код преобразуется в React.createElement('div'); и за это отвечает Babel. Компонент должен встроиться между <div>, у которых id задан как #root. На всякий пожарный, можно еще разок запустить команду npm i webpack-cli -P и npm i --save webpack-cli.

Обратите внимание, что здесь используется export default вместо просто export. Поэтому мы импортируем компонент без использования фигурных скобок. Зачастую использовать export default это не лучшая практика, на большом проекте возникнут проблемы с переименованиями и очевидностью кода.

В мире дизайнеров компоненты это символы в Sketch/Figma/XD. Это глупые компоненты, т.е. компоненты без внутренней логики, которые нужны для отображения некой эстетики, отрисовки визуального представления. Так называемые атомы/молекулы. Компонент, который мы написали выше, как раз «глупый», это атом. В мире React умный компонент это компонент с состояниями (state), у такого компонента должна быть память, а глупый компонент не имеет своего state. Сам React не очень хорошо работает со state, поэтому необходим Redux или ему подобные. Обновили state, компонент перерисовался.

Компонент для разработчика это любой HTML-элемент. Например, <h1>Header</h1> это уже компонент/атом, который мы можем переиспользовать. Если говорить о React, то такой компонент будет записан как <H1 />. Все компоненты записываются с заглавной буквы. И разумеется, компонент может быть не только на уровне атома, но и токена, молекулы, организма, и даже более глобальной сущностью, как целая страница. Можно создать компонент <Layout />, который отвечает за всю страницу, и внутри него есть компонент <Welcome /> с неким контентом. Который может меняться на <About />, <Cataloge />, <Check />. Такое переключение компонентов реализуется с помощью React Router.

Мы уже создали наш первый компонент, но давайте сделаем это правильно. В интернете можно найти множество примеров создания компонентом с помощью createClass, но этот способ больше не поддерживается. Если мы хотим использовать классы, есть более современный вариант: class Message extends Components {}. Класс Components импортируется из React-библиотеки. Либо еще один способ с помощью стрелочных функций:

const Message = props =&gt;; {
return <h1>Value</h1>;
}

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


Что у нас уже есть? У нас есть монстр Франкенштейна в виде смеси JS и HTML, и заставить это хоть как-то жить умеет Babel. Да, мы все еще на этапе настройки окружения, для преобразования кода JSX в JS понадобится установить Babel. Его, и некоторые пресеты командой npm install -D babel-loader @babel/core @babel/preset-env

Webpack:

  • @babel/preset-react — позволяет переводить JSX в JS
  • @babel/preset-env — для перевода ES6 и ES5

Второй шаг настройки Babel это создание ручками файла .babelrc для определения конфигурации Babel, внутри которого создаем JSON-объект:

{
    "presets":[
        "@babel/preset-react",
        "@babel/preset-env"
    ]
}

И сообщаем React про существование Babel, создав и дописав в webpack.config.js такой код: 

const path = require("path");

module.exports = {
    entry: "./src/App.js",
    output: {
        path: path.resolve(__dirname, '/dist'),
        filename: "main.js"
    },
    mode: "development"
}

Скажем, что при встрече с файлом в формате .js необходимо использовать babel-loader:

 module: {
        rules: [
            {
                test:/\.js$/,
                exclude: /node_modules/,
                use: "babel-loader"
            }
        ]
    }

Что получается: скрипт запускает Webpack, который смотрит на настройки в файле webpack.config.js, и в нем он видит инструкцию для открытия App.js. Но перед этим он должен запустить Babel, который каждой встрече с расширением .js обрабатывает файл .js, и лишь потом React генерирует main.js.

Если на этом этапе у вас уже возникли сложности, можно скачать готовый проект командой npx create-react-app app-name. Не забыв после исполнения команды сменить директорию cd app-name. И команда npm start вас перебросит по адресу http://localhost:3000/ с работающим шаблонным проектом.

Рендер

Настало время увидеть наш компонент в живую. Для отображения компонента на странице нужно привязываться к классу/id: 

ReactDOM.render(<App />, document.querySelector("#root"))
ReactDOM.render(Component, document.getElementById('root'));

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

Работая с React, мы обязаны придерживаться неких правил, и основное: мы никогда не взаимодействуем с реальным DOM, только с виртуальным.

Второе важное правило: каждый компонент уровня атома/молекулы/организма это отдельный файл, компоненты могут наследовать друг от друга содержимое, и при наследовании через extends мы в конструкторе должны указывать super().

И третье: если вы встретили в компонентах React.createClass, то это устаревший способ создания компонента, как мы уже говорили выше. Выглядит код такого компонента примерно так: 

 
import React from 'react';
import ReactDOM from 'react-dom';
 
const App = React.createClass({
    render() {
    return <div>Старый компонент</div>
}
});
 
ReactDOM.render(<App />, document.querySelector('#root'));

Такой код уже не компилируется. Хотя вы почти наверняка столкнетесь с такими компонентами, и нужно как-то с ними работать. Для этого устанавливаем библиотеку npm i create-react-class, добавляем еще один import createReactClass from 'create-react-class'; и меняем const App = React.createClass ({ на const App = createReactClass ({. Либо глобально создаем окружение React.createClass = createReactClass;. Но такие компоненты лучше переписывать на новые.

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

 
import React from 'react';
import ReactDOM from 'react-dom';
 
const App = (props) => {
    return <div>
    <h1>Заголовок</h1>
    </div>;
}
 
ReactDOM.render(<App />, document.querySelector('#root'));

И третий способ создания компонентов основан на классах. Понадобится функция .map(). Создаем новый компонент в новом файле, например, новый файл-компонент с любым именем, и заодно сразу прописываем ему экспорт. Без экспорта мы не сможем подключить ни один компонент:

 
import React from 'react';
import ReactDOM from 'react-dom';
 
 
class Component3 extends React.Component {
    render() {
        return <div>
            <button>Good</button>
            </div>;
    }
}
 
ReactDOM.render(<Component3 />, document.querySelector('#root'));
 
export default Component3;

Для импорта созданных компонентов достаточно дописать в начале родительского файла import Component3 from './Component3'; и прописать его ReactDOM.render(<Component3 />, document.querySelector("#root2"));. Собственно, мы уже собрали простое окно их трех компонентов.

ReactDOM.render(<Component1 />, document.querySelector("#root"));
ReactDOM.render(<Component2 />, document.querySelector("#root2"));

Более того, мы можем вызывать компонент внутри компонента, таким образом создавая цепочку атомы -> молекулы -> организмы.

const App2 = (props) => {
    return <div>
    <h1>Заголовок</h1>
    <Component3 /> <Component3 /> <Component3 />
    </div>;
}

Но такие компоненты все равно уступают компонентам из Sketch или Figma, мы же наверняка хотим уметь менять текст в кнопке. С этого момента мы должны расценивать текст в рамках компонентов как токены, часть нашей дизайн-системы, которая хранится в отдельной базе данных. Но пока что пойдем более простым путем и передадим компоненту переменную.

Посмотрим на внутренности компонента с помощью console.log(this). Мы видим свойство props и текст, который мы хотели бы поменять. Пропишем console.log(this.props.textInside), и получим доступ к захардкоженному тексту внутри кнопки «Good». Из чисто нездорового интереса можно сделать вот так: {this.props.textInside}, получив состояние, которое подтверждает, что родительский компонент может спускать данные своим дочерним компонентам. То есть, родительский компонент передает атрибутами (props) данные вниз своим дочерним элементам и тем самым управляет ими.

Мы уже умеет задавать некое значение кнопке <button>{this.props.contentText}</button>, и после этого можем свободно прописывать любой текст для каждой кнопки при вызове компонента: <Component3 contentText="OK"/> <Component3 contentText="Cancel"/> <Component3 contentText="Delete"/>.

Но это инлайновый способ, и на большом проекте так делать весьма неудобно. Мы хотели рассматривать текст как токены. Так давайте реализуем такую механику. Передадим данные нашему блоку :

        const menuData = [
            {href: "/", title: "Confirm"},
            {href: "/back", title: "Cancel"},
            {href: "/404", title: "Delete"},
            {href: "/payment", title: "Close"},
        ];

Далее в компоненте, который будет импортирован, дописываем
const items = this.props.items.map() , благодаря которому на каждый элемент массива будет запущена некая функция. Выглядеть это будет так:

import React from 'react';
 
class AppNew extends React.Component {
    render() {
    const items = this.props.items.map((item, index) =&gt; {
        return <div key="{index}"><a href="{item.href}">{item.title}</a></div>;
    })
 
return (
        <div>{items}</div>
       );
    }
}
 
export default AppNew;

И импортируем в компоненте и на страничке подтянутся заранее подготовленные данные:

class Component3 extends React.Component {
    render() 
    {
        const menuData = [
            {href: "/", title: "Confirm"},
            {href: "/back", title: "Cancel"},
            {href: "/404", title: "Delete"},
            {href: "/payment", title: "Close"},
        ];
 
        return <div>
            <AppNew 
                title = "Меню"
                items = { menuData }
                />
            </div>
    }
}
ReactDOM.render(<Component3 />, document.querySelector('#root'));
export default Component3;

Мы слегка нарушили подход атомарного дизайна, в котором компонент размещается на странице и лишь потом в него подгружаются данные. В нашем случае мы сначала заливаем данные в компонент, а потом он рендерится на странице. Почти наверняка именно с таким подходом вы столкнетесь в реальной жизни, поэтому будем его придерживаться. А теперь сделаем из написанного кода два компонента, один из которых будет простым отдельным класс для визуального стиля (глупым компонентом), и второй для запросов. Создадим новый файл MenuItem и перенесем в него логику:

import React, { Component } from 'react';
 
class JustData extends Component {
    render() {
        return (
            <li>
                <a href="{this.props.href}">{this.props.children}</a>
            </li>
        );
    }
}
 
export default JustData;

Так как пропсы (свойства) задаются только от родителей, то дописываем оригинальный компонент: const items = this.props.items.map((item, index) => { return {item.title} ; .

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

Мы пишем компоненты для разработчиков и не можем быть уверены, что все участники команды будут использовать компоненты правильно. Например, новый участник команды не передаст нужные текстовые данные. На этот случай нужно заложить в компонент значения по умолчанию. Компонент JustData технически это функция-конструктор. А любая функция в JS это объект, и объекту можно задать свойства. Значит, задаем свойства по умолчанию сразу после JustData:

JustData.defaultProps = {
    children: "Значение по умолчанию",
    href: "/"
}

Более того, мы обязаны задать тип данных, которые должны передаваться в компонент. Например, это может быть текст для кнопок, цифры для MAC-адресов или булевые значения. Устанавливаем библиотеку npm i prop-types для работы с типами данных props:

JustData.propTypes = {
    children: PropTypes.string.isRequired,
    href: PropTypes.number.isRequired,
}

Состояния

Состояние компонента меняется, значит, он отображается по другому. Появляется новый текст в кнопке или добавляются визуальные свойства. Для этого используются state. Добавим стили, так как без css жить немного грустно. Но webpack по умолчанию не знаком с css, нужны загрузчики, т.е. это специальные лоадеры, которые позволяют записать css-код в main.js. Продолжает докидывать библиотечек, npm css-loader style-loader, вторая библиотека как раз перемещает стили в index.html.

Открываем webpack.config.js и задаем новое правило: если будет встречен файл в формате test: /.css$/, то применяется use: ["style-loader","css-loader"] в последовательности справа налево, как указано в скобках. Сначала запускается css-loader, а потом style-loader. И теперь можно даже заняться темизацией.

Итак, шаблон странички с которой мы будем работать, наш код в index.js

import React from "react";
import ReactDOM from "react-dom";
import './style.css';
 
import DisplayElement from "./Component_2";
ReactDOM.render(<DisplayElement />, document.querySelector("#root"));

и для component2.js:

import React, { Component } from 'react';
 
export default class DisplayElement extends Component {
    constructor(props) {
        super(props);
        this.state = {
            display: false
        }
    }
 
render () { 
    let newsBlock = <div>
        <h1>Hey!</h1>
        <div>
            <h3>header</h3>
            <span>Oh oh oh</span>
        </div>
    </div>
 
  }
}

у нас есть метод render(), теперь добавим конструктор. Немного расширяем код, дописав:

return (
        <div classname="link">
            {newsBlock}
            <footer> опа</footer>
        </div>
    )

И теперь компонент надо украсить стилями. Стили применяются глобально, поэтому дописываем в самый самый родительский файл-компонент app.js стринг import '.app/styles/style.css'; и в соответствующем файле style.css допишем стили для такого рода ссылок:

body {
    color: white;
    background-color: black;
  }
 
.link {
    cursor: pointer;
    border-bottom: 2px dotted blue;
    color:blue;
}

и присвоим класс h2 class="link", стилизовать компоненты можно с помощью className или id, есть техническая возможность даже использовать inline styles но это весьма плохая практика. Слово class в JS служит для создания функции-конструктора, и значит, оно зарезервировано. В JSX мы использует classname вместо class для задания стилей. Значит, <h2 className="link" и вуаля, мы умеем применять стили к нашим компонентам.

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

Жизненный цикл

У компонентов есть жизненный цикл. Это методы, которые в течении жизни компонента на странице в определенный момент выполняются. Этих методов много, нам нужно как минимум понимать методы render, constructor, но их намного больше. Методы делятся на три категории:

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

Это легко проверить, достаточно посмотреть момент вызова render вот таким простым кодом, в консоли будет выведен рендер в момент отработки этого метода

import React, { Component } from 'react';
 
export default class LifeCyclemount extends Component {
    constructor(props) {
        super(props);
        console.log('constructor');
    }
 
    render() {
        console.log('render');
        return (
            <div>
                LifeCycleMount
            </div>
        )
    }
}

Обновляем данные

Состоит из пяти пунктов

import React, { Component } from 'react';
 
export default class LifeCyclemount extends Component {
    constructor (props) {
        super(props);
        this.state = {count: 0}
        console.log("constructor();");
    }
 
    static getDerivedStateFromProps(props, state) {
        console.log('getDerivedStateFromProps');
        return null;
    }
 
    shouldComponentUpdate() {
        console.log("shouldComponentUpdate");
        return true;
        //это для обновления свойств компонента 
    }
 
    render() {
        console.log('render')
        return (
            <div>
                {this.state.count}
                <button onclick="{()" ==""> {
                    this.setState({count: this.state.count +1})
                }}&gt;Увеличить счетчик</button>
            </div>
        )
    }
 
    getSnapshotBeforeUpdate() {
        console.log('getSnapshotBeforeUpdate');
        return null;//этот null улетит в snapshot следующего метода, 
        //getSnapshotBeforeUpdate это про 
    }
 
    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('componentDidUpdate');
        //ajax-запросы, обработчики событий
    }
}

Браузерная строка

А теперь необычный момент. Мы хотим использовать браузерную строку как элемент навигации на уровне компонентов и да, есть техническая возможность управлять содержимым браузерной строки. Это особенно актуально, когда ваш сайт состоит из сложных комбинаций компонентов и условий. А еще очень хочется раскидывать компоненты по сеточке. Мы опять настраиваем webpack, но уже под сервер, так как нам нужна маршрутизация. Вводим в терминал команду npm i webpack-dev-server -D и после установки в файле webpack.config.js дописываем, чтобы папка dist стала корневой папкой:

},
    devServer: {
        contentBase: path.resolve(__dirname, "dist")
    }

и для удобства пропишем скрипт для запуска сервера, зайдя в package.json и дописав: "start": "webpack-dev-server --open", так мы сразу запускаем и Dev Server, и webpack. При запуске скрипта должна автоматически открыться вкладка в браузере с localhost. Пробуем, дописав в терминале команду npm start.

И пропишем логику, чтобы сервер предоставлял только файл index.html: 

 devServer: {  ontentBase: path.resolve(__dirname, "dist"),        historyApiFallback: true    }

Далее, устанавливаем React Router npm i react-router@3. Настройка сервера завершена, и мы можем работать с React Router. Надо знать три основные компонента: router для маршрутизации перехода между страниц при смене созданных нами компонентов. Второй компонент от React Router это route для прописывания маршрутов (путей в браузерной строке). И третий компонент Link для замены ссылок в браузере, так как технически мы можем не менять URL-адрес в SPA.

Следующий шаг: установка npm i bootstrap. Для подключения CSS классов bootstrap к React требуется дописать следующий импорт в основной файл App.js : import 'bootstrap/dist/css/bootstrap.min.css';

И заверстаем простую страничку в файле Layout.js

import React from "react";
 
export default class Layout extends React.Component {
    render() {
        return(
            <div classname="container">
                <div classname="row">
                    <div classname="col-4">
                        <ul>
                            <li><a href="/">Figma</a></li>
                            <li><a href="/">React</a></li>
                            <li><a href="/">Miro</a></li>
                            <li><a href="/">CJM</a></li>
                        </ul>
                    </div>
                <div classname="col-8">
                    компонент
                </div>
            </div>
        </div>
        )
    }
}

И второй файл будет App.js

import React from "react";
import ReactDOM from "react-dom";
 
import 'bootstrap/dist/css/bootstrap.min.css';
import Layout from "./app/components/Layout";
 
ReactDOM.render(<Layout />, document.getElementById('app'));

В результате сборки мы уже должны увидеть Layout, который отображает 4 пункта меню.

Переходим к переключению компонентов и отслеживанию состояния браузерной строки. За компоненты сойдут любые из написанных нами ранее. В первую очередь, добавляем import { Router, Route, browserHistory } from "react-router"; и донастраиваем маршрутизацию следующим способом:

ReactDOM.render(<router history="{browserHistory}">
	<route path="/" component="{Layout}"> 
		<route path="ourMission" component="{Data}"></route>
	</route>
</router>, document.querySelector('#root'));

Зададим странице компонент по умолчанию Main: <indexRoute component = {Main} />. теперь, как только в браузерной строке появится путь /, сразу подгружается компонент Main. Но также надо доимпортировать import { Router, Route, browserHistory, indexRoute } from "react-router";. И код ссылок должен стать более реактовским:

import { Link } from 'react-router';
 
<ul>
   <li><Link to="/">Figma</Link></li>
   <li><Link to="/">React</Link></li>
   <li><Link to="/">Miro</Link></li>
   <li><Link to="/">CJM</Link></li>
</ul>

Для отображения компонента главной страницы мы дописали import { Router, Route, browserHistory, IndexRoute } from 'react-router'; при клике на ссылку отправляется запрос на сервер, прося предоставить файл about.html. Но у нас же одна страница, SPA, какие еще запросы к страницам? Все верно, все запросы должны идти к index.html. и тут опять возникает загвоздка. Ведь получать обновленный index.html на каждое действие пользователя весьма накладно по ресурсам. Это можно поправить, в layout импортируем import { Link } from "react-router"; именно для этого мы и поменяли все ссылки a href на link to. В компонентах вместо a href="/about" нужно использовать link to="/about".

Теперь мы можем ходить по ссылкам без перезагрузки страницы. Если адрес страницы введен неверно, то для отображения компонента 404 можно обойтись такой строкой <Route path='*' component={noPage} />.

Далее процесс еще более интересный. Укажем ссылкам некий путь, <li><Link to="/CJM">CJM</Link></li> и при клике на CJM в строке браузера добавится текст CJM. Как только это происходит, система ищет строку типа <Route path ="Cjm" component={Cjm}></Route> и если она есть, то подгружает соответствующий компонент.

Flux и redux

Проблема компонентов, описанных выше, это постоянное получение новых данных. За полный демонтаж компонентов и их повторное монтирование с подгрузкой всех данных браузеры спасибо не скажут. Хочется иметь некий глобальный объект, в котором будут храниться данные пользователя. На помощь приходит Flux, как паттерн проектирования проекта, подход к упорядочиванию проект. Можно провести такую ассоциацию, что пользовательские данные это тоже дизайн-токены, к которым мы можем получать доступ из разных компонентов, и им нужна правильная организация.

Итак, пользователь нажал на кнопку, при клике на кнопку мы должны сгенерировать некий объект, специальную сущность. При клике происходит Ajax-запрос, некий Action (действие), этот запрос должен попасть к хранилищу данных и поменять его наполнение, дозаполнить, обновить и так далее. Отправитель получает этот объект и отдает в хранилище (их может быть много), компонент получает данные из хранилища и обновляет их в себе. Получается следующая цепочка: Components -> Actions -> Dispatcher ->Stores.

Вместо Flux в чистом виде мы используем Redux, который реализует ровно то, что мы описали выше. Есть и альтернатива в виде MobX. Но мы используем Redux, так как мне нравится идея использовать только одно хранилище, из которого данные и затягиваются в компоненты. Обеспечивает это provider, он же запускает полную перерисовку виртуального дерева. Еще помним про умные компоненты? В данном случае умный компонент знает про существование Redux и умеет с ним работать. Глупый компонент же никак не подключен к хранилищу. И глупые компоненты получают данные от умных компонентов через props.

Настало время ввести в обиход новое для нас слово, reducers. Это специальные функции, они диктуют, как должен меняться store. Объекты попадают к reducers, которые меняют данные внутри хранилища.

Вписываем команду npm install redux react-redux redux-logger redux-thunk redux-promise-middleware, где react-redux , такое многообразие позволяет связать глобальное хранилище с React. Redux может существовать и без React.

Наши первые строки:

import { createStore } from 'redux';
const store = createStore(, );

Как мы видим, требуется передать два аргумента: первый это специальная функция для регулирования значения состояния хранилища (reducer). Для изменения состояния хранилища передаем в reducer некий action, делается это так store.dispatch({type: "INC", payload: 1}); . Тут мы передаем тип данных и значение, причем значений может быть много. Это позволяет передать action в функцию reducer. Второй аргумент — начальное состояние store. После изменений строка должна приобрести такой вид: const store = createStore(reducer, 0);, и где-то выше появиться заглушка для функции const reducer = function() {    }.

Мы определили хранилище, указали, кто может менять его и задали начальное значение. Настало время подписаться на наше хранилище, то есть мониторить изменения, и выводить текущее и новое значение нашего хранилища:

store.subscribe(() =&gt; {
  console.log('Changes', store.getState() );
})

Далее, мы можем вернуть практически любое значение вот таким вот образом

const reducer = function(state, action ) {
  return 'd';
}

либо изменить на return state; и тогда мы будем возвращать текущее состояние стора. Можно проводить простые математические операции множество раз и выводить их в консоль:

const reducer = function(state, action ) {
  if (action.type === "INC") {
    return state + action.payload;
  }
  if (action.type === "DEC") {
    return state - action.payload;
  }
  return state;
}
 
const store = createStore(reducer, 0);
 
store.subscribe(() =&gt; {
  console.log('Changes', store.getState() );
})
store.dispatch({ type:"INC", payload: 1 })
store.dispatch({ type:"DEC", payload: 12 })
store.dispatch({ type:"INC", payload: 45 })

Работать с числами это забавно, но что насчет реально больших данных, ведь контент — король? Создадим для короля основу с имеющимися данными и возможностью хранить то, что введет пользователь. Но через один reducer управлять огромным наборов текстовых данных — сложно и нечитаемо. Вполне логично создать много редюсеров. Для этого существует import { combineReducers, createStore } from "redux"; и  вот шаблон для этого:

const useReducer = (state = {}, action) =&gt; { return state; }
const messageReducer = (state = [], action) =&gt; { return state; }
 
 
const reducers = combineReducers({
  user: useReducer,
  message: messageReducer
});
const store = createStore(reducers);

В коде выше мы видим указание на функции userReducer и messageReducer, которые должны предоставить данные. Также, присутствуют стандартные данные по умолчанию, сейчас это просто пустой массив state = {},. поэтому мы делаем return state; При определении хранилище определяется action сам себе, и при отправлении самому себе отправится каждому редюсеру. Теперь state заполнится пустым объектом.

Состояние хранилище указывается свойствами, там пусто, значит state внутри себя будет иметь пустой массив. Когда мы определяем хранилище, этому хранилищу определяется некий dummy-action, который отправляется каждому редюсеру. state хранит в себе пустой объект и он же будет возвращен, store определит свое свойство user как пустой массив. Теперь мы можем просто отправить action с данными нашему стору.

Обратите внимание на иммутабельность. Мы не можем задавать данные повторно, мы обязаны их заново передавать новый объект.

const userReducer = (state = {name: "max", age: 798}, action) =&gt; { 
  switch(action.type) {
    case "CHANGE_NAME": 
    return { ...state, name: action.name };
    //достаем все свойства их старого объекта и вставляем в новый
    //то свойство которое хотим задать в итоге должно быть последним
    case "CHANGE_AGE":
      return { ...state, age: action.age };
  }
  return state;
}
 
const messageReducer = (state = [], action) =&gt; { return state; }
 
 
const reducers = combineReducers({
  user: userReducer,
  message: messageReducer
});
 
const store = createStore(reducers);
 
store.subscribe(() =&gt; {
  console.log(userReducer);
  console.log('Changed', store.getState());
})
 
store.dispatch({ type: "CHANGE_NAME", name: "M2424ax" });
store.dispatch({ type: "CHANGE_AGE", age: 87 });

Теперь хочется отправить посредника между отправляемым экшеном и срабатыванием reducer. Для этого дописываем import { applyMiddleware, createStore } from 'redux'; и import { createLogger } from 'redux-logger'; , и допустим у нас есть следующий код

const reducer = (state = 0, action) =&gt; {
  return state;
}
 
const middlewares = applyMiddleware(createLogger());
const store = createStore(reducer, middlewares);
 
store.dispatch({type: 'INIT'});

Обратите внимание на строку const middlewares = applyMiddleware(createLogger()); , которая будет передавать значение в reducer для подключения посредников к создаваемому хранилищу. Если action сработал, то состояние должно измениться, в консоли отобразится состояние до и после отработки редюсера.

Если подходить чуть менее кустарно к написанию кода, то прописываем thunk и это позволяет прописать ajax-запрос внутри функции и сразу диспатчнуть полученные данные: import thunk from 'redux-thunk';, и меняем строку на const middlewares = applyMiddleware(thunk, createLogger());

store.dispatch((dispatch) =&gt; {
	dispatch({type: "INCREMENT"}),
	dispatch({type: "DECREMENT"})
  })

Научимся работать с запросами: import axios from 'axios';. У запроса может быть три состояния: pending, fulfilled, rejected, и на каждое нужен свой action. Для примера мы будем работать с экспортированными свойствами объектов из Figma в формате JSON, вот тестовый файл. Вы можете экспортировать в качестве JSON любой свой исходник в Figma, воспользовавшись моим сервисом для этого.

const initialState = { 
	db: [], 
	fetched: false,
	userLoad: true,
	loading: true,
	error: null,
	fetching: true
  }
 
  const reducer = (state = initialState, action) =&gt; {
	switch (action.type) {
	  case "FETCH_USERS_PENDING":
		return { ...state, fetching: true }
	  case "RECEIVE_USERS":
		return { ...state, fetching: false, error: action.payload }
	  case "FETCH_USERS_ERROR":
		return {
		  ...state,
		  fetching: false,
		  fetched: true,
		  db: action.payload
		}
	}
	return state;
  }
 
  const middleware = applyMiddleware (thunk, createLogger());
  const store = createStore(reducer, middleware);
 
 
 
 store.dispatch((dispatch) =&gt; {
	 dispatch({ type: "FETCH_USERS_PENDING"})
	 axios.get('http://raw.githubusercontent.com/your-scorpion/JSON_figma_style_export/main/db.json') 
	 .then(response =&gt; {
		 dispatch({ type: "RECEIVE_USERS", payload: response.data })
	 })
	 .catch(err =&gt; {
		 dispatch({ type: "FETCH_USERS_ERROR", payload: err })
	 })
 })

И в консоли мы видим данные, которые получили из внешнего файла.

Теперь, когда мы можем получить практически любое свойство из исходника и передать в компонент, интеграция дизайна с разработкой должна идти чуть более гладко. И последний маленький штрих, нужно подключить Redux к проекту React. В файле с getElementById импортируем import { Provider } from 'react-redux';, и с помощью провайдера надо предоставить хранилище. Импортируем:

ReactDOM.render(
  <Provider store = {store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Готовые компоненты

Не обязательно писать все с нуля. Уже созданы готовые библиотеки со всеми необходимыми молекулами, и мы можем их использовать. Самая популярная это Material Design, есть популярные китайские ant, grommet, semantic. Либо множество готовых компонентов в NPM, просто смотрим на количество пропсов и выбираем наиболее гибкий. Устанавливаем по инструкции и дописываем код в компоненте, у нас сразу получается не плохая кнопка:

import Button from '@material-ui/core/Button';
 
function App22() {
  return (
    <button variant="contained" color="secondary" type="submit" onclick="{(event)" ==""> console.log(event.target)}&gt;
      Sign Up
    </button>
  );
}
 
export default App22;

Мы можем писать сложные компоненты и своими силами, выдавая их разработчикам:

В любом случае, вы каким-то образом раздобыли/написали кастомные компоненты и вам надо начать с ними работать. Закидываем папку с компонентами в папку src, и следующая комбинация команд должна запустить что-либо:

npm install <your-path>/somename --save
cd <your-path>/folder/папка с package.json
npm install
npm start
//либо
import 'название_группы_компонентов/ReactSymbolsKit.css'
import { кнопка } from 'reactsymbolskit'

Далее достаточно разместить уже знакомый нам <Button="text" /> и наслаждаться легкой версткой на компонентах. Скорее всего, будет хотеться менять цвета у компонентов, для этого достаточно установить npm install node-sass-chokidar@0.0.3 --save.

Теперь нам достаточно импортировать компоненты от разработчиков import { RSButton } from 'reactsymbols'; import { RSSwitch } from 'reactsymbols'; и передавать параметры, которые заранее прописаны. Документация к компоненту должна содержать информацию, что мы можем передать size (smallmediumlarge), color, iconSize, iconColor, value и так далее:

<RSSwitch
    checked
    label='Label'
    value='Text'
    background='#000000'
    rounded='88px'
/>
 
<RSButton
    value='Remove'
    size='large'
    iconName='MdWork'
    level='code'
    iconSize={24}
    iconColor='9BFFB7'
    background='#000000'
/>

По аналогии с настройкой таких компонентов. Более того, можно брать любые иконки из Material Design и Font Awesome. Достаточно использовать имя MdArrowBack или FaAbacus и легко менять им цвет.

React это просто библиотека для компонентного подхода, это позволяет нам реализовать очень гибкую или наоборот, строгую и монолитную дизайн-систему, основанную на коде и токенах из дизайн-макетов.

2 комментария

  1. Ruslan Ivanov

    Какие приемы в фигме позволяют сделать структуру такой, чтобы было достаточно гибко?

    • Цветков Максим (Author)

      Если подключать библиотеку, то компоненту можно прописать _ или . в начале имени, тогда он не будет виден.

      При добавлении новых стилей, косая черта / группирует стили.

«Взаимодействуя с данным сайтом, вы, как пользователь, автоматически даете согласие согласие на обработку персональных данных» Согласие

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.