question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add ability to destroy stores

See original GitHub issue

What problem is this solving

Sometimes we need to define stores dynamically - e.g. to be able to use 2 instances of the same store that don’t share the same state.

In such a case, in Vuex, I create a factory function that takes dependencies and return an initialized instance of a Vuex store. Then, when I no longer need that store I can call unregisterModule() and destroy that store.

In Pinia I didn’t find such an option.

Proposed solution

<script>
import createMyStore from '@/store/createMyStore';
import { onUnmounted } from '@vue/composition-api';

export default {
  name: 'App',

  setup() {
    const useStore = createMyStore({
      id: 'myId',
      useSettings: () => ({
        value: 1,
      })
    });
    const { combinedValue, $destroy } = useStore();

    onUnmounted(() => {
      $destroy();
    })

    return {
      combinedValue,
    }
  },
}
</script>
import { defineStore } from 'pinia'

export default ({ useSettings, id }) => {
  const mainId = 'myStore'

  return defineStore({
    id: `${mainId}_${id}`,

    state: () => ({
      value: 1,
    }),

    getters: {
      combinedValue() {
        const settings = useSettings();
        return this.value + settings.value;
      },
    },
  })
}

Describe alternatives you’ve considered

I don’t think there’s any alternative. We can call $reset() to restore the state - this will free-up the memory, but it won’t destroy the instance entirely.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:12 (6 by maintainers)

github_iconTop GitHub Comments

9reactions
wujekbogdancommented, Jun 29, 2021

Example

Let’s assume that:

  • The user store has a dependency on the settings store
  • The settings store may have multiple implementations. They share the same interface but fetch data from various sources
  • The settings store ID needs to be determined dynamically.
  • We need separate instances of the same store that don’t share the state - each instance has it’s own copy of the store.

Initial approach (no dynamic stores).

_Source: https://github.com/wujekbogdan/pinia-dynamic-stores/tree/feature/no_dynamic_stores_

In the given example the user store depends on a settings store that may have 2 implementations: localSettings and remoteSettings. They both share the same interface but fetch the data differently.

This example is oversimplified. In reality there might be much more going on in each implementation. That’s why they are separate stores.

In this example it’s the user’s store responsibility to initialise a proper settings store based on the init action parameter, but things may get more complex if there are more dependencies.

// Home.vue
<template>
  <div>
    <code>
      {{ userStore.userProfile }}
    </code>
    <p>
      <select name="settingsSource" id="settingsSource" v-model="settingsSource">
        <option value="local">
          use local settings
        </option>
        <option value="remote">
          use remote settings
        </option>
      </select>
    </p>
  </div>
</template>

<script>
import { ref, watch } from '@vue/composition-api';
import useUserStore from '@/store/user';

export default {
  name: 'App',

  setup() {
    const userStore = useUserStore();
    const settingsSource = ref('local');

    watch(settingsSource, (source) => userStore.init(source));

    userStore.init(settingsSource.value);

    return { userStore, settingsSource };
  },
};
</script>
// user store
import { defineStore } from 'pinia';
import useLocalSettings from './localSettings';
import useRemoteSettings from './remoteSettings';

/**
 * @param {'local'|'remote'} source
 */
const useSettings = (source) => {
  const sources = {
    remote: useRemoteSettings,
    local: useLocalSettings,
  };

  return sources[source]();
};

export default defineStore({
  id: 'user',

  state: () => ({
    source: null,
    userData: {
      firsName: 'lorem',
      lastName: 'ipsum',
    },
  }),

  getters: {
    userProfile(state) {
      if (!this.source) {
        return null;
      }

      const userSettings = useSettings(this.source);

      return {
        ...state.userData,
        ...userSettings.settings,
      };
    },
  },

  /**
   * @param {'local'|'remote'} source
   * @return {Promise<void>}
   */
  actions: {
    init(source) {
      this.source = source;
      const { getSettings } = useSettings(source);
      return getSettings();
    },
  },
});
// local settings provider
import { defineStore } from 'pinia';

export default defineStore({
  id: 'localSettings',

  state: () => ({
    userSettings: null,
  }),

  actions: {
    async getSettings() {
      try {
        this.userSettings = JSON.parse(localStorage.getItem('settings'));
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('Unable to read user settings from localStorage', e);
        this.userSettings = null;
      }
    },
  },

  getters: {
    settings(state) {
      return state.userSettings;
    },
  },
});
// remote settings provider
import { defineStore } from 'pinia';

const URL = 'https://jsonplaceholder.typicode.com/todos/1';

export default defineStore({
  id: 'remoteSettings',

  state: () => ({
    userSettings: null,
  }),

  actions: {
    async getSettings() {
      try {
        const response = await fetch(URL);
        this.userSettings = await response.json();
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('Unable to fetch user settings', e);
        this.userSettings = null;
      }
    },
  },

  getters: {
    settings() {
      return this.userSettings;
    },
  },
});

