State is always undefined when using the #connectTo decorator in a config pipeline step in Aurelia Store - aurelia

I have an Aurelia app using Aurelia Store. I'm having some trouble when using the #connectTo decorator in an Aurelia pipeline step.
I have added the following step to my config pipeline:
config.addPipelineStep('authorize', AuthorizeStep);
And this step looks like:
#connectTo()
export class AuthorizeStep {
state: State;
run(navigationInstruction, next) {
if (navigationInstruction.getAllInstructions().find(x => x.config.isAdmin))
{
if (!this.state.user.isAdmin) {
return next.cancel();
}
}
return next();
}
}
However, my state is always undefined. Looking at other parts of my project, I can see the state and user are being populated, but it seems like in this AuthorizeStep it doesn't seem to work.
I think this issue may be due to the fact that my AuthorizeStep doesn't have a bind lifecycle method, but if so, what can I do about this?

The maintainers of Aurelia responded (only after I raised an issue on their GitHub) here.
Basically, as the bind lifecycle does not exist within this class, the #connectTo decorator won't work. Instead, I will need to manually inject the Store and subscribing to the state.

Related

Nuxt class-based services architecture (register globally; vs manual import)

In my Nuxt app I'm registering app services in a plugin file (e.g. /plugins/services.js) like this...
import FeatureOneService from '#/services/feature-one-service.js'
import FeatureTwoService from '#/services/feature-two-service.js'
import FeatureThreeService from '#/services/feature-three-service.js'
import FeatureFourService from '#/services/feature-four-service.js'
import FeatureFiveService from '#/services/feature-five-service.js'
export default (ctx, inject) => {
inject('feature1', new FeatureOneService(ctx))
inject('feature2', new FeatureTwoService(ctx))
inject('feature3', new FeatureThreeService(ctx))
inject('feature4', new FeatureFourService(ctx))
inject('feature5', new FeatureFiveService(ctx))
}
After doing this I can access any of my service on vue instance like this.$feature1.someMethod()
It works but I've once concern, that is, this approach loads all services globally. So whatever page the user visits all these services must be loaded.
Now I've 20+ such services in my app and this does not seem optimal approach to me.
The other approach I was wondering is to export a singleton instance within each service class and import this class instance in any component which needs that service.
So basically in my service class (e.g. feature-one-service.js) I would do like to do it like this..
export default new FeatureOneService() <---- I'm not sure how to pass nuxt instance in a .js file?
and import it my component where it is required like so...
import FeatureOneService from '#/services/feature-one-service.js'
What approach do you think is most feasible? if its the second one, then how to pass nuxt instance to my singleton class?
Yep, loading everything globally is not optimal in terms of performance.
You will need to either try to use JS files and pass down the Vue instance there.
Or use mixins, this is not optimal but it is pretty much the only solution in terms of reusability with Vue2.
Vue3 (composition API) brings composables, which is a far better approach regarding reusability (thing React hooks).
I've been struggling a lot with it and the only solution is probably to inject services to the global Vue instance at the component/page level during the initialisation (in created hook), another option is to do that in the middleware (or anywhere else where you have access to the nuxt context. Otherwise you won't be able to pass nuxt context to the service.
I usually set up services as classes, call them where necessary, and pass in the properties of the context which the class depends on as constructor arguments.
So for example, a basic MeiliSearchService class might look like:
export class MeilisearchService {
#client: MeiliSearch
constructor($config: NuxtRuntimeConfig) {
super()
this.#client = new MeiliSearch({
host: $config.services.meilisearch.host,
apiKey: $config.services.meilisearch.key
})
}
...
someMethod() {
let doSomething = this.#client.method()
...
}
...
}
Then wherever you need to use the service, just new up an instance (passing in whatever it needs) and make it available to the component.
data() {
const meiliSearchService = new MeiliSearchService(this.$config)
return {
meiliSearchService,
results,
...
}
},
methods: {
search(query) {
...
this.results = this.meiliSearchService.search(query)
...
}
}
As I'm sure you know, some context properties are only available in certain Nuxt life-cycle hooks. I find most of what I need is available everywhere, including $config, store, $route, $router etc.
Don't forget about Vue's reactivity when using this approach. For example, using a getter method on your service class will return the most recent value only when explicitly called. You can't, for example, stick the getter method in a computed() property and expect reactivty.
<div v-for='result in latestSearchResults'>
...
</div>
...
computed: {
latestSearchResults() {
return this.#client.getLatestResults()
}
}
Instead, call the method like:
methods: {
getLatestResults() {
return this.#client.getLatestResults()
}
}

