npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

shared-ref

v2.0.3

Published

SharedRef allows you to create synchronized refs across tabs, suitable for Vue framework.

Downloads

108

Readme

SharedRef

SharedRef allows you to create synchronized refs across tabs, suitable for Vue framework.

This can easily allow you to achieve: the function of synchronizing login in other tabs when one tab logs in. Alternatively, when a user changes certain configurations, other tabs will also automatically take effect.

You can even use your imagination to easily synchronize data between iframes. Moreover, if you are using Electron or doing some hybrid development, you can also synchronize data between multiple webviews.

Under the hood, we create a SharedWorker to maintain the state of refs, synchronizing changes in values across multiple tabs.

If the user's browser does not support SharedWorker, it will automatically fall back to using Worker to achieve the same.

Additionally, you can customize how SharedRef stores the values of refs. You can store the ref variables in IndexedDB, ensuring that the values are not lost even if the browser is closed.

Furthermore, you can go a step further and store the values of refs on the server to achieve cloud-based automatic synchronization. If you wish to do this, we recommend using Milkio to build your server application, as it allows for easy bidirectional communication without the need for WebSockets.

Example

<script lang="ts" setup>
import { sharedRef } from 'shared-ref';

const counter = await sharedRef({
    key: "counter",
    value: 0
});

const onClick = () => counter.value++;
</script>

<template>
    <div>{{ counter }}</div>
    <button type="button" @click="onClick">click</button>
</template>

Installation

npm i shared-ref

Initialization

First, let's understand what a SharedWorker is.

A SharedWorker is an independent JavaScript process running outside the tab. We cannot control DOM elements of the page within a SharedWorker, and it does not have objects like window, document, etc. It is a JavaScript process running independently of the page process. However, it can communicate with any same-origin page.

Creating a SharedWorker is like specifying a JavaScript file to run as a SharedWorker.

Let's create a worker.ts file and initialize the Worker part of SharedRef in it.

import { defineSharedRefWorker } from "shared-ref";

const sharedRefWorker = defineSharedRefWorker({});

Load this Worker when the page initializes. Place this in your index.ts or main.ts at the top, before your Vue initialization.

import { initSharedRef } from "shared-ref";

initSharedRef({
  worker: ({ SharedWorker }) => (new SharedWorker(new URL("./worker.ts", import.meta.url), {
    type: "module",
  }))
});

// Next comes your Vue initialization code
const app = createApp(App);

Usage

The usage of SharedRef differs slightly from a regular Ref.

import { sharedRef } from 'shared-ref';

const counter = await sharedRef({
    key: "counter",
    value: 0
});

The parameter for sharedRef is an object, where value is the initial value of the Ref you want to create. key is a unique identifier for the Ref and needs to be unique throughout the page. We use key to maintain synchronization of values between different tabs. When Refs with the same key are created in different tabs, they will automatically synchronize.

It's worth noting that the return value of the sharedRef method is a Promise, so you need to add the await keyword to wait for it to load (which is very fast).

Why is a key necessary? Because Symbols cannot be passed across, and perhaps, you might need to maintain data synchronization between different pages for the same data (for example, a sharedRef for a user avatar on both the personal information page and the homepage, which are not the same instance of sharedRef but need to remain consistent).

Suspense

Since sharedRef is asynchronously loaded, we need to wrap it with the Suspense component to make Vue support asynchronous components.

If you need to use SharedRef in your root component, you can move all your root component code to a new component and then in your root component, include only a Suspense and reference your root component like this:

<template>
    <Suspense>
        <YourRootComponent />
    </Suspense>
</template>

Shallow Reactivity

Regular Refs are deeply reactive because they automatically convert values to reactive, but SharedRef does not.

This means that if you store an object in SharedRef and then modify properties of that object, it will not trigger an update in SharedRef.

This behavior is intentional because every time the value changes, it is copied to multiple tabs. Storing a large object or array in SharedRef would lead to performance issues as these large objects or arrays would be copied to multiple tabs even if only a small part of them is modified.

If you want to create an object but also want its values to be reactive when updated, you can change your approach like this:

// Intuitive way of writing, will not trigger reactivity unless counter.value is directly modified
const counter = await sharedRef({
    key: "counter",
    value: {
        count1: 0,
        count2: 0
    }
})

