Search code examples
solid-js

Is Context good for dependency injection of services in SolidJS?


I'm just getting started with SolidJs and am learning about Context. It seems like this might be a good way to inject singleton services throughout my application, some of which might be reactive. Is that an appropriate use? For example, I might have an authentication service for handling determining who is the current user and provide a sign out method. I might configure database access too, such as whether to work with a local emulator or the real db. Here is a stripped down version of how it works, with string substituting for the actual services and all the configuration of them. It makes sense to me to configure these services as props on the ServicesProvider on the root. Also notice I've made the ServicesContext variable private to the module and nullable, because I don't want to define a default value; instead I want the value defined as a function of parameters passed to the Provider object. This is different than the samples I've seen since the variable is not exported.

let ServicesContext: Context<string> | undefined = undefined;

export function ServicesProvider(props: {
  useMocks: boolean;
  children: JSXElement;
}) {
  const svcs = props.useMocks ? "mocks" : "real stuff";
  ServicesContext = createContext(svcs);
  return (
    <ServicesContext.Provider value={svcs}>
      {props.children}
    </ServicesContext.Provider>
  );
}

export function useServices() {
  if (ServicesContext === undefined)
    throw new Error(
      "Trying to use the services context when it hasn't been rendered."
    );
  return useContext(ServicesContext);
}

// When using the provider, I want to configure all the services
// here, like whether they are mocked or not and other settings.
render(
  () => (
    <ServicesProvider useMocks={true}>
      <RootApp />
    </ServicesProvider>
  ),
  root
);

Solution

  • Since Solid allows accessing values through scope chain, you can initialize your services in their own modules and pass them directly to the related component. So, you are not limited to context but context API definitely provides cleaner way for some uses cases especially if you are going to run some context sensitive logic.

    Here is a simplified snipped from an actual application:

    const App = () => {
      // Initializations and application wide bindings
    
      return (
        <AppContext.Provider
          value={{
            commands,
            keybindings,
            state,
            socket,
          }}
        >
          <ErrorBoundary fallback={errorFallback}>
           {/* Other Components */}
          </ErrorBoundary>
        </AppContext.Provider>
      );
    };
    

    The inner components can bind their own commands when they are mounted and unbinds when they are cleaned up. This way, we can use same keybindings for different purposes.

    As you might guess, state service is used to pass reactive values to the inner components.

    You can read more about context:

    There is only one downside though, but with a simple solution. Vite's hot module reloading does not work for components using AppContext if you use an empty object as the default value. That is because AppContext falls back to its default value when module reloads. Since we would be trying to access an undefined property, we get an error.

    Extracting the context component into its own module solves this problem since it moves the context component out of the update path, allowing the module to preserve its state. Now, HMR breaks only when you update the context module, but we don't update context component that often anyways.

    There are other workarounds, the most crude one being the use of // @refresh reload pragma which cold reloads the page and takes away the benefits of HMR.