DEV Community

Andi Rosca
Andi Rosca

Posted on • Originally published at andi.dev

Syncing solid-js resources to a global store

Solid provides a createResource function that gives you a few niceties out of the box for working with async data.

Stuff like:

  • automatic refetching on reactivity changes
  • signals for checking if the data is loading
  • and of course, Suspense.

Suspense can't be triggered any other way, so most solid apps are going to be using resources extensively.

The drawback is that createResource pushes you into a pattern of data locality, where the data is only being used in the component that defined the resources (or it's children).

Data locality is nice until it isn't

Most complex web-apps make use of a global state, and tying data management and fetching to components makes it clunky to properly sync data between global and local.

A naive first approach for achieving this syncing might look like this:

const [data] = createResource(fetchData);
const [state, setState] = useContext(GlobalState);

createEffect(() => {
  setState("data", data());
});
Enter fullscreen mode Exit fullscreen mode

The issue with this is that you can sync the data to the store, but if there's any reactive change to the data in the store, the resource data wouldn't change.

So syncing only works one way resource -> store.

The storage option

Solid resources accept a storage property that can be used to override the internal logic for holding the resource's data.

We can imagine the simplified internals of a createResource to look something like this:

const createResource = (fetchingFunction) => {
  const [data, setData] = createSignal();
  createEffect(() => {
    fetchingFunction().then(result => setData(result));
  })
}
Enter fullscreen mode Exit fullscreen mode

The resource fetches the data automatically and stores the result in a reactive signal.

The storage option would let us replace that createSignal call with any other function that has the return type as a signal, which would be [Getter, Setter].

The solid docs have an example of using the storage option to store the data in a solid store instead of a signal. See createDeepSignal here.

function createDeepSignal<T>(value: T): Signal<T> {
  const [store, setStore] = createStore({ value });
  return [
    () => store.value,
    (v: T) => {
      const unwrapped = unwrap(store.value);
      typeof v === "function" && (v = v(unwrapped));
      setStore("value", reconcile(v));
      return store.value;
    }
  ] as Signal<T>;
}
Enter fullscreen mode Exit fullscreen mode

In this example, using a store alongside the reconcile function for setting data is done for the purpose of having the fetched data be reactive only on the properties that have changed value instead of the whole data object triggering reactivity.

Abusing the storage option

But what if we take this example further, and instead of creating a new store for each resource, we create a custom storage function that gets and sets data to a global store?

Let's assume we have a solid store we can access through a hook like:

const [state, setState] = useGlobal();
Enter fullscreen mode Exit fullscreen mode

And that the store's state is:

{
  songs: [
    {name: 'Never Gonna Give You Up', rating: 8},
    {name: 'Windows Erros Remix [10 Hours]', rating: 10}
  ]
}
Enter fullscreen mode Exit fullscreen mode

What we would want is that in our component, we use a resource that fetches the songs data and manages local state for loading.

But that the resource is backed by the global state, so that if there are any changes to the songs in the global state, the changes would be reflected in the resource, and the other way around as well. Two-way binding: store <-> resource.

const storeBackedSongs = () => {
  const [state, setState] = useGlobal();

  const getter = () => state.songs;

  const setter = (value) => setState('songs', songs => {
    // setters can receive either a value, or a function
    // that returns a value for that reason we need to handle both cases
    if(typeof value === "function") return value(songs);
    return songs;
  });

    return [getter, setter];
}

// Using it in a component
const Songs = () => {
  const [songs] = createResource(fetchSongs, {storage: storeBackedSongs});

  return <Suspense fallback={<Loading />}>
    <SongsList songs={songs()} />
  </Suspense>
}
Enter fullscreen mode Exit fullscreen mode

Now if somewhere else in the app someone changes the songs through the store:
setState("songs", []);

If the resource and component are still mounted, the SongsList component will update with the empty array value.

Similarly, if you change the value of the songs resource, either by refetching or with a mutate, that will update the store state and reflect the changes in all places where store.songs is used.

const [songs, {mutate, refetch}] = createResource(fetchSongs, {storage: storeBackedSongs});

// Both of these calls will update not only the resource's data, but store.songs as well
mutate([]);
refetch();
Enter fullscreen mode Exit fullscreen mode

SSR

For people using solid-start, this pattern works just as well for createRouteData and the other route data functions, since they all wrap around resources and expose the storage option.

Extra info

The example above is simplified so it's easier to understand the main points.

The code does work, but it's missing types. If you were to copy the implementation and you're using typescript you will run into some weird type issues stemming from the complex generic type that storage accepts.

I have published a full working example here: link to repo.
It has proper types and also a generic createResourceStorage function that you can copy and reuse to simplify your code.

Happy coding!

Top comments (0)