Как мы разрабатывали поле ввода новых сообщений в нашем мессенджере (Gem4me)

    Всем привет!



    Меня зовут Александр Бальцевич, я работаю на лидерской позиции Web-команды проекта Gem4me. Проект представляет из себя инновационный месенджер для всех и каждого (пока в моих фантазиях, но мы стремимся к этому ;-) )


    Коротко о стэке веб-версии: ReactJS (кто бы сомневался) + mobX (лично я вообще не в восторге, но мигрировать никуда не планируем; если интересны детали, в чем именно не устраивает, пишите комментарии — возможно сделаю про это отдельную статью) + storybook + wdio (скриншотное тестирование).


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




    Требования


    Какие возможности должно предоставлять поле ввода?


    • Банально печатать текст, с возможностью писать в несколько строк (нажимая shift + Enter). Опытные пользователи также привыкли использовать markdown-символы для быстрого форматирования сообщения.



    • Вставка текста из буфера. Часто при копировании текста также копируется фон под этим текстом (пример смотрите ниже) и форматирование. Но в поле ввода должен попасть чистый аккуратный текст и ничего лишнего.



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



    Функционал "упоминание пользователя" ("mention") слишком здоровый, его вынесу в отдельный блок. Нужно реализовать следующее:


    • При добавлении символа “@“ после пробела включается режим “упоминание пользователя”. После ввода этого символа нужно сообщить системе, чтобы она отобразила первую пачку участников группы:



    • При добавлении любого символа после "@" нужно отправить этот символ для фильтрации участников группы ("@T" => "T");
    • Если мы в режиме “упоминания пользователя” и у нас, к примеру, набран следующий фильтр "@Tes", и мы начинаем перемещать каретку стрелкой влево ("@Te|s"), все символы справа от каретки не должны учавствовать в фильтрации участников группы. Подобным образом система должна реагировать на любое перемещение каретки внутри набранного фильтра;
    • Если у нас уже есть текст в поле ввода, например "Привет @Хабр Как дела?", и каретка находится слева от символа "@" ("При|вет @Хабр Как дела?") или после первого пробела от "@" ("Привет @Хабр |Как дела?"), мы должны отключить режим “упоминания пользователя” и воспринимать дальнейшие символы как обычный текст;
    • В случае, если мы печатаем текст ("Привет @Хабр Как дела?|") и позже решили вернуться к моменту упоминания пользователя — неважно каким способом, хоть кликом мышки ("Привет @Ха|бр Как дела?") — мы обязаны показать участников группы, подходящих под фильтр между символом "@" и кареткой;
    • В случае, если мы выбираем какого либо участника группы из списка ("@Ха|бр Привет"), режим "упоминания пользователя" должен закончиться и имя выбранного участника группы должно полностью подставиться с пробелом после него. Позиция каретки — справа от пробела, чтобы пользователь мог продолжить печатать текст. Все, что было справа от каретки, должно там и остаться (“@Хабр |бр Привет”).

    Фух, вроде все.


    Бэклог требования


    Также есть требования, которые мы еще не реализовали, но для тех, кому любопытно — вот парочка идей:


    • Во время набора текста или его вставки из буфера, если в нем есть ссылки, мы должны это понять и над полем ввода показать превью сайта. Это помогает пользователю понять, что его текст воспринимается как ссылка и дает возможность увидеть, во что мессенджер интерпретирует ссылку.
    • Как можно чаще держать фокус внутри поля ввода нового сообщения. К примеру, вы открыли чат с кем-то — автофокус в поле ввода (уже реализовано), вы проскролили сообщения и бесцельно покликали пару раз — фокус по-прежнему должен остаться в поле ввода. И если вы сделали форвард сообщения и после этого начали печатать, то текст должен начать печататься уже внутри поля ввода. Требования пока не до конца сформированы, поэтому и описание довольно поверхностное.

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


    Даешь реализацию народу!


    HTML Тэг


    Начинаем мы с выбора, какой HTML тэг использовать в качестве поля ввода. Те, кто пробовал реализовывать сложные поля ввода, к примеру со специфическими правилами форматирования цифр или, как в нашем случае, со сложным взаимодействием с режимом упоминания пользователя, уже знают, что классические тэги input, textarea имеют ограничения, которые не позволяют реализовать нужный функционал.


    К примеру input изначально настроен на то, чтобы текст печатали в одну строку, что противоречит нашему первому требованию.


    Что касается textarea — в нем теоретически можно реализовать режим "упоминания пользователя", но есть еще и визуальная составляющая. К примеру, нам нужна динамическая высота поля ввода (см. ниже). Изначально поле ввода выглядит как обычный однострочный input, но при нажатии shift + Enter у поля ввода увеличивается высота и появляется вторая строка. Высота поля ввода увеличивается до 5 строк, дальше появляется скрол. Для тэга textarea это не совсем типичное поведение, там высота фиксирована, а при увеличении количества строк просто появляется скрол.






    Это не единственное пограничное поведение, которое нас не устраивало в textarea, так что мы, подумав, решили использовать тэг div. С ним можно извращаться как угодно, правда, нельзя редактировать контент, вот незадача! Но это легко правится с помощью свойства contenteditable.


    The contenteditable global attribute is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing. (MDN)

    Благодаря этому свойству все наши базовые требования удовлетворены, многострочные сообщения больше не проблема, да и стилизовать поле ввода стало проще.


    Итого наше поле ввода выглядит следующим образом:


    <div
      className={styles.input}
      placeholder="Type a message"
      contentEditable
    />

    Управление состоянием поля ввода


    Первое, что нам нужно — получать событие о том, что в поле что то вводится. В классическом input тэге существует свойство onChange. В div тэге есть альтернатива в виде onInput, там точно так же получаем синтетическое событие, из которого можем извлечь текущее значение поля ввода. Мы же решили использовать addEventListener для слушания события, это нам в дальнейшем немного поможет унифицировать код. Давайте обновим компонент:


    class Input extends Component {
        setRef = (ref) => {
            this.ref = ref;
        };
        saveInputValue = () => {
            const text = this.ref.innerText;
            this.props.onChange(text);
        };
        componentDidMount() {
            this.ref.addEventListener('input', this.saveInputValue);
        }
        componentWillUnmount() {
            this.ref.removeEventListener('input', this.saveInputValue);
        }
        render() {
            return (
                <div
                    className={styles.input}
                    ref={this.setRef}
                    placeholder="Type a message"
                    contentEditable
                />
            );
        }
    }

    Таким образом мы слушаем событие input и из ref уже достаем через innerText контент. При этом мы оставили div uncontrolled, т.е. мы в него не присваиваем постоянно обновленное значение, а лишь считываем контент.


    Отправка сообщения


    Следующий шаг — отправка сообщения. Мы все привыкли, что сообщение отправляется по нажатию Enter. В данной реализации по нажатию Enter курсор переходит на новую строку. Поэтому нам нужно предотвратить переход на новую строку и отправить сообщение. До события input мы можем повесить событие keydown. Смотрим:


    onKeyDown = (event) => {
      if (event.keyCode === ENTER_KEY_CODE && event.shiftKey === false) {
        event.stopPropagation();
        event.preventDefault();
    
        this.props.submit();
      }
    };
    componentDidMount() {
      this.ref.addEventListener('keydown', this.onKeyDown);
      ...
    }

    event.preventDefault() отменяет дефолтное поведение Enter, а event.stopPropogation() останавливает всплытие данного события. Не забываем, что комбинация Shift+Enter дает пользователю возможность попасть на новую строку.


    Копи-пейст текста и файлов


    Наша цель — вставить скопированный текст без каких-либо артефактов. Для этого надо программно контролировать вставку чего-либо в поле ввода. Для таких целей существует событие paste.


    handlePaste = (event) => {
      event.preventDefault();
    
      const text = event.clipboardData.getData('text/plain');
      document.execCommand('insertText', false, text);
    };
    componentDidMount() {
      ...
      this.ref.addEventListener('paste', this.handlePaste);
    }

    В первой строке event.preventDefault() предотвращает дефолтное поведение вставки, а дальше мы уже извлекаем непосредственно текст (Более подробно можете изучить АПИ объекта event.clipboardData === DataTransfer). Все, что теперь нужно — вставить текст в поле ввода. Для этого используется document.execCommand('insertText', false, text). Используется именно этот метод, т.к. он имитирует дефолтное поведение вставки, то есть вставляет текст в месте курсора.


    Остается лишь доработать вставку файлов. Тут тоже большой магии нет. У нас есть привычный нам fileList, который нужно проверить — пустой ли он, и если нет, то отправить на загрузку картинки:


    handlePaste = (event) => {
      event.preventDefault();
    
      if (event.clipboardData.files.length > 0) {
        // ...
      }
    
      const text = event.clipboardData.getData('text/plain');
      document.execCommand('insertText', false, text);
    };

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


    Emoji


    В данный момент мы используем emoji вашей системы. Есть общеизвестная таблица unicode emoji и все emoji из нее во всех приложениях будут отображаться корректно. Поэтому Emoji как тип данных — это обычная строка, и первое желание — использовать тот же execComand(‘insertText’). Этот метод вставляет элемент туда, где стоит курсор, что нас вполне устраивает и даже прекрасно работает. Но! Если, пытаясь открыть Emoji, пользователь промахнулся по кнопке открытия списка, то фокус оттуда ушел и метод перестает работать.




    Мы немного подумали, как это можно починить, и явно добавили this.ref.focus() перед тем, как вызывать метод:


    insertEmoji = (emoji) => {
      if (this.ref) {
        this.ref.focus();
      }
      document.execCommand('insertText', false, emoji);
    }

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


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


    Отвлечемся на работу с кареткой


    Для работы с курсором в браузере наша команда обнаружила прекрасный API — Selection (MDN).


    A Selection object represents the range of text selected by the user or the current position of the caret. To obtain a Selection object for examination or manipulation, call window.getSelection().

    Вызвав метод window.getSelection() мы можем получить объект, отвечающий за выделенный в текущий момент текст, в котором хранится как начало выделения, так и конец. В случае, если ничего не выделено, то начало и конец будут указывать на одно и то же место — на позицию каретки. Мы решили использовать это API для выяснения позиции каретки. Самое интересное наше открытие состояло в том, как он возвращает позицию. Рассмотрим это на примере:




    В данном примере курсор стоит в слове gem4me, между буквами "m" и "e". Получаем selection объект и смотрим на selection.anchorNode (выделен на скрине) и selection.anchorOffset (5). Интересно, что selection.anchorNode хранит только одну строку, хоть там и нет разделяющих строку тегов, а оффсет хранит, сколько символов до левого края этой ноды. Таким образом, сохранив эту ноду и оффсет, мы можем затем восстановить позицию каретки — что и требовалось. Вот как выглядит код:


    updateCaretPosition = () => {
      const { node, cursorPosition } = this.state;
    
      const selection = window.getSelection();
      const newNode = selection.anchorNode;
      const newCursorPosition = selection.anchorOffset;
    
      if ( node === newNode && cursorPosition === newCursorPosition) {
        return;
      }
    
      this.setState({ node, cursorPosition });
    }

    Теперь нужно определить, когда вызывать этот метод. Я насчитал три места, которые покроют все передвижения:


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


    Но не все кнопки на клавиатуре вызывают onInput. К примеру, стрелки на клавиатуре — не вызывают. Поэтому второй пункт — это реагировать на любое нажатие кнопки клавиатуры, а именно событие keyup (не keydown — для того, чтобы сперва случилось действие, а уже потом мы считали позицию каретки). Получается, что сохранение каретки во многих случаях будет происходить 2 раза (если мы печатаем текст, на событие input и на событие keyup). Но тут мы можем лишь сравнивать значения и не обновлять state, если они совпадают. Мы решили это оставить и ничего не изобретать, т.к. в нашем случае лучше переволноваться, чем недоволноваться =).


    И есть третий случай. А что, если пользователь мышкой поставил каретку на определенную позицию в тексте? В этом случае нам нужно реагировать еще и на событие click и считывать позицию каретки.


    Вроде бы все покрыли. Ура-ура!


    Вернемся к Emoji


    Теперь мы точно знаем, где находится каретка, и нам остается лишь вернуть ее в нужную точку перед вставкой Emoji. Для вставки мы опять же используем браузерное API Range, связанное с Selection.


    The Range interface represents a fragment of a document that can contain nodes and parts of text nodes

    Мы уже знаем ноду и теперь можем выделить фрагмент документа, но каретка туда пока не встанет. Вот как это работает:


    const { node, cursorPosition } = this.state;
    
    const range = document.createRange();
    range.setStart(node, cursorPosition);
    range.setEnd(node, cursorPosition); 

    