Dependency injection-based approach (dynamic stores)

_Source: https://github.com/wujekbogdan/pinia-dynamic-stores/tree/feature/dynamic_stores_

Let’s imagine that we need several components on the page that use the user store but each component requires its own state. The solution is to initialize multiple instances of the user store and inject the settings store as a dependency using a factory pattern.

This pattern not only allows for having multiple instances of the same store, but also simplifies the logic. The user store no longer needs to worry about resolving dependencies. In case of just one dependency it doesn’t look like a big advantage, but in case of multiple dependencies the code can get really messy.

The factory pattern allows for moving that logic away from a store and let the wrapping module handle dependency resolution/injection. IRL it doesn’t need to be a Vue component - it could be a dedicated module that performs the initialization.

// Home.vue

<template>
  <div>
    <User source="local" />
    <User source="remote" />
  </div>
</template>

<script>
import User from './User.vue';

export default {
  name: 'App',

  components: {
    User,
  },
};
</script>
// User.vue
<template>
  <div>
    <p>
      source: {{ src }}
    </p>
    <code>
      {{ userStore.userProfile }}
    </code>
  </div>
</template>

<script>
import { onUnmounted } from '@vue/composition-api';
import UseUserStoreFactory from '@/store/user';
import useLocalSettings from '@/store/localSettings';
import useRemoteSettings from '@/store/remoteSettings';

/**
 * @param {'local'|'remote'} source
 */
const UseSettingsFactory = (source) => {
  const sources = {
    remote: useRemoteSettings,
    local: useLocalSettings,
  };

  return sources[source];
};

export default {
  name: 'App',

  props: {
    source: {
      type: String,
      required: true,
      validator(source) {
        return ['remote', 'local'].includes(source);
      },
    },
  },

  setup(props) {
    const useSettings = UseSettingsFactory(props.source);
    const useUserStore = UseUserStoreFactory({
      useSettings,
      // In reality this may be really dynamic e.g. `myId_${uuid()}`
      id: `myId_${props.source}`,
    });

    const userStore = useUserStore();

    userStore.init();

    onUnmounted(() => {
      // I would like to be able to call userStore.$destroy() to perform a full cleanup of
      // dynamically registered `userStore` store.
      // e.g. useUserStore.$destroy();
    });

    return { userStore, src: props.source };
  },
};
</script>
// user store
import { defineStore } from 'pinia';

/**
 * @param {Function} useSettings
 * @param {String} id
 */
// In reality this store may receive multiple dependencies
export default ({ useSettings, id }) => defineStore({
  id: `user_${id}`,

  state: () => ({
    userData: {
      firsName: 'lorem',
      lastName: 'ipsum',
    },
  }),

  getters: {
    userProfile(state) {
      const userSettings = useSettings();

      return {
        ...state.userData,
        ...userSettings.settings,
      };
    },
  },

  /**
     * @param {'local'|'remote'} source
     * @return {Promise<void>}
     */
  actions: {
    init() {
      const { getSettings } = useSettings();
      return getSettings();
    },
  },
});
// localSettings and remoteSettings stores are identical as in the previous example. 

Another use case for dynamically created stores are non-SPA applications. I’ve been working on a project with a “classic” PHP backend. The majority of of the views were rendered on the backend, but all dynamic components were rendered with Vue. Each dynamic widget was a separate Vue application. Each widget was responsible for registering Vuex stores on its own and destroying these stores when they were no longer needed. On one page there could be multiple widgets that use the same store, but don’t share the state - they were using separate instances of the same store.

7reactions
posvacommented, Jun 28, 2021

Let’s say that I called the createMyStore function 100 times (each time with a different parameter) - I will have 100 instances of that store listed in dev tools even if components that use these store no longer exist.

Usually, you create one store and add 100 items to it instead.

Even in Vuex, the store was still there. We could still expose a tree shakeable function that clears up the traces of the store though

Read more comments on GitHub >

github_iconTop Results From Across the Web

Add ability to destroy stores · Issue #557 · vuejs/pinia - GitHub
Sometimes we need to define stores dynamically - e.g. to be able to use 2 instances of the same store that don't share...
Read more >
c# - How i can destroy an GameObject but also store the ...
What you can do is hold a reference to that object and then store it somewhere and make it invisible. Its called object...
Read more >
Innovation Killers: How Financial Tools Destroy Your Capacity ...
When a company is looking at adding capacity that is identical to existing capacity, it makes sense to compare the mar-ginal cost of...
Read more >
Kanai's Cube - Game Guide - Diablo III - Blizzard Entertainment
With Kanai's Cube, you can destroy a Legendary item and store its power. Most Legendary powers can be extracted—these are highlighted in orange...
Read more >
Destroy It!
Toggle the ability to destroy any ads or annoying elements of a webpage.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found