💎 of solid-primitives, part 2: storage
Published: 2024-03-26 14:31:11

[Solid.js](https://solidjs.com) 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](https://primitives.solidjs.community/).

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](https://primitives.solidjs.community/package/storage) 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`](https://www.solidjs.com/docs/latest/api#createsignal) or [`createStore`](https://www.solidjs.com/docs/latest/api#createstore) and returns something similar, augmented in order to persist changes:

```ts
const [state, setState] = makePersisted(createSignal(0));
```

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

```ts
{
  /** 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;
};
```

#### (De-)serialization

The [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://github.com/lxsmnsyc/seroval), which is also used in [solid-start](https://start.solidjs.com/).


#### Storage selection

In most cases, [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/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`](https://developer.mozilla.org/en-US/docs/Web/API/Window/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`](https://localforage.github.io/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`:

```ts
makePersisted(createSignal(0), { sync: storageSync });
```

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](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) or the [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API):

```ts
makePersisted(createSignal(0), {
  storage: localForage,
  sync: messageSync(broadcastChannel)
});
```

If even that is not enough and you want to sync over the client/server-boundary, you can use a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) connection and wsSync:

```ts
makePersisted(createSignal(0), { sync: wsSync(ws) });
```

To create the web socket, we also provide a [`websocket` primitive](https://primitives.solidjs.community/package/websocket#makews).

Maybe you want to synchronize over your own channel, e.g. [Pusher](https://pusher.com/)? Writing your own sync API is simple:

```ts
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,
];
```

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

```ts
makePersisted(createSignal(0), {
  sync: multiplexSync(storageSync, wsSync(ws))
});
```

> 🛈 We have primitives for the [Web Socket API](https://primitives.solidjs.community/package/websocket) and the [Broadcast Channel API](https://primitives.solidjs.community/package/broadcast-channel), 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](https://discord.com/invite/solidjs).

Be sure to also check the [first installment of this series](https://dev.to/lexlohr/of-solid-primitives-part-1-context-2bah) in case you missed it.