Мы указали начало и конец фрагмента, одну и туже точку. В итоге фрагмент — это и есть положение каретки. Осталось лишь вставить в эту позицию каретку следующим кодом:


    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);

    Мы получаем текущий selection, убираем все выделения и присваиваем ему новое выделение, которое и есть позиция каретки. Наконец наш продвинутый this.ref.focus() готов! Курсор стоит там, где надо, поэтому мы можем со спокойной душой использовать уже знакомый нам метод:


    document.execCommand('insertText', false, emoji);

    Вот как выглядит метод в целом:


    customFocus = () => {
      const { node, cursorPosition } = this.state;
    
      const range = document.createRange();
      range.setStart(node, cursorPosition);
      range.setEnd(node, cursorPosition);
    
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);
    }
    
    insertEmoji = (emoji) => {
      this.customFocus();
      document.execCommand('insertText', false, emoji);
    };

    Режим редактирования


    С уже работающим функционалом, описанным выше, вставлять новые фичи стало проще. Напомню последнюю задачу:


    В случае, если поле ввода пустое, при нажатии стрелки вверх должен включиться режим редактирования для последнего написанного сообщения (пример смотрите ниже). Опустим детали — что кроме текста в поле ввода должен появиться некий блок над ним, дающий понять, что пользователь сейчас находится в режиме редактора. Для поля непосредственно нужно, чтобы у нас появился текст последнего сообщения, а курсор стоял в конце этого текста. При нажатии Esc нужно выйти из режима редактирования (то есть очистить поле ввода).

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


    if (
      event.keyCode === UP_ARROW_KEY_CODE &&
      this.props.isTextEmpty &&
      this.props.mode === INPUT_CONTEXT_TYPES.STANDARD
    ) {
      …      
    }

    Дальше нам нужно сообщить системе, что включен режим редактирования, и вставить сообщение. Вот как выглядит в итоге реализация:


    if (
      event.keyCode === UP_ARROW_KEY_CODE &&
      this.props.isTextEmpty &&
      this.props.mode === INPUT_CONTEXT_TYPES.STANDARD
    ) {
      event.preventDefault();
      event.stopPropagation();
    
      this.props.setMode('edit');
      document.execCommand('insertText', false, this.props.lastMessage); 
    }

    Каретка после выполнения document.execCommand('insertText') по дефолту останется в конце сообщения, что нас полностью устраивает.


    Осталось обработать кнопку Esc для отключения мода редактирования. Используем keydown с проверкой на редактирование и очищаем значение в поле ввода. Для очистки используем просто this.ref.textContent = "".


    Упоминание пользователя


    Судя по требованиям, это одна из самых трудоемких задач, но благодаря решению предыдущих задач у нас уже довольно много готовых инструментов. Один из них — как раз метод обновления положения каретки, т.к. из требований видно, что мы должны отработать реакцию на положение курсора. Сейчас нам необходимо отобразить список участников группы, отсортированный по фильтру между кареткой и символом "@". Поэтому дополним метод updateCaretPosition:


    updateAndProcessCaretPosition = () => {
      const { node, cursorPosition } = this.state;
    
      const selection = window.getSelection();
      const newNode = selection.anchorNode;
      const newCursorPosition = selection.anchorOffset;
    
      if (node === newNode && cursorPosition === newCursorPosition) {
        return;
      }
    
      if (this.props.isAvailableMention) {
        this.props.parseMention(node.textContent, cursorPosition);
      };
    
      this.setState({ node, cursorPosition });
    }

    Поле isAvailableMention зависит от того, в каком типе чата мы находимся. Если чат групповой, значение будет всегда true. Остается только провести парсинг этой строки:


    parseMention = (text, cursorPosition) => {
      if (text && cursorPosition && text.includes('@')) {
        const lastWord = text
          .substring(0, cursorPosition)
          .split(' ')
          .pop();
    
        if (lastWord[0] === '@') {
          this.setFilter(lastWord.substring(1, cursorPosition));
          return;
        }
      }
      this.clearFilter();
    };

    Вначале мы проверяем, есть ли символ "@" в текущей строке. Если есть — забираем последнее слово, разделенное пробелом, и если первый символ это "@", то это и есть упоминание пользователя и мы должны сообщать системе об этом. Осталось лишь вставить само упоминание пользователя в поле ввода, а для этого у нас уже все есть.


    insertMention = (insertRestPieceOfMention) => {
      this.customFocus();
      document.execCommand('insertText', false, insertRestPieceOfMention + ' ');
    };

    Как видите, строка с функцией document.execCommand('insertText') повторяется: ее мы, конечно же, тоже вынесли в отдельный метод.


    Эпилог


    Я наивно полагал, что статья про такую малость, как поле ввода, будет легко написана за пару часов, но сейчас, отредактировав эти 20тыс+ символов, должен признать, что это было нелегко. Надеюсь, кому-то из вас пригодится наш опыт и я старался не зря. У нас за время разработки накопилась масса экспертизы — можем рассказать о таких темах, как создание самой модели сообщения, которых мы привыкли видеть десятки типов (текстовое, аудио, файл, стикер, системное, ответ текстом на чей-то стикер и многие другие), о подгрузке сообщений, их синхронизации, удалении, редактировании или скроле к следующему упоминанию пользователя. Если вам интересно почитать про подводные камни реализации месенджера — пишите в комментариях, буду рад приоткрыть занавес.


    Александр Бальцевич, команда Gem4me

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 19

      0
      А что за мессенджер вообще планируется?
      Насколько он будет зависим от единого центра?
      Какое шифрование там планируется, и, в случае e2e, какие будут способы проверить, не произошла ли атака mitm?
      Будет ли он децентрализованным (например, федеративным)?
      Будет ли возможность поднять свой сервер и его клиентам общаться с клиентами на других серверах?
      Какие вообще отличия от того же джаббера, например?
        +1
        Месседжер не планируется, а уже давно работает. Вот только не нужен он никому, кроме МЛМ-адептов Gem4me, ждущих уже не один год, что их месседжер убьёт все Ватсапы с Телеграмами. И вот тогда они (адепты) заживут ваще шикарно. Протокол закрыт, код клиента закрыт. По факту — это простая финансовая пирамида. Ниже добрый человек скинул ссылки.
          0
          Все эти проблемы с потерей фокуса разве не решаются если поле ввода поместить в iframe?
            0
            не очень понимаю, как оно решит проблему с фокусом
            Даже если и решит как то, то коммуникация с этим полем будет сложнее, этой же реализации работы с запоминанием каретки
              0
              В частности Вы пишите-«если, пытаясь открыть Emoji, пользователь промахнулся по кнопке открытия списка, то фокус оттуда ушел и метод перестает работать.

