When an object's state changes another object's state changes also in react native redux - react-native

I am trying to hold user info with defaultUser as default state after fetching. But If user state changes with UPDATEUSERSTATE, defaultUser also changes. I could not understand that behaivour
Firstly fetching the data from restApi
Updating user state on MainComponent
If User changes textinput on ModalView, updating the user state.
const userReducer = (state = {} ,action) => {
switch (action.type) {
case actionTypes.GETUSERINFOBYUSERNAME_SUCCESS:
return {
...state,
isFetching: action.isFetching,
error: action.error,
user: action.user,
defaultUser:action.user,
open: true
};
case actionTypes.UPDATEUSERSTATE:
return {
...state,
user: action.user
}
default:
console.log("[userReducer]: defalt state");
return state;
}
};
//ACTIONS
export const getUserInfoByUserNameSuccess=(user) => {
return {type: actionTypes.GETUSERINFOBYUSERNAME_SUCCESS, isFetching: true, error: null, user: user}
}
export const updateUserState=(user) => {
return {type: actionTypes.UPDATEUSERSTATE, user:user}
}
//CALLING GETUSERINFO
this.props.onGetUserInfoByUserName(val);
const mapDispatchToProps = dispatch => {
return{
onGetUserInfoByUserName : userName => dispatch(getUserInfoByUserNameFetching(userName))
};
};
//AND CALLING UPDATEUSERSTATE
textChangedHandler = (key,value) => {
let user = this.props.user;
user[key]=value;
this.props.onUpdateUserState(user);
}
const mapDispatchToProps = dispatch => {
return{
onUpdateUserState : (user) => dispatch(updateUserState(user))
};
};

The problem here is that you're directly setting the values of two separate objects to reference the same object (both user and defaultUser are being set to reference object action.user). So if you change the value of one of the objects, the reference changes which changes the second object.
Redux doesn't replace the object with a new one, but rather does a shallow copy of the new values. See the below snippet for an example:
var actionUser = { foo: 1 }
var defaultUser = actionUser
var user = actionUser
user.bar = 2
console.log(actionUser)
// { foo: 1, bar: 2 }
console.log(defaultUser)
// { foo: 1, bar: 2 }
console.log(user)
// { foo: 1, bar: 2 }
To fix this you can use the Object.assign() method to assign the references to a new object. See below snippet:
var actionUser = { foo: 1 }
var defaultUser = Object.assign({}, actionUser)
var user = Object.assign({}, actionUser)
user.bar = 2
console.log(actionUser)
// { foo: 1 }
console.log(defaultUser)
// { foo: 1 }
console.log(user)
// { foo: 1, bar: 2 }
So whenever you assign a new value from your actions to any of your state objects, use Object.assign().
Example (from your code):
case actionTypes.GETUSERINFOBYUSERNAME_SUCCESS:
return {
...state,
user: Object.assign({}, action.user),
defaultUser: Object.assign({}, action.user),
};
case actionTypes.UPDATEUSERSTATE:
return {
...state,
user: Object.assign({}, action.user),
}

Related

How to reactively re-run a function with parameters when pinia store state changes

