Как я могу обновить все компоненты React, используя контекст без поставщика?

Учитывая этот простой кастомный хук

import React, { createContext, useContext } from 'react';


const context = {
   __prefs: JSON.parse(localStorage.getItem('localPreferences') || null) || {} ,
   get(key, defaultValue = null) {
      return this.__prefs[key] || defaultValue;
   },
   set(key, value) {
      this.__prefs[key] = value;
      localStorage.setItem('localPreferences', JSON.stringify(this.__prefs))
   }
};

const LocalPreferenceContext = createContext(context);

export const useLocalPreferences = () => useContext(LocalPreferenceContext);

export const withLocalPreferences = Component => () => <Component localPreferences={ useLocalPreferences() } />;

Когда я использую любой из них, вызываю setпо контексту ничего не обновляет. Конечно, как React узнает, что я что-то обновил? Но что можно сделать, чтобы это заработало (кроме использования провайдера)?

** Редактировать **

Хорошо, так что есть альтернатива, кроме использования useContextтогда? На самом деле это настоящий вопрос; как мне обновить компоненты с помощью этого крючка (или HOC)? ЯвляетсяuseStateединственный способ? Как? Используете какой-нибудь эмиттер событий?

1 ответ

Решение

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

Поэтому я рекомендую добавить компонент с поставщиком, который управляет взаимодействием с локальным хранилищем. Что-то типа:

const LocalPreferenceProvider = () => {
  const [prefs, setPrefs] = useState(
    () => JSON.parse(localStorage.getItem("localPreferences") || null) || {}
  );

  // Memoized so that it we don't create a new object every time that
  //   LocalPreferenceProvider renders, which would cause consumers to
  //   rerender too.
  const providedValue = useMemo(() => {
    return {
      get(key, defaultValue = null) {
        return prefs[key] || defaultValue;
      },
      set(key, value) {
        setPrefs((prev) => {
          const newPrefs = {
            ...prev,
            [key]: value,
          };
          localStorage.setItem("localPreferences", JSON.stringify(newPrefs));
          return newPrefs;
        });
      },
    };
  }, [prefs]);

  return (
    <LocalPreferenceContext.Provider value={providedValue}>
      {children}
    </LocalPreferenceContext.Provider>
  );
};

В комментариях вы упомянули, что хотите избежать кучи вложенных компонентов, и у вас уже есть большой стек провайдеров. Это часто случается по мере увеличения размера приложения. Лично мое решение - просто извлечь поставщиков в их собственный компонент, а затем использовать этот компонент в моем основном компоненте (что-то вроде<AllTheProviders>{children}</AllTheProviders>). По общему признанию, это просто решение типа "вне поля зрения, из виду", но это все, что меня действительно волнует в этом случае.

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

Следующий код неполный, но может быть что-то вроде этого:

const subscribers = [];
let value = 'default';
const globalObject = {
  subscribe: (listener) => {
    // add the listener to an array
    subscribers.push(listener);
    
    // TODO: return an unsubscribe function which removes them from the array
  },
  set: (newValue) {
    value = newValue;
    this.subscribers.forEach(subscriber => {
      subscriber(value);
    });
  },
  get: () => value
}

export const useLocalPreferences = () => {
  let [value, setValue] = useState(globalObject.get);
  useEffect(() => {
    const unsubscribe = globalObject.subscribe(setValue);
    return unsubscribe;
  }, []);
  return [value, globalObject.set];
})

Вы можете использовать библиотеку pub/sub, если не хотите реализовывать ее самостоятельно, или если это превращается в большую часть проекта, вы можете использовать существующую глобальную библиотеку управления состоянием, такую ​​как Redux или MobX.

Другие вопросы по тегам