
Понимание работы процессов приходит с изучением механизмов, которые приводят в движение мелкие части большого пазла. Если представить, что Вам дали задачу объяснить, что такое React за полчаса, скорее всего, Вы бы выбрали один из двух вариантов:
-
пересказать все то, что изложено на первой странице официальной документации reactjs.org
-
либо прокомментировать каждый из импортов в репозитории react
Разумеется, можно попробовать скомбинировать оба шага, но есть ли варианты интереснее?
Подготовка
Давайте создадим пустой проект, в который установим две dev зависимости:
yarn add -D parcel typescript
В нашем проекте parcel будет использоваться в качестве бандлера, который не требует настройки (как раз то, что нам нужно), а typescript (точнее typescript compiler — tsc) понадобится для легкого компилирования jsx в js. Для решения второй задачи можно было бы использовать babel, но используя typescript мы дополнительно получим статическую типизацию. Выполним следующую команду:
yarn tsc --init
Подробнее про файл node_modules/.bin/tsc
При установке пакетов, yarn (или npm) проверяет, есть ли у зависимости исполняемый файл через поле bin в файле package.json.
Когда мы устанавливали typescript, бинарных файла было сразу два:
// node_modules/typescript/package.json "bin": { "tsc": "./bin/tsc", "tsserver": "./bin/tsserver" }
Если поле bin заполнено, то пакетный менеджер создает symlink (символическую ссылку) на указанный путь и помещает ее в директорию node_modules/.bin
Таким образом node_modules/.bin/tsc — это символическая ссылка на файл node_modules/typescript/bin/tsc
Когда мы запускаем инструкцию yarn <bin_name> — пакетный менеджер проверит наличие <bin_name> по адресу node_modules/.bin и если таковой найден, то он исполняется.
Это поможет нам сгенерировать файл конфигурации для typescript — tsconfig.json. Далее нам необходимо сделать несколько косметических изменений:
-
Раскомментируем строку
"jsx": "preserve"— и заменим"preserve"на значение"react". Таким образом мы указываем, какой тип output в случае появления jsx мы получим (подробнее поговорим о jsx в следующем разделе). Все варианты можно рассмотреть по ссылке. -
Изменим значение флага «strict» с
"true"на"false". Сделаем это, чтобы не отвлекаться на предупреждения во время работы над нашей версией React.В итоге, изменения в tsconfig.json будут выглядеть следующим образом:
// tsconfig.json - // "jsx": "preserve" /* Specify what JSX code is generated. */, + "jsx": "react" /* Specify what JSX code is generated. */, - "strict": true /* Enable all strict type-checking options. */, + "strict": false /* Enable all strict type-checking options. */,
Все готово для начала работы! Чтобы убедиться, что мы готовы писать код, предлагаю начать с создания index.html со следующим содержимым:
// index.html <script src="index.tsx" type="module"></script>
Соответсвенно, следующим шагом будет создание index.tsx, в котором мы выведем сообщение в консоль console.log("hello react");
// index.tsx console.log("hello react");
Для того, чтобы запустить веб-сервер, добавим следующий блок в файл package.json в корне нашего проекта:
// package.json "licence": "MIT", "scripts": { "start": "parcel index.html" },
Таким образом, после запуска yarn start в терминале, мы запустим приложение на 1234 порту локалхоста http://localhost:1234, при этом страница будет совершенно пустой, но в консоли будет выведено приветствие из файла index.tsx
JSX & React Elements
Рассмотрим объявление переменной в следующем блоке кода:
const element = <h1>React, what are you?</h1>;
Официальная документация React (в переводе которой на русский язык принял участие в том числе Ваш покорный слуга), начинает объяснение JSX с фразы:
Этот странный тег — ни строка, ни фрагмент HTML
? Интересное начало!
На мой взгляд, самым наглядным объяснением будет пример из песочницы typescript или babel (для babel не забудьте отжать галочку react слева!), где наше выражение <h1>React, what are you?</h1> превращается в следующую запись.
"use strict"; React.createElement("h1", null, "React, what are you?");
Первое знакомство с React.createElement
Рассмотрим подробнее данную запись.
Очевидно, что первый параметр — это тег, в нашем случае h1.
Второй параметр равен null, потому что мы не передали атрибуты. Если передать один или несколько атрибутов, второй параметр превратится в объект, в качестве ключей/значений которого будут имена и значения атрибутов. Такой объект в реакте называется пропс.
Третий параметр — это содержимое нашего тега, обычная строка. Если бы вложением была не строка или число, а другая разметка, то мы бы получили новый вызов React.createElement.
Важно! Если внутри тега будет вложено сразу несколько дополнительных тегов, число параметров может увеличиться с трех до n + 2, где n — это количество вложенных тегов одного уровня. Таким образом:
<div> <p>1</p> <p>2</p> </div>
преобразуется в
React.createElement("div", null, React.createElement("p", null, "1"), React.createElement("p", null, "2"));
где у начального вызова React.createElement можно насчитать 4 параметра.
Получается, что после транспайлинга <h1>React, what are you?</h1> вместо верстки в переменную element запишется результат вызова React.createElement с тремя параметрами.
Убедимся в этом сами, в файле index.tsx добавим следующее содержимое:
// index.tsx const element = <h1>React, what are you?</h1>; console.log(element);
Сохраним изменения и проверим сообщение в консоли на странице http://localhost:1234
Uncaught ReferenceError: React is not defined
Ошибка вызвана тем, что TypeScript compiler выполнил свою работу как надо и в результате в переменную element должен попасть результат работы React.createElement, но проблема в том, что мы нигде не определили переменную React.
А что в обычной жизни?
В обычной жизни (до появления 17-ой версии react) можно было бы просто установить npm пакет react и добавить в начало файла index.tsx следующий импорт:
// index.tsx import React from "react"
Действительно, это решит проблему (убедитесь, что у Вас добавлен атрибут type=»module» тегу script в index.html) и в результате мы увидим следующий вывод в консоль:

