Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide/Inject outside setup #6220

Closed
HipyCas opened this issue Jul 3, 2022 · 12 comments
Closed

Provide/Inject outside setup #6220

HipyCas opened this issue Jul 3, 2022 · 12 comments
Labels
✨ feature request New feature or request

Comments

@HipyCas
Copy link

HipyCas commented Jul 3, 2022

What problem does this feature solve?

As of now, the provide and inject methods only work in the context of the component setup, but they could also be really useful outside that context.

For example a case that I'm facing. I have an app where I want to check if the user wants dark mode when the app is opened, either via a setting that you can change in the app or by media queries in case the setting is not configured. So the part that loads and sets the dark mode preference in App.vue looks something like this:

if(darkModeManuallySet()) {  // Check if the user manually set dark mode
    const setting: boolean = getDarkModeSetting();  // Get the saved setting value
    toggleDarkMode(setting);  // Toggle dark mode accordingly 
    provide('dark_mode_set', setting);  // Provide this setting so other pages can set some stuff taking this into account
} else {
    const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");  // Get preference from media query
    toggleDarkMode(prefersDark.matches);
    provide('dark_mode_set', setting);
}

Then in my settings page, I could have a switch that takes as initial value whether the dark mode is loaded or not:

<template>
    <!-- More page elements -->
    <input type="checkbox" id="darkCheck" v-model="darkMode" >
    <!-- ... -->
</template>

<script setup>
const darkMode = ref(inject('dark_mode_set') as boolean);
// More code...
</script>

What does the proposed API look like?

There isn't any change in API needed in theory, just enabling the API to work outside the setup hook.

@HipyCas HipyCas added the ✨ feature request New feature or request label Jul 3, 2022
@ShGKme
Copy link

ShGKme commented Jul 3, 2022

"Global provide/inject API" could not exists by design.

Provide/Inject works in component's subtree in Vue App. Value is provided in one instance in one place of tree and injected in another instance in any place of subtree.

In simple words, injected value depends on all upper tree and may be various in different places of the component's tree or even in different Vue apps on the page.

const app1 = createApp(App).provide('key', 'foo');
const app2 = createApp(App).provide('key', 'bar');

globalInject('key'); // ? foo or bar ?

If you need "global provide/inject" you might either not need provide/inject at all or need to provide some global value instead.

So if you need getDarkModeSetting and toggleDarkMode outside Vue component instances then you should create/store them outside components and composables.

There may be dozens ways to do it in JavaScript from simple ES Modules with global reactive variables or global store to IoC container.

For example, a simple solution with global variable without provide/inject:

// 📁 darkMode.js
import { ref, readolny } from 'vue';

export const isDarkMode = ref(false);

export function getDarkModeSetting() {
  return isDarkMode.value;
}

export function toggleDarkModeSettings(newMode) {
  isDarkMode.value = newMode;
}
// 📁 Literally any place of app, inc. setup
import { darkMode } from './path/to/darkMode.js';

const { getDarkMode, toggleDarkMode } = darkMode;

Or you may use approach similar to Vue Router or Vuex plugins. First create thing globally, then provide it.

Create scope with dark mode functionality:

// 📁 createDarkMode.js
import { ref, readolny } from 'vue';

export function createDarkMode(initial) {
  const isDarkMode = ref(initial);

  function getDarkModeSetting() {
    return isDarkMode.value;
  }

  function toggleDarkModeSettings(newMode) {
    isDarkMode.value = newMode;
  }

  return {
    isDarkMode: readonly(isDarkMode),
    getDarkModeSetting,
    toggleDarkModeSettings,
  };
}

Create global dark mode:

// 📁 darkMode.js
import { createDarkMode } from './path/to/createDarkMode.js';

export const darkMode = createDarkMode(false);

Provide global dark mode to Vue app:

// 📁 main.js
import { darkMode } from './path/to/darkMode.js';

const app = createApp(App).provide('DARK_MODE', darkMode);

Inject as usual in components:

// 📁 SomeComponent.vue

const { isDarkMode, toggleDarkMode } = inject('DARK_MODE');

...or use in any other ES module in the app without provide/inject:

// 📁 any place
import { darkMode } from './path/to/darkMode.js';

const { getDarkMode, toggleDarkMode } = darkMode;

@yyx990803
Copy link
Member

I don't understand the request here. Your use case doesn't seem to require provide outside a component context.

FYI there's app-level provide: https://vuejs.org/api/application.html#app-provide

@HipyCas
Copy link
Author

HipyCas commented Jul 5, 2022

I think I may have not explained myself properly.

I am using the provide/inject inside a component, but in the onMounted hook, outside the setup scope. In the code that I need this, I check whether the users prefers dark mode with a media query and a class in the body tag, which require to be check when the component is already mounted (which of course comes after setup in another lifecycle).

@Sanicboi
Copy link

Sanicboi commented Jul 5, 2022

You can use Pinia state management to achieve global provide/inject. It is just easier.More details here

@LinusBorg
Copy link
Member

LinusBorg commented Jul 5, 2022

I am using the provide/inject inside a component, but in the onMounted hook, outside the setup scope. In the code that I need this, I check whether the users prefers dark mode with a media query and a class in the body tag,

You can still do the injection in setup and then check the result in the hook. No need for any new API.

setup() {
  const prefersDarMode = inject('darkModeCheck')

  onMounted(() => {
    // assuming you provided a ref.
    if (prefersDarkMode.value === true) {
      // do something
    }
  }) 
}

