1

Given this simple custom hook

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() } />;

When I use either of these, calling set on the context does not update anything. Sure, how React would know that I have updated anything? But what could be done to make it work (excluding using a Provider)?

** Edit **

Ok, so what is the alternative other than using useContext then? That's the real question, really; how do I update the components using this hook (or HOC)? Is useState the only way? How? Using some event emitter?

Yanick Rochon
  • 51,409
  • 25
  • 133
  • 214
  • 1
    Why do you not want to use a provider? If you don't want a provider, context is probably not the right tool to be using. – Nicholas Tower Aug 19 '20 at 18:28
  • If you want the component to update, just mix it with a `useState`. – Keith Aug 19 '20 at 18:30
  • You have to have a provider if you want to use context. There isn't any way around that. From what it looks like, all you're doing is getting and setting items from localStorage, which if that is the case, you could just use the useState hook instead. – Chris Aug 19 '20 at 18:31
  • Is the following an accurate description of what you're trying to do? You want a centralized piece of code which loads a value from local storage on boot, and then you can make changes to it from any component in the app. Those changes get pushed to local storage, and also trigger components that are using the value to rerender. – Nicholas Tower Aug 19 '20 at 18:36
  • @NicholasTower yup. pretty much. – Yanick Rochon Aug 19 '20 at 18:42
  • @YanickRochon I would do that with context and a provider. Are you open to that, or are providers banned as a solution? – Nicholas Tower Aug 19 '20 at 18:43
  • @NicholasTower not banned, I was trying to avoid creating a crap load of nested components.... I already have a layout with about a dozen providers stacked on top of each other, and wanted a better solution. – Yanick Rochon Aug 19 '20 at 18:45

1 Answers1

3

I think using context does make sense here, but you will need to use a provider, as that's a core part of how context works. Rendering a provider makes a value available to components farther down the tree, and rendering with a new value is what prompts the consumers to rerender. If there's no provider than you can at least get access to a default value (which is what you have in your code), but the default never changes, so react has nothing to notify the consumers about.

So my recommendation would be to add in a component with a provider that manages the interactions with local storage. Something like:

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>
  );
};

You mentioned in the comments that you wanted to avoid having a bunch of nested components, and you already have a big stack of providers. That is something that will often happen as the app grows in size. Personally, my solution to this is to just extract the providers into their own component, then use that component in my main component (something like<AllTheProviders>{children}</AllTheProviders>). Admittedly this is just an "out of sight, out of mind" solution, but that's all i really tend to care about for this case.

If you do want to completely get away from using providers, then you'll need to get away from using context too. It may be possible to set up a global object which is also an event emitter, and then have any components that want to get access to that object subscribe to the events.

The following code is incomplete, but maybe something like this:

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];
})

You could pull in a pub/sub library if you don't want to implement it yourself, or if this is turning into to much of a project, you could use an existing global state management library like Redux or MobX

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • missing `children` in function props destructuring... – Yanick Rochon Aug 19 '20 at 19:18
  • I ended up writing a `AllProviders` component, as you suggested, putting my stairway-to-hell-of-Providers inside. I guess I could refactor and combine a few, but I'm preaching for a separation of contract architecture :) Perhaps i should write a `useGlobalState` hook and publish it... – Yanick Rochon Aug 20 '20 at 17:42