Reevaluate Nuxt.js middleware without a route change

I'm wondering if it's possible to essentially "reevaluate" the middleware conditions without actually changing the current route.
The middleware's purpose is to prevent non-logged-in users from accessing the "dashboard".
My issue is, a user could become logged in or logged out without necessarily changing route but they wouldn't be redirected until they try and change pages.
I have a VueX action that triggers when the user's auth state changes but this (from what I can see), can't access the redirect or route variables.
// /mixins/auth.js
const reevaluateAuthStatus = (store, redirect, route) => {
console.log(route)
const redirectPolicy = route.meta.map((meta) => {
if (meta.auth && typeof meta.auth.redirectPolicy !== 'undefined') { return meta.auth.redirectPolicy[0] }
return []
})
const user = store.getters['auth/getUser']
if (redirectPolicy.includes('LOGGEDOUT')) {
if (user) {
return redirect('/dashboard')
}
} else if (redirectPolicy.includes('LOGGEDIN')) {
if (!user) {
return redirect('/login')
}
}
}
module.exports = {
reevaluateAuthStatus
}
// /middleware/auth.js
import { reevaluateAuthStatus } from '../mixins/auth'
export default function ({ store, redirect, route }) {
reevaluateAuthStatus(store, redirect, route)
}
Appreciate any help on this :)
You cannot re-evaluate a middleware AFAIK because it's mainly this (as stated in the documentation)
middlewares will be called [...] on the client-side when navigating to further routes
2 clean ways you can still achieve this IMO:
use some websockets, either with socket.io or something similar like Apollo Subscriptions, to have your UI taking into account the new changes
export your middleware logic to some kind of call, that you could trigger again by calling the $fetch hook again or any other data-related fetching hook in Nuxt
Some more ugly solutions would probably be:
making an internal setInterval and check if the actual state is still valid every 5s or so
move to the same page you are actually on with something like this.$router.go(0) as somehow explained in the Vue router documentation
Still, most of the cases I don't think that this one may be a big issue if the user is logged out, because he will just be redirected once he tries something.
As if the user becomes logged-in, I'm not even sure on which case this one can happen if he is not doing something pro-active on your SPA.
I don't know if it's relevant or not, but I solved a similar problem this way:
I have a global middleware to check the auth status. It's a function that receives Context as a parameter.
I have a plugin that injects itself into context (e.g. $middleware).
The middleware function is imported here.
In this plugin I define a method that calls this middleware passing the context (since the Plugin has Context as parameter as well): ctx.$middleware.triggerMiddleware = () => middleware(ctx);
Now the middleware triggers on every route change as intended, but I can also call this.$middleware.triggerMiddleware() everywhere I want.

Avoid app logic that relies on enumerating keys on a component instance

