I'm porting a project from AngularJs to Vue3. In Angular we have a service component, which creates und supplies objects to components. The objects are automatically reactive, if they are used in a binding in a component. If component A changes an object, the View of component B shows the change of the state immediately.
Is there a way to accomplish such a service in Vue3? The goal is, to maintain an array of objects, and every change to the array is reflected in the Views of the different components.
CodePudding user response:
Differences: Vue VS AngularJS
Vue is a UI framework that doesn't specifically have such entities as services, it is up to a developer to implement them. Also in contrast to Angular, Vue doesn't provide dependency-injection (DI) features.
Composables as service in Vue
In Vue, reusable component code is supposed to be provided by composables or composable functions, their reactivity is implemented with composition API:
// Usage: service = useLocalService()
const useLocalService = () => {
const someState = reactive({});
const someAction = () => /* change state */;
return { someState, someAction }
};
provide / inject to share services in Vue (compare DI)
For global singleton services that are inherent in Angular, service state is defined in a module and evaluated once, so it's shared across the app:
// Usage: import * as service from '...';
export const someGlobalState = reactive({});
export const someGlobalAction = () => /* change state */;
Whether it's beneficial to wrap it in a composable function for consistency depends on a situation:
// Usage: service = useGlobalService()
const someGlobalState = reactive({});
const someGlobalAction = () => /* change state */;
const useGlobalService = () => {
return { someGlobalState, someGlobalAction }
};
In Angular 2 or later, DI containers allow to provide service instances per component hierarchy. In Vue, this is done through provide
/inject
:
// Usage in a parent: setupRootOrNestedService()
const setupRootOrNestedService = () => {
const someRootOrNestedState = reactive({});
const someRootOrNestedAction = () => /* change state */;
provide('rootOrNestedService', { someRootOrNestedState, someRootOrNestedState });
};
// Usage in a child: service = useRootOrNestedService()
const useRootOrNestedService = () => {
return inject('rootOrNestedService');
};
This allows to identify services by tokens at any level and benefit from using DI pattern.
Pinia
Pinia
(a Vue store) library provides lightweight state management influenced by Flux pattern that relies on composition API and allows for multiple stores. The result is similar to the above, with the addition of Vue devtools support and proper TypeScript typing:
// Usage as local service: service = useLocalOrGlobalService()
// Usage as global service: export default useLocalOrGlobalService()
// And: import * as service from '...';
const useLocalOrGlobalService = () => {
return defineStore('localOrGlobalService' random(), {
state: () => ({}),
actions: {
someAction() { /* change state */ }
}
})
};
Pinia doesn't restrict ways in which a store is instantiated and provided to components, so it can be combined with provide
/inject
if necessary.
CodePudding user response:
Just for the records: This is, what I came up with, as simple sample code. First I wrote a little service:
import { reactive } from "vue";
export const objectService = {
objects: reactive([]), // reactive is important, otherwise it doesn't work
start: function(){
for (var i = 0; i < 10; i )
((j)=>{
setTimeout(()=> // I use setTimeout as example for an async operation
objectService.objects.push({value: j}), 1000);
})(i);
}
};
The point here is, that the service manipulates the state in an async operation (in real live systems a rest call or access to indexedDb, etc).
Now there are two components running in two separate Vue apps. The reason, why they run in separate apps is out of the scope of the issue here. App1:
<template>
<!--App1.vue-->
<p>Here is App1</p>
<div v-for="obj,ix in objectService.objects" :key="ix">{{obj.value}}
<input v-model="obj.value" />
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { objectService } from '../services/object-service.js';
const App1 = defineComponent({
setup() {
return { objectService };
},
})
export default App1;
</script>
App2:
<template>
<!--App2.vue-->
<p>Here is App2</p>
<div v-for="obj,ix in objectService.objects" :key="ix">{{obj.value}}
<input v-model="obj.value" />
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { objectService } from '../services/object-service.js';
const App2 = defineComponent({
setup() {
// const os = objectService;
objectService.start();
return { objectService };
},
})
export default App2;
</script>
These 2 components are identical with one exception: App2 calls the start method of the service. The result is shown in both components. And each of the components can alter the state, which is immediately reflected in the other component.
The point of the whole solution is the use of reactive({}) in the service. I hope, this is useful for somebody.
Again thanks to Estus Flask & hc_dev to point me into the right direction.