@HipyCas
Copy link
Author

HipyCas commented Jul 5, 2022

My main problem is not with the inject but rather with the provide, as I need to provide the value based on media queries, which I can only access through the document in the onMounted hook.

@LinusBorg
Copy link
Member

LinusBorg commented Jul 5, 2022

  1. create and provide a ref in setup.
  2. set the value of the ref whenever you are ready to, i.e. in onMounted().
  3. inject and use the ref in children in a way that makes sense, depending on the use case. if its value is set delayed, maybe watch it. or access it in the child's onMounted() ...

@UnluckyNinja
Copy link

I think you mistook "provide" as a method to provide a default value rather than to create a store for later use.
As above replies said, you should provide a reactive variable (either a ref or a reactive), then you can change the actual value/object property later at anytime. I suggest that you can read official guide about "provide/inject" for various examples.

@Eben-Hafkamp
Copy link

I definitely mistook the wording "provide", "inject" to mean exactly that, set and read values. I was using pinia because of this misunderstanding.

@mrleblanc101
Copy link

I'm creating a Vue plugin.
It provide a component, the component has some sane default props value.
I want to let the use override the props value in the app.use(component, options).

If I do this:

const options = inject<PluginOptions>(PLUGIN_KEY);

const props = withDefaults(defineProps<Props>(), {
    expanded: false,
    duration: options?.duration || 300,
    hwAcceleration: options?.hwAcceleration || false,
});

I get this warning:

[@vue/compiler-sfc] defineProps() in <script setup> cannot reference locally declared variables because it will be hoisted > outside of the setup() function. If your component options require initialization in the module scope, use a separate normal > <script> to export the options instead.

But if I hoist it as mentioned like so:

<script lang="ts">
const options = inject<PluginOptions>(PLUGIN_KEY);
</script>

<script setup lang="ts">
const props = withDefaults(defineProps<Props>(), {
    expanded: false,
    duration: options?.duration || 300,
    hwAcceleration: options?.hwAcceleration || false,
});
</script>

I get this error instead:

[Vue warn]: inject() can only be used inside setup() or functional components.

It seems to me that being able to use inject outside the setup function would solve my problem, no ?

@UnluckyNinja
Copy link

@mrleblanc101
You should start a new issue as it's actually a different topic. This is a closed issue, and you will probably not get an answer.
Also, it seems to be a coding problem rather than an issue related to Vue. And you might misunderstand what the warning says. There are better places for that.
But still, here is an easy solution:
Don't put injected var in props' default, instead, leave props' default undefined, and determine wanted value in separate logic, like:

const options = inject<PluginOptions>(PLUGIN_KEY);

const props = withDefaults(defineProps<Props>(), {
    expanded: false,
    duration: undefined,
    hwAcceleration: undefined,
});

const duration = props.duration ?? options?.duration ?? 300
const hwAcceleration = props.hwAcceleration ?? options?.hwAcceleration ?? false
// (or wrap them in `computed` so they become reactive)

So props are no longer dependent on local variables.

Playground:
https://sfc.vuejs.org/#eNqdVNtu00AQ/ZXBL06k1A4tT8ZNVAECBII+gBCSJeTY42SjvWkvCVGUf2d3Yxs7bRHqi+W5nTkzc+xjdCdlsrMYZVGuK0WkAY3GykXBCZNCGTiCwmYGUokdqRFO0CjBIHY18es+6Y1gsg0kqTc8ZlzwgleCawNMr+HWA03iD0ipgB9C0fpFPPUpLfQkvv/8/f3HL78+vfsZz+BYcIA0hW/qAJVgDLkBsyEahDU+VFtVGiJ4Btfz+azgJ4eVp+cZHHtnGGSSlgadBZDXZBdeAF4GO+0cuee7yAPty9TrR1Ih61rfFtGr+byInii+ebR4s7+rKqTYQxhlcYiRpz3xaBadF3zFSplsteDuTmExRRvQRZSdV+V9buneLqKNMVJnaaqbyl9iqxOh1ql7S5TlhjBMULOrlRJ7jcoBF5GHOLktupbd/S41AbTka0/YpQ/1QfgWK3OpDHOQCPdKSO0u3zLE37LkNdYZrISgWHJ3N+//e0tu2QpV6x6valjkmLp52x7Urgn/Kn3OoNdzMDu5ih7sPFs+6rEYKXXqZu3qnJLDuHtiNm+xKS01elJjQziGTeThuZhMW3kPN9KUVOODfVgX9OX1E/SH8dOQSgfh2ARWSe9YLrv5liPnjZNyWzzu0kNcuIdAD0Nhnn9/krRcIV10HPL0bIcQ4dIa98Moa8HpAbJdSS067XXJQYE9xLj7fwGNSwLc8MM7/QH38cHG

@mrleblanc101
Copy link

@UnluckyNinja Thanks for your time, I found this one while doing research and it felt pretty close to what I needed to solve my issue (a global inject), also opened a new issue because I might have found a bug related to the alternative someone suggested me (use inject inside the props default factory function) #7677

Your solution is probably what I will end up doing, but I wanted to keep the default value inside the withDefault because it make more sense. It's kinda weird to use withDefault but not providing a default value and having to use 2 other variables/computed to achieve the same thing.

@github-actions github-actions bot locked and limited conversation to collaborators Sep 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
✨ feature request New feature or request
Projects
None yet
Development

No branches or pull requests

8 participants