»
              Если он в iframe то не уйдет
                0
                Сейчас потестил обычный input добавил в iframe и кликнул вне iframe и фокус ушел. И звучит это логично. Ведь если бы на 1 странице внутри iframe был бы input и вне iframe еще один input. Тогда у нас была бы возможность поставить 2 каретки, и в какой из них печатался бы текст
            +1

            Можно тезисно про mobx, чем не устроил и если если переезжать, то на что?

              +1
              — местами не работали реакции, когда прям вся команда ожидала реагирования. Мы начали изучать исходники, там код далеко не в лучшем состоянии и они ищут мантейнера
              — пришлось очень много эксперементировать с архитектурой сторов. Т.к. месенджер очень связный, к примеру одно и тоже сообщение может быть в многих состояних: в самом чате, быть запиненым сверху, быть на редактировании в превью, быть в реплае, пересылаться через форвард и еще парочка кейсов. У них всех схожие и отличающиеся @computed свойства и action. Как это хэндлить? есть сырой месседж с бэка, а над ним базовый класс, а там по 2 слоя абстрации в зависимости от использования. В итоге изобретали свой велосипед архитектурный со сторами, вью моделями и шинами в общении и прочим
              — если сильно полагаешься на реактивное програмирование, в виде постоянных реакций, очень тяжело дебажить код. А если в итоге начинаешь использовать только в @observable и @computed реактивность для обновления UI, стоит ли тогда вообще юзать для этого mobX
              и прочие мелкие сомнения, про то что mobx это все таки черный ящик, которому ты должен доверять, в каком порядке он резолвит реакции, синхронно или асинхронно и все такое

              По поводу поменять, да кроме редакса, альтернатив не слышал
                0
                местами не работали реакции

                Нужен конкретный кейс с кодом и ссылочку на codesandbox где эти реакции не работают. Чтобы любой мог это проверить и убедится в этом. Иначе любой может говорить все что угодно, например что у него react не рэднерит тэг img какое же это дно.
                пришлось очень много эксперементировать с архитектурой сторов

                То, что вы описали тут, это не проблемы Mobx, Redux и т.п. Быть программистом и разрабатывать архитектурные решения никто не отменял. Нельзя просто взять какой-то инструмент и он сам по себе работает вне зависимости от того какое у вас приложение, как оно устроена и какая у него логика работы.
                если сильно полагаешься на реактивное програмирование, в виде постоянных реакций, очень тяжело дебажить код. А если в итоге начинаешь использовать только в @observable и @computed реактивность для обновления UI, стоит ли тогда вообще юзать для этого mobX

                Для обновления ui и нужно использовать только @observable и @computed.
                autorun и reaction это не прямые инструменты для обновления ui. И уж темболее никто вам не диктует и не навязывает использовать именно прям все реактивное при реактивное.
                Есть ещё шикарная штука в MobX'e — when.
                fetchData = async () => {
                    // Мы не можем выполнить эту функция и дернуть АПИ, потому что у нас есть зависимость, после получения данных от которой мы можем дергать нужную нам АПИшку
                    await when(() => myDependency.dataLoaded === true);
                    const someIds = myDependency.items.map(item => item.id);
                    await apiRequest.data({ids: someIds });
                    // ...
                }
                

                и прочие мелкие сомнения, про то что mobx это все таки черный ящик, которому ты должен доверять, в каком порядке он резолвит реакции, синхронно или асинхронно и все такое

                Нет никакой магии
                mobx5 — ES6 Proxy
                mobx4 — getters / setters

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

                codesandbox.io/s/zen-surf-g9r9t?file=/src/App.tsx

                Посмотрите код, посмотрите консоль, по нажимаете на кнопочку и снова посмотрите в консоль. После этого раскомментируйте конфигурацию реакций
                setTimeout(() => {
                  configure({
                    reactionScheduler: f => {
                      setTimeout(f, 1);
                    }
                  });
                }, 1);
                

                вот эту, обновите страницу, повторите все действия и посмотрите, что результат уже стал совсем другой, просто небом и земля, теперь все батчится автоматом и реакции все асинхронные и нет нужны в action/runInAction. Далее уберите обертку из setTimeout в которую завернута конфигурация и повторите все, обратите внимание на самый верх вывода консоли, в этом случае даже в самом старте и инициализации кода уже будут асинхронные реакции и autorun уже сработает не 2 раза подряд вначале, а забатчится и сработает 1 раз.

                Теперь вы понимаете как именно это работает и что происходит при тех или иных манипуляциях.

                Лично я использую такой конфиг реакций (завернутый в setTimeout, т.к. при ините мне нужно чтобы реакции были синхронными, а далее в ходе работы приложения уже синхронные не нужны), чтобы все синхронные изменения батчились автоматом и потом уже асинхронно вызывались реакции.
                  0
                  Нужен конкретный кейс с кодом и ссылочку на codesandbox где эти реакции не работают. Чтобы любой мог это проверить и убедится в этом. Иначе любой может говорить все что угодно, например что у него react не рэднерит тэг img какое же это дно.

                  Как меня просили я тезисно описал, и в случае если есть большой интерес это действительно можно в отдельную статью холиварную вынести обсуждение с примерами и прочим

                  То, что вы описали тут, это не проблемы Mobx, Redux и т.п. Быть программистом и разрабатывать архитектурные решения никто не отменял. Нельзя просто взять какой-то инструмент и он сам по себе работает вне зависимости от того какое у вас приложение, как оно устроена и какая у него логика работы.

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

                  Для обновления ui и нужно использовать только @observable и @computed.
                  autorun и reaction это не прямые инструменты для обновления ui. И уж темболее никто вам не диктует и не навязывает использовать именно прям все реактивное при реактивное.
                  Есть ещё шикарная штука в MobX'e — when.

                  Я немного не к тому вел мысль, скорее хотел донести, если от mobx использовать, только 2 эти штуки для обновления UI, то нужен ли мне мобх вообще. Вот мы и пытаемся построить взаимодействие наших сторов тоже на реакциях, а не только UI обновлять.

                  А по поводу when, это не то чтобы шикарная штука, чуть не аккуратно расставишь их, и можешь наплодить кучу промисов, которые никогда не зарезолвятся

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

                  Вы действительно хорошо покрыли базовый кейс. А что если у меня целое дерево реакций. К примеру, я в месенджере перехожу с одного чата в другой, я меняю id группы, мы реагируем на смену, переключаем чат, потом запускаем загрузку сообщений в истории, реагируем на загрузку истории, проверку все ли пользователи есть в мобх или надо догрузить, вычисляем положение скрола взависимости от сообщений, чтобы показать последнее прочитанное и там еще целый ряд реакций. Проблему уже в том, что мы не всегда можем продгнозировать, в каком порядке вызовутся реакции. Опять же я туманно поясняю, для этого я и говорил, что стоило бы в отдельную статью это выносить :)
                    0
                    Я понял, у вас все сводится к тому, что в асинхронном мире Javascript'a вы боретесь с асинхронностью и хотите всё максимально синхронно, т.к. асинхронность конкретно вам мешает, вставляет палки в колеса и делает все не предсказуемым и не прогнозируемым.
                    Вам не нужна эта синхронность, это не Javascript way. В противном случае вы выбрали не тот язык, для синхронности идеально подходит PHP, там все чисто сверху вниз и на каждый запрос создается изолированный процесс. Да PHP можно использовать асинхронно, но это мы затрагивать не будем.
                    Javascript особенно в браузере это вообще не про синхронность, здесь асинхронно всё, вплоть до рендера страницы самим браузером. Поэтому записывать в минусы то, что работает не синхронно, должно быть записано в минусы тому, кто считает что это минус.

                    А по поводу when, это не то чтобы шикарная штука, чуть не аккуратно расставишь их, и можешь наплодить кучу промисов, которые никогда не зарезолвятся

                    Ну как бы никто вам не говорит плохой код написать невозможно если использовать Mobx, с таким же успехом можно while(true) {} воткнуть и потом обвинять в этом JS, потому что в JS так можно сделать. Это же абсурд, не так ли?

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

                    Опять же, это сугубо ваша проблема, а не проблема инструмента. Если вам не хватило дня на пролистывание документации по mobx и экспериментов на подобии тех, что я вам уже скинул по ссылке, то вам нужно этим обязательно заняться.
                    Если не получается рисовать картины, разве в этом виноват холст или кисточка? Особенно когда другие люди по соседству замечательно рисуют на таких же холстах и такими же кисточками, и у них получаются замечательные картины.

                    Как меня просили я тезисно описал, и в случае если есть большой интерес это действительно можно в отдельную статью холиварную вынести обсуждение с примерами и прочим

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

                    Я немного не к тому вел мысль, скорее хотел донести, если от mobx использовать, только 2 эти штуки для обновления UI, то нужен ли мне мобх вообще.

                    Ну если не нужен, то удачи с тонной лапшекода и говнокода гарантируемо идущего с этим в довесок для всех этих дел при использовании голого реакта или реакта + редакса, посмотрим как вы будете кайфовать тогда =)

                    А что если у меня целое дерево реакций.

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

                    Да хоть миллион, порядок реакций не важен, JS асинхронный и проектироваться он должен асинхронно и никак иначе. Если вам кровь из носа нужно откуда-то испускать команды, чтобы синхронно в другом месте что либо делалось, то банальный EventEmitter к вашим услугам, никто не запрещает всё это совмещать. Я использую EventEmitter вместо кучи reaction'ов, это банально проще контролировать и поддерживать. Но это не значит что я не использую reaction и autorun, ещё как использую, но нужно знать грань где лучше одно, а где другое.
                      0
                      Я понял, у вас все сводится к тому, что в асинхронном мире Javascript'a вы боретесь с асинхронностью и хотите всё максимально синхронно, т.к. асинхронность конкретно вам мешает, вставляет палки в колеса и делает все не предсказуемым и не прогнозируемым.

                      По факту да) если я не могу гарантировать предсказуемость, как же я смогу пользователю гарантировать результат, по крайней мере эти две вещи кажутся связанными

                      Ну как бы никто вам не говорит плохой код написать невозможно если использовать Mobx, с таким же успехом можно while(true) {} воткнуть и потом обвинять в этом JS, потому что в JS так можно сделать. Это же абсурд, не так ли?

                      Тут идея чуть в другом, инструмент дает больше возможностей ошибиться. Это как использовать addEventListener('click', this.click), вместо onClick. Во первых, тебе надо сделать removeEventListener и быть уверенным что this.click, это тот же инстанс функции. Вроде все очевидно и не допустишь баг, но с onClick нет возможностей накосячить. А тут тебе в руки дают револьвер в видео потенциальных зависающих промисов и утечек памяти с реакциями которые незадиспоузились, и говорят не престрели детей главное))

                      Я использую EventEmitter вместо кучи reaction'ов, это банально проще контролировать и поддерживать. Но это не значит что я не использую reaction и autorun, ещё как использую, но нужно знать грань где лучше одно, а где другое.

                      Если бы поделились какими наработками, я был бы очень рад :)
                        0
                        Если бы поделились какими наработками, я был бы очень рад :)

                        На здоровье)
                        codesandbox.io/s/dreamy-cartwright-7ncgx?file=/src/App.tsx

                        По факту да) если я не могу гарантировать предсказуемость, как же я смогу пользователю гарантировать результат, по крайней мере эти две вещи кажутся связанными

                        Ну смотрите, синхронность в UI без батчинга изменений и минимального кол-ва рендеров это анти-производительность, анти-паттерн, анти-{дальше придумаете сами}, даже голый реакт против этого и не дает это вам сделать просто так, вот пожалуйста:
                        codesandbox.io/s/friendly-hoover-67l7j?file=/src/App.tsx

                        Это абсолютно логично и правильно когда UI ждет пока все синхронные изменения выполнятся, чтобы так сказать получить финальное состояние на текущий момент и уже только после этого инициализировать рендер.
              +1
              И есть третий случай. А что, если пользователь мышкой поставил каретку на определенную позицию в тексте?

              А что если пользователь на iPadOS и каретку перетаскивает пальцем?

                0
                Вообще теоретически сработает тот же клик, но мы не поддерживаем сейчас веб версией мобильные устройства, а предлагаем скачать нативное приложение
                0

                В эмодзях на Windows нет флагов стран (точнее в качестве них используются пары букв), поэтому добавьте ещё один пункт в ТЗ:


                — Заменять текстовые эмодзи на элементы <img /> с изображениями-полифилами.

                  0
                  все верно, у нас стоит такая задача на обсуждении, для более унифицированного формата картинок между платформами и так же это позволит расширить количество emoji
                  0

                  Благодарю за подробную статью и разбор

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое