Vue Reactivity: Creating reactive data with provide and inject - vuejs3

My aim is to validate a users' credentials from vue frontend, fetch their data, store the data in a globally available variable and use them in any component. After some work, I'm able to use Vue's reactive() method with inject and provide. Here's my current code:
In store/index.js
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = () => reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
let user = createState.currentUser
user = userData
console.log(user)
}
})
export const useState = () => inject(stateSymbol)
Then in main.js
import { createApp } from 'vue'
import App from './App.vue'
// store modules
import { stateSymbol, createState } from './store'
// create app
const app = createApp(App)
app.provide(stateSymbol, createState())
Then in login/register component named Login.vue
import { useState } from "../store"
import axios from 'axios'
export default {
name: 'RegisterLogin',
setup() {
let state = useState()
axios.get(getUser, jwtConfig).then(response => {
userId = response.data.id
const currentUser = getUserProfile+userId // URL
console.log(currentUser)
const getCurrentUser = axios.get(currentUser, jwtConfig)
getCurrentUser.then(response => {
state.logIn(response.data)
console.log(state.currentUser)
})
})
}
}
console.log(user) in store/index.js logs the userData as expected. I believe with that, createState.currentUser should be mutated. The problem however is console.log(state.currentUser) logs an empty proxy object. Also, if I try accessing the state.currentUser from another component like Home.vue:
<template>
{{ state.currentUser }}
</template>
<script>
import { useState } from '../store/'
export default {
name: 'Index',
setup() {
return {
state: useState(),
}
},
}
</script>
...an empty object is always displayed even after the user logs in successfully. Is there anything I'm doing wrong? Thank you for your assistance (in advance).

I was looking through the code but couldn't find anywhere where the variable was getting mutated, but I believe I know what's going on.
You are reassigning the user object, not assigning the createState.currentUser to another object.
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
// over here you assign user to createState.currentUser
let user = createState.currentUser;
// then you reassign user to `user data`
user = userData
console.log(user)
}
})
export const useState = () => inject(stateSymbol)
you could instead try
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = () => reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
createState.currentUser = userData
console.log(createState.currentUser)
}
})
export const useState = () => inject(stateSymbol)
As an aside, why are you using inject? I can't figure out what you're expecting it to do.
export const useState = () => stateSymbol would have the same result, but you don't even need to pass the function, you can just use something more concise like...
//////// store
import { reactive } from 'vue'
// global states
export const store = reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {}
})
export const logIn = (userData) => {
store.currentUser = userData
}
////////////////////////////////////////////////
////// main
import { store, logIn } from "../store"
import axios from 'axios'
export default {
name: 'RegisterLogin',
setup() {
axios.get(getUser, jwtConfig).then(response => {
userId = response.data.id
const currentUser = getUserProfile+userId // URL
console.log(currentUser)
const getCurrentUser = axios.get(currentUser, jwtConfig)
getCurrentUser.then(response => {
logIn(response.data)
console.log(store.currentUser)
})
})
}
}

It appears #Daniel was right after all. My logIn method in store/index.js was not really mutating the currentUser property. In fact, I'm unable to access that property from the method. To solve this, I had to do the mutation immediately after the axios call like state.currentUser = response.data in then method and boom, the state is updated in all components! Thanks.

Related

How to use `useRoute`/`useRouter` in a Pinia Store using Setup Store syntax in Vue3?

I've been trying to get my Pinia store up and running in Vue 3 and it all has been pretty effortless until I wanted to access some parameters in the url.
I have a store (simplified) like so:
import { defineStore } from 'pinia';
import { useRoute } from 'vue-router';
import { useLocalStorage } from '#vueuse/core';
export const useUserStore = defineStore('user', () => {
const route = useRoute();
const uuid = ref(
useLocalStorage('uuid', route.params.id)
)
return { uuid };
})
Unfortunately, the route remains undefined as if useRoute() is not triggered properly. I've seen that you can add plugins to add the router instance to the pinia store on initialisation, but there's no way I can find to access that this instance in a Setup Store.
Any help would be greatly appreciated
route is not defined when the pinia is initiated.
You need to wait a bit.
One way to do this is to call the function when the component is loaded.
export const useUserStore = defineStore('user', () => {
const route = useRoute();
const id = ref('');
const setId = () => {
id.value = route.params.id as string; // don't need as string if you don't use TypeScript
};
return { id, setId };
});
<script setup lang="ts">
import { useUserStore } from '../stores/user';
const user = useUserStore();
user.setId(); // call the function from pinia, route.params works just fine
</script>
Link Demo

React-redux Toolkit: Cannot set new state, when passing reducer as prop to another function

I am trying to use react redux toolkit and pass setter function to set new state on firebase's 'onAuthStateChanged'. The plan was to pass user's state (object or null) to reducer, depending if user is logged in or logged out. This is my first usage of redux, so I can't get why my code doesn't work. There is no errors, but in redux devtools state is always equal to null.
Configure Store:
import { configureStore } from '#reduxjs/toolkit'
import { Provider } from 'react-redux';
import userReducer from './utils/userReducer';
const store = configureStore({
reducer: {
user: userReducer,
}
})
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
My reducer:
import { createSlice } from "#reduxjs/toolkit";
export const userSlice = createSlice({
name: 'user',
initialState: null,
reducers: {
setUser: (state, action) => {
state = action.payload;
}
}
})
export const {setUser} = userSlice.actions;
export default userSlice.reducer;
Where I am dispatching it:
import { setUser } from '../utils/userReducer'
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
const handleLogin = async (e) => {
e.preventDefault()
const { user } = await logInWithEmail(email, password)
await setCurrentUser(() => dispatch(setUser))
}
Firebase function, where I am trying to use reducer:
export const setCurrentUser = async (setUser) => {
await onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser)
})
}
I understand, that with useContext it would be much easier, but I am trying to learn redux by implying it.
Try like that:
import { setUser } from '../utils/userReducer'
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
const handleLogin = async (e) => {
e.preventDefault()
const { user } = await logInWithEmail(email, password)
// This line updated
await setCurrentUser((currentUser) => dispatch(setUser(currentUser)))
}
The reason:
your setCurrentUser function prop setUser is just function () => dispatch(setUser), but this function does not receive any prop, and dispatch(setUser) does not do anything. you need to pass value (payload) to reducer function.
Additionally, try passing dispatch itself as prop and dispatch inside of onAuthStateChanged.
export const setCurrentUser = async (dispatch) => {
await onAuthStateChanged(auth, (currentUser) => {
dispatch(setUser(currentUser))
})
}
import setUser reducer function if handleLogin and setCurrentUser function is in different files separately.

Vue + Pinia + Firebase Authentication: Fetch currentUser before Route Guard

Recently I started to use Pinia as a global store for my Vue 3 Project. I use Firebase for the user authentication and am trying to load the current user before Vue is initialized. Ideally everything auth related should be in a single file with a Pinia Store. Unfortunately (unlike Vuex) the Pinia instance needs to be passed to the Vue instance before I can use any action and I believe that is the problem. On first load the user object in the store is empty for a short moment.
This is the store action that is binding the user (using the new Firebase Web v9 Beta) in auth.js
import { defineStore } from "pinia";
import { firebaseApp } from "#/services/firebase";
import {
getAuth,
onAuthStateChanged,
getIdTokenResult,
} from "firebase/auth";
const auth = getAuth(firebaseApp);
export const useAuth = defineStore({
id: "auth",
state() {
return {
user: {},
token: {},
};
},
actions: {
bindUser() {
return new Promise((resolve, reject) => {
onAuthStateChanged(
auth,
async (user) => {
this.user = user;
if (user) this.token = await getIdTokenResult(user);
resolve();
},
reject()
);
});
},
// ...
}})
and this is my main.js file
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import { useAuth } from "#/store/auth";
(async () => {
const app = createApp(App).use(router).use(createPinia());
const auth = useAuth();
auth.bindUser();
app.mount("#app");
})();
How can I set the user before anything else happens?
I figured it out. Had to register the router after the async stuff
//main.js
(async () => {
const app = createApp(App);
app.use(createPinia());
const { bindUser } = useAuth();
await bindUser();
app.use(router);
app.mount("#app");
})();

Correct way of reusing functions in Composition API

I use Vue3 Composition API and am trying to explore its reusability possibilities. But I feel that I don't understand how it should be used.
For example, I extracted the login function to a file, to use it on login, and also after registration.
#/services/authorization:
import { useRoute, useRouter } from "vue-router";
import { useStore } from "#/store";
import { notify } from "#/services/notify";
const router = useRouter(); // undefined
const route = useRoute(); // undefined
const store = useStore(); // good, but there is no provide/inject here.
export async function login(credentials: Credentials) {
store
.dispatch("login", credentials)
.then(_result => {
const redirectUrl =
(route.query.redirect as string | undefined) || "users";
router.push(redirectUrl);
})
.catch(error => {
console.error(error);
notify.error(error.response.data.message);
});
}
interface Credentials {
email: string;
password: string;
}
#/views/Login:
import { defineComponent, reactive } from "vue";
import { useI18n } from "#/i18n";
import { login } from "#/services/authorization";
export default defineComponent({
setup() {
const i18n = useI18n();
const credentials = reactive({
email: null,
password: null
});
return { credentials, login, i18n };
}
});
And the problem is that route and router are both undefined, because they use provide/inject, which can be called only during setup() method. I understand why this is happening, but I don't get what is correct way to do this.
Currently, I use this workaround #/services/authorization:
let router;
let route;
export function init() {
if (!router) router = useRouter();
if (!route) route = useRoute();
}
And in Login (and also Register) component's setup() i call init(). But I feel that it's not how it's supposed to work.

redux state not changing

I'm using a Net Core, React-Redux boiler-plate, and when I run the fetch api action, the reducer state does not change at all.
Here is my action
import axios from "axios";
import config from '../config';
const ROOT_URL = config[process.env.NODE_ENV].api;
export const FETCH_EVENTS = "FETCH_EVENTS";
export function fetchEvents() {
const url = ROOT_URL +"/Event/GetAllEvents";
const request = axios.get(url);
return {
type: FETCH_EVENTS,
payload: request
};
}
my index reducer:
import { combineReducers} from 'redux';
import { routerReducer } from 'react-router-redux';
import dataReducer from './dataReducer'
const reducers = {
events: dataReducer
};
const rootReducer = combineReducers({
...reducers,
routing: routerReducer
});
export default rootReducer;
and my reducer:
import { FETCH_EVENTS } from "../actions/ExtractActions";
export default function (state = [], action) {
switch (action.type) {
case FETCH_EVENTS:
console.log("inside reducer")
return [action.payload, ...state];
}
return state;
}
So I add this code in the Home component:
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchEvents }, dispatch);
}
function mapStateToProps(state) {
return {
events: state.events
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
but when I try to run the action and try to see if the reducer state has changed, I get on console log an empty array for "this.props.events". Even though if I am trying to store api data to the state, I even tried modifying the reducer method and simply returning a string, but this.props.events returns an empty array [] again. I am guessing my redux is not working but I don't know why. I've been debugging all night long
componentWillMount() {
this.props.fetchEvents()
console.log(this.props.events)
}
I found the error. For some reason I had to call this.props.events in the render() method and not componentwillmount.
axios.get() is an async function. That's why you couldn't see the updated state when you logged it right after fetching the events. I would recommend you to use the redux-devtools-extension for debugging. Hope this helps. Cheers!

Resources