in my complex Vue project I am getting this console warning:
[Vue warn]: Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead.
Unfortunately I can not find the reason for this warning just by the above message.
How can I track down the reason for this warning?
Check if your watching an entire route object anywhere in your code. Doing so throws that error (in my case).
Refer this vue documentation on watching routes
Accessing router and current route inside setup
The route object is a reactive object, so any of its properties can be watched and you should avoid watching the whole route object. In most scenarios, you should directly watch the param you are expecting to change.
Was able to fix this with the suggestion done by Glass Cannon.(https://stackoverflow.com/a/70205284/11787139)
To clarify and maybe help someone else: I was trying to send an Axios request to the server of which the data I sent through was composed of a direct component reference emitted by the component function.
Component
saveItem(){
this.saved = true;
setTimeout( this.resetState, 2500);
this.$emit('saveitem', this)
},
Parent
saveitem(e){
const data = {item : e}
axios.post(target, data, {headers . . .).then((response) => {}
})
The error disappeared when I instead fetched the index of the list item by doing so:
saveitem(e){
let item;
this.items.forEach( function(item, index, array) {
if(item.id == e.id) pointer = item
})
data.item = pointer
axios.post(target, data, {headers . . .).then((response) => {}
})
}
So I was also having this issue, but not for the reasons the accepted answer provided. It was occurring due to my Vuex store. After a lot of digging I discovered the cause was the presence of the "CreateLogger" plugin.
So if you're having this issue and it's not due to you watching an entire route, check if you're using the CreateLogger plugin in Vuex. That might be the culprit.
This happens for me when I pass this to a data object
data() {
return {
updateController: new UpdateController({
reportTo: this
})
}
}
This used to work fine with Vue 2 but causes this error in Vue 3.
Making this modification solved the problem for me.
data() {
return {
updateController: new UpdateController({
reportTo: () => this
})
}
}
I know this might be anti-pattern but I needed to inject partial reactivity to a non-reactive part of a JS library and this was the most not complicated way of achieving this that I can think of.
This happens to me when destructuring a ref without .value.
This was happening to me only in Firefox, and when I removed the Vue Dev Tools extension it stopped. After re-installing Vue dev tools it hasn't come back. Make sure you have the latest version of the Vue Dev Tools for your browser.

Aurelia store connectTo never sets target property

I set the aurelia-store up as per the docs; in the main.ts at the bottom of all the plugins (from the skeleton app with dotnet core) I have as the last plugin defined:
aurelia.use.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-store'), { initialState })
Then my app needs to login the user and save their bearer token.
await aurelia.start();
await aurelia.setRoot(PLATFORM.moduleName("modules/login/login.vm"));
In the login class I am trying to use the #connectTo decorator. However it never sets the dependency property. So I am stuck on this simple part at the very start of the app and my work already suggested not to use Aurelia but I said I wanted to for fast POC.
I've copied the docs exactly and still have the issue. Notably, I had to turn off strictNullCheck in the tsconfig to make the doc code parse.
Login.ts
#connectTo({
target: 'state',
selector: {
userToken: (store) => store.state.pipe(pluck('userToken')),
loginRedirected: (store) => store.state.pipe(pluck('loginRedirected'))
}
})
export class Login {
static inject = [Aurelia, Store]
public state: State;
app: Aurelia;
constructor(Aurelia, private store: Store<State>) {
this.app = Aurelia
store.registerAction('ChangeUserToken', this.changeUserToken)
store.registerAction('LoginRedirected', this.loginRedirect)
}
activate() {
... this.state is always undefined.
if (!this.state.loginRedirected) { //error
}
}
}
I expect the this.state property to have a state object populated from the global state store with the initialState values.
e.g.
{ userToken: "", loginRedirected: false }
I just need to set the userToken in login and retrieve it in app.js. This is not possible; what could be missing to make this basic function actually work?
ConnectTo is a helper decorator to avoid manual state subscriptions since the Stream of states is a vanilla rxjs observable. If you take a closer look at the official plugin documentation you will notice that it sets up the subscription in a different lifecycle hook.
That said connectTo cant solve everything and with manual subscription you have the most flexibility.
Dont give up with your quest you just had bad luck of falling into a more complicated scenario of startup timing right at the begin which easy enough might bite you with lots of other Frameworks/Libraries as well. Also make sure to visit the official discourse.aurelia.io forum and post back solutions to SO.

Accessing vue-axios inside Vuex module actions

In my Vuex setup, the store is currently structured like this:
store
- modules
- common_data
- index.js
- mutations.js
- actions.js
- getters.js
Now, one of the actions inside actions.js is defined as:
populateTimeZones(context) {
var baseUrl = context.getters.getApiBaseUrl;
this.$http.get(baseUrl + '/time-zones')
.then(function (res){
if(res.status == 'ok'){
this.$store.commit('SET_TIME_ZONES', res.info.timeZones);
} else {
alert('An error occurred. Please try again!');
return;
}
}.bind(this));
}
I think the Vue instance is not available here, which is causing the action to fail with the error: Cannot read property 'get' of undefined. I've tried other combinations like this.axios and vue.axios but the result is same.
Am I doing something wrong? What is the common pattern for handling such cases?
You won't be able to access the Vue instance without passing it through or creating a new one.
A simple way to do what you want is to simply pass your this through to your action this.$store.dispatch('populateTimeZones', this), then change your method signiture to populateTimeZones({ context }, vueInstance). This would then allow you to access vueInstance.$http.
Another idea would be something called vue-inject which is a DI container for Vue. It would allow you to register axios as a service and then simply call VueInjector.get('axios') whenever you need it. On the Vue component itself you can do dependencies: ['axios'] and then use like this.axios. Not so much an issue in this case but helpful when you have lots of services doing work for you