Целью же этой статьи является разбор базовых концепции реакта, поэтому мы пойдем другим путем и напишем свою реализацию React.
В качестве быстрого решения, создадим переменную React и присвоим ей пустой объект:
// index.tsx + const React = {}; const element = <h1>React, what are you?</h1>; console.log(element);
Теперь ошибка в консоли примет другой вид:
Uncaught TypeError: React.createElement is not a function
Что выглядит вполне логично, поэтому создадим метод createElement и выведем в консоль передаваемые параметры:
// index.tsx const React = { createElement: (...params) => { console.log(params); }, }; // ...
Ошибка повержена, и мы наблюдаем массив из трех параметров, в точности как те, что мы видели в песочнице TypeScript!
Чтобы лучше разобраться, как работает React.createElement — усложним разметку. В итоге файл index.tsx примет следующий вид:
// index.tsx const React = { createElement: (...params) => { console.log(params); }, }; const element = ( <div> <header>Header</header> <main> <h1>Page title</h1> <p>lorem...</p> </main> <footer>Footer</footer> </div> ); console.log(element);
Если посмотреть, что выведет консоль — получим интересную картину:
[‘header’, {…}, ‘Header’]
[‘h1’, {…}, ‘Page title’]
[‘p’, {…}, ‘lorem…’]
[‘main’, {…}, undefined, undefined]
[‘footer’, {…}, ‘Footer’]
[‘div’, {…}, undefined, undefined, undefined]
У неискушенного читателя может возникнуть несколько вопросов:
-
Чем обусловлен именно такой порядок вызовов?
-
Откуда взялись параметры undefined при вызовах для тегов main и div?
-
Вызовы createElement рекурсивны. Если взять родительский тег div, то в процессе вызова сначала выполнится вложенный вызов
React.createElement("header", null, "Header")и другие вложенные вызовы, а только потом закончит работу первоначальный вызовReact.createElement("div", …); -
Настоящий вызов React.createElement (из npm пакета react) возвращает объект React элемента, а наша же функция пока только выводит параметры в консоль и ничего не возвращает (undefined). Исправим это! ???
Модифицируем нашу функцию createElement, чтобы она возвращала элемент следующего вида:
// index.tsx const React = { createElement: (tag, props, ...children) => { return { tag, props: { ...props, children, }, }; }, }; // ...
В реакте содержимое элемента автоматически доступно как проп children. Вложенных элементов может быть много, а может и не быть совсем, поэтому собираем все вложенные элементы в массив с помощью rest оператора.
После проделанных манипуляций в консоли можно увидеть древовидную структуру, которая полностью соответствует нашей разметке!
Но как же вывести результат на экран? ?
Перед тем как сделать это, заменим в файле index.tsx элемент на компонент.
Подробно разница между элементом и компонентом разобрана в официальном блоге на reactjs.org.
С практической точки зрения, вместо переменной element мы создадим функцию App, которая будет возвращать элемент:
const App = () => ( <div> <header>Header</header> <main> <h1>Page title</h1> <p>lorem...</p> </main> <footer>Footer</footer> </div> );
Чтобы научить нашу версию метода React.createElement работать с компонентами, добавим следующую проверку:
// index.tsx const React = { createElement: (tag, props, ...children) => { + if (typeof tag === "function") { + return tag({ ...props, children }); + } return { tag, props: { ...props, children, }, }; }, }; + const App = () => ( - const element = ( <div> <header>Header</header> <main> <h1>Page title</h1> <p>lorem...</p> </main> <footer>Footer</footer> </div> ); + console.log(<App />); - console.log(element);
Если tag является функцией, createElement вернет результат ее вызова, передав props, в состав которых будет входить и children.
Эта особенность работы React.createElement частично объясняет ограничение JSX expressions must have one parent element.
ReactDOM render
Но для того, чтобы приложение появилось на экране браузера, необходимо в файле index.tsx добавить следующую строку кода.
ReactDOM.render(<App />, document.getElementById("root"));
Сразу мы получим ожидаемую ошибку Uncaught ReferenceError: ReactDOM is not defined, чтобы обойти которую добавим заглушку вида:
const ReactDOM = { render: (...params) => { console.log(params); }, };
В консоли пропала ошибка и мы видим массив из двух элементов. Первый — уже знакомое нам древовидное представление разметки, а второй null. Значение null появилось ожидаемо, ведь document.getElementById(«root») не смог найти элемент с атрибутом id равным root. Чтобы такой элемент появился, добавим в index.html следующую строку:
// index.html + <div id="root"></div> <script src="index.tsx" type="module"></script>
В качестве id элемента можно было выбрать любое значение, но root очень хорошо подчеркивает назначение только что добавленного тега. Это будет корневой контейнер, в который мы добавим наше дерево (jsx элемент — результат вызова App).
Далее напишем реализацию ReactDOM.render:
const ReactDOM = { render: (element, container) => { if (typeof element === "string" || typeof element === "number") { container.appendChild(document.createTextNode(String(element))); return; } const { props, tag } = element; const domElement = document.createElement(tag); if (props.children) { for (const child of props.children) { ReactDOM.render(child, domElement); } } for (const prop in props) { const value = props[prop]; if (prop !== "children") { domElement[prop] = value; } } container.appendChild(domElement); }, };
Разберем каждый блок кода в ReactDOM.render отдельно:
-
В случае, когда элемент, пришедший к нам является примитивом (строкой или числом) — мы создаем текстовую ноду и добавляем ее к контейнеру.
if (typeof element === "string" || typeof element === "number") { container.appendChild(document.createTextNode(String(element))); return; }
2. Если элемент не является примитивом, то мы ожидаем объект, у которого есть поля tag и props. На основании поля tag создадим DOM элемент.
const { props, tag } = element; const domElement = document.createElement(tag); if (props.children) { for (const child of props.children) { ReactDOM.render(child, domElement); } }
В указанном выше блоке, мы проверяем наличие пропы children. Если она есть — то для каждого чайлда рекурсивно вызываем ReactDOM.render, где в качестве контейнера передается созданный DOM элемент.
Таким образом, мы рендерим всех наследников в родителя (который в свою очередь может быть наследником для другого элемента).
Далее, для всех остальных пропов (кроме children) добавим соответствующие атрибуты DOM элементу.
for (const prop in props) { const value = props[prop]; if (prop !== "children") { domElement[prop.toLowerCase()] = value; } } container.appendChild(domElement);
И в самом конце добавим полученный DOM элемент в контейнер.
В итоге на экране появится ожидаемый результат — наша разметка!

