Введение

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

case 'some_case':
              return {
                ...state,
                  keyOne:{
                    ...state.keyOne,
                    someProp: payload
                  }
              }
Войти в полноэкранный режим

Выйти из полноэкранного режима

И нет ничего уродливее, чем куча избыточных строк кода, чтобы ваше состояние могло понять, что вы что-то с ним делаете. И это был один из самых фундаментальных архитектурных недостатков/вопросов, с которым команда React столкнулась после введения таких фреймворков, как Vue, svelte, где каждое создаваемое вами состояние является неизменным по своей природе и прослушивает изменение состояния по умолчанию, даже если вы мутируете вложенные данные.
и даже такие библиотеки, как Инструментарий Redux использует immer под капотом, чтобы сделать работу разработчика более удобной.


Почему происходит сценарий?

Что ж, в React все состояния неизменяемы по своей природе, а это означает, что всякий раз, когда вам нужно изменить состояние, данные должны быть скопированы в новый адрес памяти, или, другими словами, должны быть некоторые затраты, и это не проблема для любого. примитивные типы данных, такие как строки или числа. но из-за самой природы объектов JS и того, как они хранятся в памяти, изменение одного свойства внутри объекта не меняет ссылку верхнего уровня на этот объект, а только изменяет его. и это проблема для метода setState и хука useState. потому что, если они не получат новую ссылку, они не смогут узнать, было ли состояние изменено (поскольку по умолчанию React использует поверхностное сравнение)

Древовидная структура данных
— у вас может возникнуть вопрос: «Зачем вообще проводить поверхностное сравнение?» хорошо, давайте подумаем об объекте как о древовидной структуре данных, и каждое свойство является узлом в дереве. Какова временная сложность глубокого сравнения в этом случае? На) [n being the number of nodes in the tree ]но фактически обновленные данные, скорее всего, будут в O (1), поэтому, если подумать, глубокое сравнение будет очень дорогим большую часть времени, и поэтому основная команда React, вероятно, решила использовать поверхностное сравнение в качестве выбора по умолчанию. , но вы можете переопределить это поведение, используя долженОбновитьКомпонент() Метод основан на компонентах класса.

и это означает, что мы, вероятно, можем как-то лучше, чем это

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

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

На прокси очень высокого уровня находится объект, который ведет себя как слой взаимодействия между использованием и фактическим объектом, из которого он был создан.
Источник : Patterns.dev
Прокси дизайн

И уже одно это дает нам сверхвозможности контролировать, как происходят обновления наших данных, а также подписываться на получение/установку событий. именно по этой причине многие современные UI-фреймворки используют Proxy под капотом.

а сегодня попробуем воссоздать очень популярный крючок использованиеImmer
который использует Immer.js для использования шаблона прокси для обработки обновлений состояния


Область действия этого пользовательского хука

[we are gonna be using typescript for this solution but it would be similar in plain javascript as well ]

import useCustomImmer from "./hooks/useCustomImmer";
import { useEffect } from "react";
export default function App() {
  const [value, setValue] = useCustomImmer({
    count: 0,
    name: {
      firstName: "Akashdeep",
      lastName: "Patra",
      count: 2,
      arr: [1, 2, 3]
    }
  });

  const handleClick = () => {
    setValue((prev) => {
      prev.count += 1;
      prev.name.count += 1;
      prev.name.arr.push(99);
    });
  };
  useEffect(() => {
    console.log("render");
  });
  return (
    <div className="App">
      <button onClick={handleClick}>Click me </button>
      <br />
      <span>{JSON.stringify(value)}</span>
    </div>
  );
}

Войти в полноэкранный режим

Выйти из полноэкранного режима

Для этого примера у нас есть состояние, которое имеет некоторые вложенные данные, и в методе щелчка дескриптора мы изменяем свойства, предоставленные нашей функцией установки в ловушке useCustomImmer. [This is supposed to trigger a re-render with the updated state ]

теперь давайте посмотрим на решение

export default function useCustomImmer<StateType>(
  initialValue: StateType
): [value: StateType, setter: (func: (value: StateType) => void) => void] {
  const [value, setValue] = useState<StateType>(initialValue);

const listener =useCallback(debounce((target:Target) => {
    setValue((prevValue)=>({...updateNestedValue(prevValue,target.path,target.value)}))
  },100),[])

  // @ts-ignore
  const valueProxy = useMemo<StateType>(
    () => new Proxy(value, createHandler([], listener)),
    [value, listener]
  );

  const setterFunction = (func: (value: StateType) => void) => {
    func?.(valueProxy);
  };

  return [value, setterFunction];
}


