Добрый день, Хабр! Решил поделиться своим небольшим, но полезным открытием в плане использования html data-attributes & css selectors.

Html data-attributes - это кастомные атрибуты, которые вы можете сами назначать куда-угодно и с каким угодно именем (но имя должно начинаться с префикса data-). Затем вы можете использовать их в css селекторах, чтобы влиять на содержимое классов и уже классами управлять элементами. Движок браузера автоматически среагирует на изменение data-атрибута и применит соответствующий код css класса.

Код реакт компоненты, где по нажатию кнопки что-то скрывается, что-то показывается:

import React, {useState} from "react";

const ComponentWithExpandableContent = () => {
    const [expanded, setExpanded] = useState(false)

    return <div style={{
        display: 'flex',
        flexFlow: 'column nowrap',
        alignItems: 'start',
        padding: 32,
        gap: 32,
    }}>

        { !expanded && <div>Initial content</div> }

        <button onClick={()=>setExpanded(!expanded)}>
            { !expanded ? 'Expand' : 'Collapse' }
        </button>

        { expanded && <div>Additional content</div> }
        { expanded && <div>More additional content</div> }

    </div>
}
export default React.memo(ComponentWithExpandableContent)

Состояние expanded=false:

Состояние expanded=true:

Теперь просто html css js.

Файл emulating-use-state.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>
    Эмулируем React useState в обычном JS 
    (via data-attributes & css selectors)
  </title>
  <link rel="stylesheet" type="text/css" href="emulating-use-state.css">
</head>
<body>

<script>
  function toggleExpand(){
    const element = document.getElementById('component')
    if (element.dataset.expanded!=='true'){
      element.dataset.expanded = 'true'
    } else {
      element.dataset.expanded = 'false'
    }
  }
</script>

<div class="someElementContext">
  <!-- data-expanded хранит состояние -->
  <div id="component" class="component" data-expanded="false">
    
    <div class="collapsableContent">
      <div>Initial content</div>
    </div>
    
    <button onClick="toggleExpand()">
      <div class="collapsableContent">Expand</div>
      <div class="expandableContent">Collapse</div>
    </button>
  
    <div class="expandableContent">
      <div>Additional content</div>
    </div>
    <div class="expandableContent">
      <div>More additional content</div>
    </div>
    
  </div>
</div>

</body>
</html>

Файл стилей emulating-use-state.scss:

.someElementContext {
  display: contents;
  
  .component {
    display: flex;
    flex-flow: column nowrap;
    align-items: start;
    padding: 32px;
    gap: 32px;
    
    // Классы, управляющие видимостью элементов на странице
    // в зависимости от текущего значения data-expanded
    .expandableContent {
      display: none;
    }
    .collapsableContent {
      display: contents;
    }
    &[data-expanded=true] .expandableContent {
      display: contents;
    }
    &[data-expanded=true] .collapsableContent {
      display: none;
    }
  }
}

Сгенерированный средой разработки emulating-use-state.css:

.someElementContext {
  display: contents;
}
.someElementContext .component {
  display: flex;
  flex-flow: column nowrap;
  align-items: start;
  padding: 32px;
  gap: 32px;
}
.someElementContext .component .expandableContent {
  display: none;
}
.someElementContext .component .collapsableContent {
  display: contents;
}
.someElementContext .component[data-expanded=true] .expandableContent {
  display: contents;
}
.someElementContext .component[data-expanded=true] .collapsableContent {
  display: none;
}

Получилось поведение аналогичное описанному в реакт компоненте.

Здесь в качестве стэйта выступает кастомный data-атрибут data-expanded - он и хранит состояние. А css селектор [data-expanded=true] изменяет свойство display из none в contents (display: contents означает показывать содержимое элемента так, как будто самого элемента не существует) и обратно в специальных классах-обёртках .expandableContent & .collapsableContent, которыми можно просто обернуть любые элементы в html разметке.

Комментарии (18)


  1. olegkusov
    01.01.2023 16:40
    +3

    Статья ради статьи? Можно было просто кинуть ссылку на MDN по data-аттрибутам


    1. r_Rain Автор
      01.01.2023 16:43
      -1

      Возможно, но здесь я привожу сравнение между реактом и ванильным джаваскриптом. Я лично считаю этот материал полезным use case использования data-атрибутов. Ну и наконец статья полностью соответствует заголовку, так что вы не обязаны читать это, если вам не интересно, но уверен, многим эта статья покажется полезной.


  1. DDroll
    01.01.2023 17:55
    +3

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


    1. r_Rain Автор
      01.01.2023 18:02
      -1

      Подошёл в зеркало посмотрелся, никаких непониманий на лице не нашёл. Реакт перерисовывает Virtual DOM в зависимости от состояния и пропсов. Так же здесь движок браузера перерисовывает в зависимости от состояния data-атрибутов. Если у вас своё видение, то вместе с несогласием с моим мнением прошу его в студию. Я только рад буду узнать поглубже ключевые моменты, которые вы освоили благодаря своему опыту :)


      1. kahi4
        01.01.2023 20:39

        У реакта уже несколько лет нет virtual dom, но это так, к слову. Но я думаю что комментарий был про то, что это просто базовые css запросы и не больше. С недавно наконец-то доехавшим :has можно вообще натворить кучу всего. Но это не заменяет фреймворки и сделать на этом логику «когда догрузятся данные с бэкэнда» хоть и можно, но понадобится js, а там можно и нормальный фреймворк накатить.


        1. r_Rain Автор
          01.01.2023 21:04

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


        1. GoodGod
          02.01.2023 07:45
          +2

          У реакта уже несколько лет нет virtual dom, но это так, к слову.

          Документация с вами не согласна: https://reactjs.org/docs/faq-internals.html

          Конкретно говорится что React Elements - это то что можно "потрогать" из реализации Virtual DOM в React. И еще Fibers говорится, но это вроде Internal, как их потрогать я не знаю, постоянно вижу упоминания в исходниках, но наружу вроде они не торчат.


          1. kahi4
            02.01.2023 15:25
            +1

            Это не более чем абстракция-костыль. Удобная для того чтобы описать как примерно оно работает, но я имел ввиду что до 16 реактор VDOM был как реальный концепт, сейчас это Fibers, которые очень с натяжкой работают как VDOM. Одна из идей в том что реакт умеет рендерить себя далеко не только в DOM, поэтому процессы reconciliation и render разнесены, и reconscilation ответственный за все хуки жизненного цикла и поиск того, что обновилось, а render уже что получится (react native, например).

            Я копался внутри как конкретно работают хуки, они складывают свое состояние в внутренний стэк внутри fiber внутри reconciliation прохода, как и весь рендер процесс, затем очищают то, что не было затронуто этим конкретным рендером (т.е. поменялось что-то внутри и старые компоненты больше не отрендерились), в общем архитектурно это довольно далеко от "отрендерили новый vdom, сравнили со старым vdom, обновили представление", хотя на высоком концептуальном уровне с натяжкой да, это все еще vdom


  1. dopusteam
    01.01.2023 19:01

    А при чем тут useState вообще?


    1. r_Rain Автор
      01.01.2023 19:10
      -1

      Доброго времени суток! useState хранит состояние: должен ли показываться некий дополнительный контент на странице или нет.

      Я описал поведение программы с помощью реакта (как это обычно в нём делается с помощью состояния компоненты) и с другой стороны воспроизвёл то же самое поведение но без реакта. На работе мне пришлось делать форму, у которой по нажатию кнопки должны раскрываться/скрываться некоторые поля. Но пришлось делать мне это на чистом JS, и тут пришла в голову идея удобно использовать data-атрибуты & css селекторы в связке с удобными классами-обёртками (.expandableContent & .collapsableContent). Мне показалось, что эта идея может быть полезна другим разработчикам и я решил написать свою первую статью, так что прошу не судить уж совсем строго.


      1. kosuha666
        01.01.2023 20:48

        Можно было изменять element.style.display свойство чтобы прятать показывать блок.

        Поспешили вы с первой статьей)) да и как по мне не в статьях счастье


        1. r_Rain Автор
          01.01.2023 20:51

          Да, я уже кажется понял, что это слишком простые вещи для хабра, наверное не стоило из этого статью делать. Насчёт element.style.display - мне хотелось изобразить именно как я это пишу в реакте и как бы вынести функционал показа/скрытия в отдельную сущность что ли) И так я добавил div который может иметь только 2 описанных мною класса.


      1. keystore
        01.01.2023 20:52

        А де тада setstate?


        1. r_Rain Автор
          01.01.2023 20:58
          -1

          Доброго времени суток! setState здесь напрямую не показан, но есть функция которая переключает стейт - это toggleExpand(). При желании можно добавить функцию setExpand(expanded), которая будет иметь аналогичный функционал, но строго выставлять состояние, беря его из аргумента expand. Например:

          function setExpand(expanded){
            const element = document.getElementById('component')
            element.dataset.expanded = expanded
          }


      1. yroman
        02.01.2023 03:19
        +1

        Это не то же самое поведение - сокрытие элемента с помощью дисплея и появление/отсутствие его в Dom дереве. Не говоря уже о том, что скрывать что-то существенное типа Полей в форме только с помощью display: none плохая практика.


  1. Fen1kz
    01.01.2023 19:36
    +3

    Я очень разочарован этой статьей. Ожидал увидеть именно эмуляцию useState как хука, потому что как именно он реализован - вообще не очевидно. А получил бесполезное "пастав атрибут и усе заработаед", приправленное зачем-то scss. Прямо теперь хочется пойти и написать статью про эмуляцию useState в жс


  1. dark_gf
    02.01.2023 01:10
    +2

    Статья уровня заменяем setExpanded(!expanded) на expanded != expanded


  1. funca
    02.01.2023 22:27

    То, что вы предлагаете технически возможно, но противоречит общей концепции.

    DOM и движок JS в браузере это разные компоненты, со своими структурами данных, механизмами управления памятью, API и т.п. Перебрасывая данные из одного места в другое, браузер делает массу дополнительных приседаний. Поэтому такие операции относительно медленные. Хотите увеличить перфоманс? - сводите взаимодействие к минимуму.

    vDOM и компания это архитектурный хак, где все состояние предлагают хранить и пересчитывать только внутри JS (reconciliation), без необходимости каждый раз ходить за ним в DOM за тридевять земель. Абстракция render дала возможность подкладывать снизу не только DOM, но и реализации для других, в том числе не браузерных приложений. В вашем варианте все это откатывается обратно, прямо в эпоху jQuery, когда использование data-атрибутов для хранения состояния, таки-да, считалось неплохим решением)