I have a Pinia auth module, auth.js
I have the following code in it:
export const useAuthStore = defineStore('auth', {
state: () => ({
token: null,
is_logged: false,
user: {
default_landing_page: {},
},
actions: [],
}),
getters: {},
actions: {
async login(formData) {
const { data } = await api.post('login', formData);
this.token = data.access_token;
this.is_logged = data.auth;
this.actions = data.user.meta_actions;
},
},
});
Then for example, I get this.actions as
['can_view_thing', 'can_edit_thing', 'can_delete_thing']
This makes it so that I can have code such as:
import { useAuthStore } from '#/store/auth';
const auth = useAuthStore();
...
<button v-if="auth.actions.includes('can_edit_thing')">Edit Thing</button>
That works and is perfectly reactive if permissions are added or removed from the auth store actions array. The problem is I want to change it so it's a function, such as:
// pinia auth store
// introduce roles
this.roles = [{ id: 1, key: 'admin' }, { id: 2, key: 'manager' }]
...
getters: {
hasAuthorization() {
return (permission) => {
// if some condition is true, give permission
if (this.roles.some(role => role.key === 'admin') return true;
// else check if permissions array has the permission
return this.permissions.includes(permission);
// also permission could be an array of permissions and check if
// return permissions.every(permission => this.permissions.includes(permission))
};
},
},
<button v-if="hasAuthorization('can_edit_thing')">Edit Thing</button>
I researched it before and you can make a getter than returns a function which allows you to pass in a parameter. I was trying to make it reactive so that if this.actions changed, then it would re-run the getter, but it doesn't.
Is there some way I can achieve a reactive function in Pinia?
Here's an example of what I don't want:
<button v-if="auth.actions.includes('can_edit_thing') || auth.roles.some(role => role.key === 'admin')">Edit Thing</button>
I want to arrive at something like:
// preferably in the pinia store
const hasAuthorization = ({ type = 'all', permissions }) => {
const superAdminRoles = ['arbitrary', 'admin', 'superadmin', 'customer-service'];
if (auth.roles.some(role => superAdminRoles.includes(role.key)) return true;
switch (type) {
case 'any': {
return permissions.some(permission => auth.actions.includes(permission));
}
case 'all': {
return permissions.every(permission => auth.actions.includes(permission));
}
}
};
<button
v-if="auth.hasAuthorization({ type: 'all', permissions: ['can_edit_thing', 'can_edit_business'] })"
>Edit Thing</button>
I don't want to create a computed prop in 100 components that causes each to reactively update when pinia state changes.
Is there any way to do this reactively so that anytime auth.actions or auth.roles changes, the function will re-run with parameters?

Vue 3 ref access always returns undefined

Ok. so I do have this image object I get from my DB and store it into a vuex store:
// State
const state = {
userImages: [],
};
// Getters
const getters = {
getImages: (state) => {
return state.userImages;
},
getFilePathExists: (state) => (fullPath) => {
return !!state.userImages.find((item) => item.fullPath === fullPath);
},
getFileNameExists: (state) => (name) => {
return !!state.userImages.find((item) => item.name === name);
},
getImagesInFolder: (state) => (folder) => {
if(folder) {
return state.userImages.filter(im => {
if(im.folders) {
return im.folders.includes(folder)
}
return false
})
}
return state.userImages
}
};
// Mutations
const mutations = {
setImages(state, val) {
state.userImages = val;
},
};
export default {
state,
getters,
// actions,
mutations,
};
So far so good. Now if I use the '''getImages''' getter, the Object gets to be loaded into a ref:
//...
const detailImage = shallowRef({});
const showImageDetails = (image) => {
if (!showDetails.value) {
showDetails.value = true;
}
activeImage.value = image.id
detailImage.value = image;
}
The JSON has a nested Object called exif_data. If I onsole.log this, I get: Proxy {Make: 'Apple', Flash: 'Flash did not fire. No strobe return detection fun…on present. No red-eye reduction mode or unknown.', Model: 'iPhone 6', FNumber: 2.2, COMPUTED: {…}, …}.
So far so good, but when I would like to access any property of the nested Object, either with or without using .value I just get undefined.
Any ideas?

Vue3: how to pass an object from provide to index

I've been using vue.js for a few weeks and I would like to understand how to globally inject to child components an object coming from the server.
When I try to inject the object using inject:['user'] to a child component it returns an empty object.
data() {
return {
user: []
}
},
methods: {
getLoggedUserData() {
axios.get('/api/get-user/' + window.auth.id
).then(response => {
this.user = response.data.user;
});
}
},
provide: {
return {
user: this.user
}
},
created() {
this.getLoggedUserData();
}
The provide option should be a function in this case to get access to this.user property:
export default {
provide() {
return {
user: this.user
}
}
}
For descendants to observe any changes to the provided user, the parent must only update user by subproperty assignment (e.g., this.user.foo = true) or by Object.assign() (e.g., Object.assign(this.user, newUserObject)):
export default {
methods: {
async getLoggedUserData() {
const { data: user } = await axios.get('https://jsonplaceholder.typicode.com/users/1')
// ❌ Don't do direct assignment, which would overwrite the provided `user` reference that descendants currently have a hold of:
//this.user = user
Object.assign(this.user, user) ✅
}
}
}
demo

How to update a Vuex child key with Object.assign?

When payload.key is a key like foo, the code below is working fine, but how to update the value of a child key like foo.bar.a ?
export const mutations = {
USER_UPDATE(state, payload) {
console.log(payload);
state.user = Object.assign({}, state.user, {
[payload.key]: payload.value
});
}
}
=== EDIT ===
This is called by:
computed: {
...mapState(['user']),
fooBarA: {
get() {
return this.$store.state.user.foo.bar.a
},
set(value) {
this.$store.commit('USER_UPDATE', {
key: 'foo.bar.a',
value
})
}
}
}
You are replacing the whole state.user Object reference with a new Object, which destroys reactivity.
This simplified code does not demonstrate the need to use Object.assign, so in this cas you can simply:
export const mutations = {
USER_UPDATE(state, payload) {
state.user[payload.key] = payload.value
}
}
Which keeps the original state.user Object reference.
I have an approach that works.
Where the payload.key is "foo.bar.a"
const mutations = {
UPDATE_USER(state, payload) {
const setter = new Function(
"obj",
"newval",
"obj." + payload.key + " = newval;"
);
let user = { ...state.user };
setter(user, payload.value);
state.user = user;
}
};
Demo
https://codesandbox.io/s/vuex-store-nested-key-setter-x1n00
Inspiration from
https://stackoverflow.com/a/30360979/815507

VueJS $set not making new property in array of objects reactive

In my VueJS 2 component below, I can add the imgdata property to each question in the area.questions array. It works - I can see from the console.log that there are questions where imgdata has a value. But despite using $set it still isn't reactive, and the imgdata isn't there in the view! How can I make this reactive?
var componentOptions = {
props: ['area'],
data: function() {
return {
qIndex: 0,
};
},
mounted: function() {
var that = this;
that.init();
},
methods: {
init: function() {
var that = this;
if (that.area.questions.length > 0) {
that.area.questions.forEach(function(q) {
Util.HTTP('GET', '/api/v1/photos/' + q.id + '/qimage').then(function(response) {
var thisIndex = (that.area.questions.findIndex(entry => entry.id === q.id));
var thisQuestion = (that.area.questions.find(entry => entry.id === q.id));
thisQuestion.imgdata = response.data;
that.$set(that.area.questions, thisIndex, thisQuestion);
})
});
}
console.log("area.questions", that.area.questions);
},
Since area is a prop, you should not be attempting to make changes to it within this component.
The general idea is to emit an event for the parent component to listen to in order to update the data passed in.
For example
export default {
name: "ImageLoader",
props: {
area: Object
},
data: () => ({ qIndex: 0 }), // are you actually using this?
mounted () {
this.init()
},
methods: {
async init () {
const questions = await Promise.all(this.area.questions.map(async q => {
const res = await Util.HTTP("GET", `/api/v1/photos/${encodeURIComponent(q.id)}/qimage`)
return {
...q,
imgdata: res.data
}
}))
this.$emit("loaded", questions)
}
}
}
And in the parent
<image-loader :area="area" #loaded="updateAreaQuestions"/>
export default {
data: () => ({
area: {
questions: [/* questions go here */]
}
}),
methods: {
updateAreaQuestions(questions) {
this.area.questions = questions
}
}
}
Here that variable has a value of this but it's bound under the scope of function. So, you can create reactive property in data as below :
data: function() {
return {
qIndex: 0,
questions: []
};
}
Props can't be reactive so use :
that.$set(this.questions, thisIndex, thisQuestion);
And assign your API output to directly questions using this.questions.