Войти в полноэкранный режим

Выйти из полноэкранного режима

Сигнатура хука будет похожа на useState, где она возвращает массив, где первое значение — это само состояние, а второе значение — функция установки, эта функция будет принимать обратный вызов, который имеет аргумент самого обновленного состояния (аналогично тому, как Функциональный подход useState работает, но здесь состояние аргумента будет не просто обычным объектом, а прокси исходного состояния)

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

Теперь возникает проблема: прослушиватель, который мы подключаем к прокси-серверу для отслеживания событий получения/установки свойств объекта состояния, по умолчанию прослушивает только свойства верхнего уровня и не применяется к вложенным свойствам. [Check this link for a more detailed explanation of the API ].

Чтобы решить эту проблему, нам нужно рекурсивно прикрепить обработчик к нашему объекту, и для этого я написал собственный метод createHandler, который делает то же самое.

const createHandler = <T>(path: string[] = [],listener:(args:Target)=>void) => ({
  get: (target: T, key: keyof T): any => {
    if (typeof target[key] === 'object' && target[key] != null )
      return new Proxy(
        target[key],
        createHandler<any>([...path, key as string],listener)
      );
    return target[key];
  },
  set: (target: T, key: keyof T, value: any,newValue:any) => {
    console.log(`Setting ${[...path, key]} to: `, value);
    if(Array.isArray(target)){
      listener?.({
        path:[...path],
        value: newValue
      })
    }else{
    listener?.({
      path:[...path,key as string],
      value
    })
  }
    target[key] = value;
    return true;
  },
});

Войти в полноэкранный режим

Выйти из полноэкранного режима

этот метод принимает массив строк и прослушиватель в качестве аргументов и возвращает объект со свойствами get/set [You can extend this further to handle more cases ]Теперь давайте сломаем это!!!!!

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

Таким образом, мы не создаем несколько вложенных прокси-серверов каждый раз, когда необходимо обновить состояние, вместо этого мы будем создавать прокси-серверы только тогда, когда кто-то пытается получить доступ к этим свойствам, и это будет намного быстрее, чем просмотр всех узлов в этом объекте, поскольку мы знаем путь, для начала (это означает глубину пути O (log (n)) базы
н)

.

В свойстве set для обработки пограничного случая мы сначала проверяем, является ли свойство массивом (поскольку, когда мы выполняем любую операцию с массивом, такую ​​​​как push и pop, кроме самого элемента, свойство «длина» также изменяется, и мы могли бы обрабатывать этот пограничный случай либо в нашем обработчике или когда мы обновляем состояние, я выбираю сделать это здесь, имейте в виду, что в javascript будут другие структуры данных, которые, вероятно, будут иметь свой пограничный случай. Я создал очень простой хук, чтобы продемонстрировать общую архитектуру).
и каждый раз, когда мы устанавливаем значение, мы также вызываем обратный вызов слушателя с целевым объектом, который будет иметь значение и путь свойства, которое необходимо обновить.

Теперь нам также нужна функция, которая принимает массив путей и значение для обновления нашего исходного объекта, что легко придумать.


const updateNestedValue =<StateType>(object:StateType,path:string[],value:any):StateType=>{
  const keys = path
  let refernce: any = object
  for(let i=0;i<keys.length-1;i++){
    if(keys[i] in refernce){
      refernce = refernce[keys[i]] 
    }     
  }
  if(keys[keys.length-1] in refernce){
    refernce[keys[keys.length-1]] = value
  }
  return object
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

И в нашем обратном вызове прослушивателя мы просто обновляем старое состояние и вызываем метод setValue с обновленным значением.


 const listener =useCallback(debounce((target:Target) => {
    setValue((prevValue)=>({...updateNestedValue(prevValue,target.path,target.value)}))
  },100),[])

Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Вот рабочая песочница для вас, ребята, но имейте в виду этот хук никоим образом не готов к производству или будет готов в будущем (и он не обрабатывает какие-либо крайние случаи), Immer.js имеет очень расширяемый и проверенный в боевых условиях дизайн, этот пост — просто способ оценить, как все работает под капотом. И причина в том, что существует множество типов данных (Maps, Set…), поддерживаемых нативным javascript, для которых реализация прокси-обработчика была бы рутиной (честно говоря, это слишком много работы для меня прямо сейчас), если вы думаете, что можно расширить текущую версию и обрабатывать эти типы, я рад просмотреть любые открытые PR.
но если вы планируете только сериализуемое состояние JSON, это может сработать для вас.


И ссылка на гитхаб

Не стесняйтесь подписываться на меня и на других платформах.