// New way of writing, can trigger reactivity
const counter = {
    count1: await sharedRef({ key: "counter:count1", value: 0 }),
    count2: await sharedRef({ key: "counter:count2", value: 0 })
}

Persistent Storage

When all tabs are closed, data will be lost as the SharedWorker is destroyed. When the page is reopened, the data will revert to its initial state. To prevent data loss, we can save data to IndexedDB.

import { defineSharedRefWorker, IndexedDBHandler } from 'shared-ref';

const sharedRefWorker = defineSharedRefWorker({
  ...IndexedDBHandler(),
});

Custom Storage Logic

You can customize how SharedRef stores values. Edit your worker.ts:

const sharedRefWorker = defineSharedRefWorker({
  async bootstrap() {
    // ...
  },
  async getHandler(event) {
    // ...
  },
  async setHandler(event) {
    // ...
  },
});

The bootstrap method is called at startup, where you can write your initialization logic. SharedRef will wait for this method to complete before starting its work.

The getHandler and setHandler methods are called when the value of SharedRef is retrieved or set. For the getHandler method, you need to return an object like this:

async getHandler(event) {
    return { empty: true, value: undefined };
    // or
    return { empty: false, value: yourValue };
},

When empty is true, it means the value does not exist, and SharedRef will use value as the default value. When empty is false, it means the value exists. Even if value is undefined, SharedRef will faithfully use undefined as the value.

Meta

You can add a meta attribute to SharedRef, which can be accessed in getHandler and setHandler. You can use this to control the behavior of SharedRef.

For example, you can set that only Refs with meta.persistence set to true will be saved to IndexedDB.

This way, you can choose to save only certain Refs to IndexedDB, while others are deleted when all tabs are closed.

const counter = await sharedRef({
    key: "counter",
    meta: {
        persistence: true
    },
    value: 0
})
async setHandler(event) {
    if (event.meta.persistence === true) {
        // Save data to indexedDB...
    } else {
        // Do nothing...
    }
},

Extending Persistent Storage

Customizing storage logic does not mean you cannot use the built-in IndexedDB storage feature. You can choose to enable it at the right time.

import { defineSharedRefWorker, IndexedDBHandler } from 'shared-ref';

const indexeddb = IndexedDBHandler();

const sharedRefWorker = defineSharedRefWorker({
  async bootstrap() {
    await indexeddb.bootstrap();
    // ...
  },
  async getHandler(event) {
    if (...) {
      await indexeddb.getHandler(event);
    }
  },
  async setHandler(event) {
    if (...) {
      await indexeddb.setHandler(event);
    }
  },
});

Broadcasting in SharedWorker

When you are in a Worker and need to notify that certain values have changed, you can use the broadcast method. This will automatically update the values in all SharedRefs using that key.

For example, imagine you are developing a to-do app that syncs data across multiple devices. When you complete a to-do item on your phone, the completed item is sent to the server. Upon receiving it, the server then sends the completed to-do item to your computer. In your SharedWorker, upon receiving the data from the server, you want to propagate it to all SharedRefs.

In this scenario, you can use the broadcast method to ensure that all open tabs in the browser reflect the completion of that specific to-do item.

const sharedRefWorker = defineSharedRefWorker({
  // ...
});

sharedRefWorker.broadcast(key, value);

Vanilla Worker

SharedRef can also serve as a simple wrapper for your Worker/SharedWorker. The return value of the initSharedRef method is a populated SharedWorker Polyfill object that you can use just like a plain SharedWorker, even implemented with Worker in browsers that don't natively support it.

const sharedWorker = initSharedRef({
  worker: ({ SharedWorker }) => (new SharedWorker(new URL("./worker.ts", import.meta.url), {
    type: "module",
  }))
});

sharedWorker.postMessage("hello world")

Within a SharedWorker, you can receive messages. Don't worry, messages within sharedRef have been filtered, and you will only receive those that sharedRef does not recognize.

const sharedRefWorker = defineSharedRefWorker({
  // ...
});

sharedRefWorker.on("message", (event) => { 
  console.log(event.data); // echo: hello world
});

If you want to do something while connecting to any page:

sharedRefWorker.on("connect", (event) => { 
  event.port.postMessage("connected!");
});

Of course, you can always access all the ports:

sharedRefWorker.ports; // Set<MessagePort>

SSR/SSG

You can use it in SSR/SSG, and its initial value will always be your value during server-side rendering. When it comes to the client payload, its value will be read from the SharedWorker.