Конечно, можно было бы просто создать .html файлик и не мучаться, но такой разбор помог нам лучше понять как работает React.createElement и ReactDOM.render.
Следущий на очереди хук useState, но сначала произведем небольшой рефакторинг.
Добавляем типизацию + разносим код по отдельным файлам
Следующий раздел можно пропустить, если Вы хотите сосредоточиться именно на том, что касается React.
Цель этого блока сделать код более читаемым и структурированным.
Опишем типы и интерфейсы, которые мы используем. Для этого создадим отдельный файл types.ts:
// types.ts export type Component<T = {}> = (props: IPropsWithChildren<T>) => JSX; export type ReactTag = HTMLTag | Component; type HTMLTag = keyof HTMLElementTagNameMap; export type JSX = IElement | string | number; interface IElement { tag: HTMLTag; props: IPropsWithChildren; } export type IPropsWithChildren<P = {}> = P & { children?: JSX[] };
Следующим шагом вынесем самописную версию ReactDOM в отдельный файл:
// react-dom.ts import { JSX } from "./types"; const ReactDOM = { render: (element: JSX, container: HTMLElement): void => { if (typeof element === "string" || typeof element === "number") { container.appendChild(document.createTextNode(String(element))); return; } const { props, tag } = element; const domElement = document.createElement(tag); if (props.children) { for (const child of props.children) { ReactDOM.render(child, domElement); } } for (const prop in props) { const value = props[prop]; if (prop !== "children") { domElement[prop.toLowerCase()] = value; } } container.appendChild(domElement); } }; export default ReactDOM;
Аналогично создадим отдельный файл для React:
// react.ts import { ReactTag, JSX, IPropsWithChildren } from "./types"; const React = { createElement: ( tag: ReactTag, props: IPropsWithChildren, ...children: JSX[] ): JSX => { if (typeof tag === "function") { return tag({ ...props, children }); } return { tag, props: { ...props, children, }, }; } }; export default React;
Отдельный файл для компонента App:
// App.tsx import React from "./react"; import { Component } from "./types"; const App: Component = () => ( <div> <header>Header</header> <main> <h1>Page title</h1> <p>lorem...</p> </main> <footer>Footer</footer> </div> ); export default App;
В итоге index.tsx превратится в удобно читаемый файл:
// index.tsx import React from "./react"; import ReactDOM from "./react-dom"; import App from "./App"; const rootContainer = document.getElementById("root"); ReactDOM.render(<App />, rootContainer);
ReactDOM rerender
Ключевой особенностью this.setState в классовых компонентах и второго параметра из массива от useState в функциональных компонентах является возможность обновлять UI при изменении состояния. Такой результат достигается благодаря возможности вызывать ререндер. Давайте реализуем такую возможность как метод ReactDOM:
// react-dom.tsx import { JSX } from "./types"; + import React from "./react"; + import App from "./App"; // ... + rerender: () => { + const rootContainer = document.getElementById("root"); + rootContainer.removeChild(rootContainer.firstChild); + ReactDOM.render(<App />, rootContainer); + }
Сначала мы удаляем всё, что находится в root контейнере. Затем рендерим <App /> в контейнер.
☢️ Обратите внимание, что мы также изменяем расширение файла react-dom.ts на react-dom.tsx, поскольку при вызове ReactDOM.render первым параметром будет JSX element. Вдобавок к этому, мы добавляем import React from "./react".
На следующем шаге перейдем к стейту.
Самодельный useState
Для того, чтобы показать работу useState — создадим отдельный компонент счетчика (Counter), который предсказуемо будет выводить значение на экран. Также у нас будет две кнопки для инкремента и декремента.
Как выглядит использование хука useState в React? Рассмотрим на примере нового компонента Counter:
// Counter.tsx import React from "./react"; import { Component } from "./types"; interface ICounterProps { initialValue: number; } export const Counter: Component<ICounterProps> = ({ initialValue }) => { const [value, setValue] = React.useState(initialValue); return ( <div> <h2>Counter: {value}</h2> <div> <button onClick={() => setValue(value - 1)}>-</button> <button onClick={() => setValue(value + 1)}>+</button> </div> </div> ); };
Мы уже с Вами знаем, что в нашей версии React нет реализации useState, поэтому напишем свою версию.
Перед началом рассмотрения реализации хука, вспомним про важную особенность стейта. Она состоит в том, что useState сохраняет значения между рендерами. Чтобы добиться такого поведения — создадим объект globalState, в котором будем хранить массив всех стейтов + курсор. Изначально массив пуст, а курсор равен нулю:
// types.ts export interface IGlobalState { states: any[]; cursor: number; } // react.ts import { IGlobalState, ReactTag, JSX, IPropsWithChildren } from "./types"; const globalState: IGlobalState = { states: [], cursor: 0, };
Когда мы перейдем к реализации useState, станет понятно зачем нам нужен курсор.
С формальной точки зрения, useState — это функция, которая принимает единственный параметр: начальное значение. Возвращает же массив и функцию для обновления стейта.
На первом рендере useState возвращает начальное значение, но затем нам нужно проверять, есть ли уже значение для данного хука в globalState. Именно курсор поможет нам получить доступ к нужному элементу массива, где хранится значение текущего стейта. Реализация будет выглядеть следующим образом:
// react.ts import ReactDOM from "./react-dom"; // ... useState<T>(initialValue: T): [state: T, setState: (newState: T) => void] { const currentCursor = globalState.cursor; const state = globalState.states[currentCursor] || initialValue; const setState = (newValue: T) => { globalState.states[currentCursor] = newValue; ReactDOM.rerender(globalState); }; globalState.cursor += 1; return [state, setState]; },
Наша задача — создать массив, состоящий из state и setState. На первой итерации массив globalState.states пуст, поэтому в качестве state вернется initialValue.
Также мы фиксируем globalState.cursor в локальной переменной currentCursor, т.к. затем глобальный курсор будет увеличен на единицу.
Может возникнуть вопрос, а как будет происходить сброс курсора?
Для этого нам необходимо добавить последний штрих. Вызывая метод ReactDOM.rerender из setState, мы передадим globalState в качестве параметра, чтобы затем установить глобальный курсор на ноль перед следующим рендером.
// react-dom.tsx - import { JSX } from "./types"; + import { IGlobalState, JSX } from "./types"; // ... - rerender: () => { + rerender: (globalState: IGlobalState) => { const rootContainer = document.getElementById("root"); rootContainer.removeChild(rootContainer.firstChild); + globalState.cursor = 0; ReactDOM.render(<App />, rootContainer); },
Вызовем компонент Counter в теле нашего главного компонента App с начальным значением 646:
// App.tsx import React from "./react"; import { Component } from "./types"; + import { Counter } from "./Counter"; const App: Component = () => ( <div> <header>Header</header> <main> <h1>Page title</h1> <p>lorem...</p> + <Counter initialValue={646} /> </main> <footer>Footer</footer> </div> ); export default App;
Таким образом на экране мы увидим интерактивный счетчик, который под капотом использует API очень похожий на react и react-dom.

Если Вас заинтересовал процесс воссоздания реакта — обязательно загляните в исходный код. Там Вы найдете много чего полезного для понимания устройства библиотеки.
Выводы
Целью данной статьи было показать альтернативный вариант знакомства с React. Буду благодарен обратной связи!
Исходники можно найти по ссылке — github.com/SmolinPavel/create-react.
ссылка на оригинал статьи https://habr.com/ru/post/652487/
Добавить комментарий