DEV Community

Alex Lohr
Alex Lohr

Posted on • Edited on

💎 of solid-primitives, part 2: storage

Solid.js is already pretty powerful, but even so, there are things it cannot do out of the box. Here's where the community comes in and provides packages to enhance your development experience: solid-primitives.

As the author of a few of those packages, I want to delve into our collection to present you a few gems that might end up being helpful to you. Here's the second one

@solid-primitives/storage

It is a very common requirement to persist your state in some sort of storage. You can do so manually, or you use our helpful storage primitive package that provides a few helpers to make this really simple.

makePersisted

This is the core of the whole package. It takes the output from createSignal or createStore and returns something similar, augmented in order to persist changes:

const [state, setState] = makePersisted(createSignal(0));
Enter fullscreen mode Exit fullscreen mode

It supports an optional options object to set up your storage exactly the way you need it:

{
  /** the item name under which the state is persisted */
  name?: string;
  /** a serialization function, default is JSON.stringify */
  serialize?: (data: T) => string;
  /** a deserialization function, default is JSON.parse */
  deserialize?: (data: string) => T;
  /** storage to persist state in, default is localStorage */
  storage?: 
    | Storage
    | AsyncStorage
    | StorageWithOptions<O>
    | AsyncStorageWithOptions<O>;
  /* if the storage accepts options, set them up here: */
  storageOptions?: O;
  /** sync storage (see below) */
  sync?: PersistenceSyncAPI;
};
Enter fullscreen mode Exit fullscreen mode

(De-)serialization

The Web Storage API only support strings. In order to be able to persist other types, we automatically serialize the data before writing and deserialize it after reading.

Using the serialize/deserialize options, you can influence this behavior; turn it off by using a function x => x for both or use a more sophisticated (de-)serialization engine like seroval, which is also used in solid-start.

Storage selection

In most cases, localStorage is exactly the right choice, since it persists data over sessions while not sending anything to the server. But maybe you want to restrict the persistence to your current session and instead choose sessionStorage.

You don't need to restrict yourself to storage APIs that already come with your browser. You can also use external packages like localForage or another export of this package, cookieStorage.

cookieStorage behaves exactly like a normal synchronous Web Storage API, but accepts an options object for the cookie settings and also has a .withOptions method, so you can bind options and use it like a normal Storage.

It works both in the client and in the server. For solid-start, no extra configuration is needed. For other SSR implementations, you need to provide a getRequestHeaders and getResponseHeaders in the options for it to work correctly, otherwise it will always read null and never set cookies.

Lastly, in case you want to use multiple storage APIs at the same time, you can use multiplexStorage, which takes an arbitrary number of storages as arguments and returns a single storage; .setItem(key, value) and .removeItem(key) will apply to all storages, but .readItem(key) will take the first valid return value; the returned storage will be asynchronous except if the storages are all synchronous, in which case it will be synchronous, too.

Storage Sync

The Web Storage API features a storage event that triggers if the storage for the same page is changed in another frame or tab so you can update your state from it.

In order to use this for localStorage or sessionStorage, you can use storageSync:

makePersisted(createSignal(0), { sync: storageSync });
Enter fullscreen mode Exit fullscreen mode

If you use one of the other storages, but want to replicate the same behavior, you can use messageSync to transmit update events over the Post Message API or the Broadcast Channel API:

makePersisted(createSignal(0), {
  storage: localForage,
  sync: messageSync(broadcastChannel)
});
Enter fullscreen mode Exit fullscreen mode

If even that is not enough and you want to sync over the client/server-boundary, you can use a WebSocket connection and wsSync:

makePersisted(createSignal(0), { sync: wsSync(ws) });
Enter fullscreen mode Exit fullscreen mode

To create the web socket, we also provide a websocket primitive.

Maybe you want to synchronize over your own channel, e.g. Pusher? Writing your own sync API is simple:

export type PersistenceSyncData = {
  key: string;
  newValue: string | null | undefined;
  timeStamp: number;
  url?: string;
};

export type PersistenceSyncCallback = (data: PersistenceSyncData) => void;

export type PersistenceSyncAPI = [
  /** subscribes to sync */
  subscribe: (subscriber: PersistenceSyncCallback) => void,
  update: (key: string, value: string | null | undefined) => void,
];
Enter fullscreen mode Exit fullscreen mode

Or you want to synchronize your state using multiple channels, then you can use multiplexSync:

makePersisted(createSignal(0), {
  sync: multiplexSync(storageSync, wsSync(ws))
});
Enter fullscreen mode Exit fullscreen mode

🛈 We have primitives for the Web Socket API and the Broadcast Channel API, so you don't have to set them up yourself. They will be presented in a later installment of this series.

Bonus features (helpers)

Maybe you are just writing your own storage, but have a hard time providing a .clear() method? Just use addClear(customStorage).

Or you have a storage that accepts options and want a .withOptions(options) method like our cookieStorage, too? We have addWithOptionsMethod(customStorageWithOptions) for that use case.

Final Words

We always try to provide the most utility to you, our user. If you have ideas how we could do that even better, feel free to tell us on our #solid-primitives channel in the Solid.js Discord.

Be sure to also check the first installment of this series in case you missed it.

Top comments (2)

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

The sync/persistence especially in combination with websockets is pretty cool.

I'm missing the missing websocket setup code or the full standalone source repo, especially since websockets are stateful:

  • the connection has to have been established before,
  • how to delay sending if the websocket is temporarily closed or in a connecting state
  • how to handle reconnects
Collapse
 
lexlohr profile image
Alex Lohr • Edited

We have another primitive handling web sockets, including heart beat with reconnect.