Vue3 reactivity weird behavior - javascript

I am learning vue3 reactivity system and cannot figure out the following strange behavior. Here's my code.
setup(props) {
let weekDay = props.weekDay;
let taskState = reactive({
[weekDay]: [[{
description: '',
status: '',
}]],
});
function changeStatus(index, event) {
let task = taskState[weekDay][index]; //works
taskState[weekDay][index] = {
...task,
status: event.target.value
};
}
return {
taskState,
changeStatus
}
}
updating the state status reactively looks like works, but if i change the function like below, this would not work. Why is it happening? are object task and taskState[weekDay][index] not
same ?
function changeStatus(index, event) {
let task = taskState[weekDay][index]; //not works
task = {
...task,
status: event.target.value
};
}

It seems that task object is not a reactive, because we change the value using an assignment to a regular object, and the object task no longer has a reactive parent such as taskState, so there is no setter attached to it and a simple value is returned, instead of a reactive proxy
function changeStatus(index, event) {
let task = taskState[weekDay][index]; //not works
task = {
...task,
status: event.target.value
};
}
if we want to make it work we should use like this:
function changeStatus(index, event) {
let task = taskState[weekDay]; //works
task[index] = {
...task[index],
status: event.target.value
};
}
in that case task[index] will be reactive and the setter will be triggered in the taskState[weekDay] object

Related

Vue and Vuex: computed property isn't called when changing state

I'm quite new with Vue and Vuex so please bear with me.
I want to make the computed function versions() get called when I change state.template, but I'm failing to do so. More specifically, when state.template.versions changes.
This is part of the component that I want to re-render when state.template.versions changes. You can also see the computed property versions() which I want to be called:
<el-dropdown-menu class="el-dropdown-menu--wide"
slot="dropdown">
<div v-for="version in versions"
:key="version.id">
...
</div>
</el-dropdown-menu>
...
computed: {
...mapState('documents', ['template', 'activeVersion']),
...mapGetters('documents', ['documentVersions', 'documentVersionById', 'documentFirstVersion']),
versions () {
return this.documentVersions.map(function (version) {
const v = {
id: version.id,
name: 'Draft Version',
effectiveDate: '',
status: 'Draft version',
}
return v
})
},
This is the getter:
documentVersions (state) {
return state.template ? state.template.versions : []
},
This is the action:
createProductionVersion (context, data) {
return new Promise((resolve, reject) => {
documentsService.createProductionVersion(data).then(result => {
context.state.template.versions.push(data) // <-- Here I'm changing state.template. I would expect versions() to be called
context.commit('template', context.state.template)
resolve(result)
})
This is the mutation:
template (state, template) {
state.template = template
},
I've read that there are some cases in which Vue doesn't detect chanegs made to an array, but .push() seems to be detected. Source: https://v2.vuejs.org/v2/guide/list.html#Caveats
Any idea on why the computed property is not being called when I update context.state.template.versions?
The issue may come from state.template = template. You guessed correctly that it was a reactivity issue, but not from the Array reactivity, but the template object.
Vue cannot detect property addition or deletion. This includes affecting a complex object to a property. For that, you need to use Vue.set.
So your mutation should be :
template (state, template) {
Vue.set(state, "template", template)
},
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
Your function won't get called because this is wrong:
context.state.template.versions.push(data)
context.commit('template', context.state.template)
the context.state object just points to your current state nothing more.
My suggested solution will be:
First You need to declare your store state correctly
state: {
template: {
versions: []
}
}
You need to update your getter to look like this with no unnecessary
conditioning:
documentVersions: state => return state.template.versions,
add a new mutation
ADD_VERSION: (state, version) => {
state.template = {
...state.template,
versions: [...state.template.versions, version]
};
}
your action should like this now:
createProductionVersion({commit}, data) {
return new Promise((resolve, reject) => {
documentsService.createProductionVersion(data).then(result => {
commit('ADD_VERSION', data);
resolve(result);
});
});
}
In your component I suggest to change your computed property from a function
to an object that contains a get and set methods (set is optional)
versions: {
get() {
return this.documentVersions.map(function (version) {
const v = {
id: version.id,
name: 'Draft Version',
effectiveDate: '',
status: 'Draft version',
}
return v
})
}
},
I think this error occurred because you did not declare your store state correctly. Make sure you have the versions property in your template object.
state: {
template: {
versions: []
}
}
This way, any changes in the versions property will be detected by vue.

TypeScript - Send optional field on POST only when there is value

I am using Formik, in my React application. I have a simple form with 3 fields. I am doing 2 operations with that form. Add/Edit Resources.
My Problem is that one field is optional. Meaning I should never send it, if its value is null. Currently, I send an empty string which is wrong.
I am using TS-React-Formik, and here is my code for the handleSubmit method:
interface IValues extends FormikValues {
name: string;
owner?: string;
groups: string[];
}
interface CreateAndEditProps {
doSubmit(service: object, values: object): AxiosResponse<string>;
onSave(values: IValues): void;
}
handleSubmit = (values: FormikValues, formikActions:FormikActions<IValues>) => {
const { doSubmit, onSave, isEditMode } = this.props;
const { setSubmitting } = formikActions;
const payload: IValues = {
name: values.name,
groups: values.groups,
owner: values.owner
};
const submitAction = isEditMode ? update : create;
return doSubmit(submitAction, payload)
.then(() => {
setSubmitting(false);
onSave(payload);
})
.catch(() => {
setSubmitting(false);
});
};
I thought a simple if statement would work, and while it does, I do not like it at all. Let me give you an example of why. If I add 2 more optional fields, as I am about to do, in a similar form, I do not want to do several if statements to achieve that.
If you could think of a more elegant and DRY way of doing it, It would be amazing. Thank you for your time.
Look at the removeEmptyKeys() below.
It takes in an Object and removes the keys that have empty string.It mutates the original Object, please change it accordingly if you expect a diff behaviour.
In your code after defining payload, I would simply call this method , removeEmptyKeys(payload)
Also it will resolve your if else problem.
removeEmptyKeys = (item)=>{
Object.keys(item).map((key)=>{
if(payload[key]===""){
delete payload[key]}
})}
var payload = {
one : "one",
two : "",
three : "three"
}
removeEmptyKeys(payload)
Please mark it as resolved if you find this useful.
For your code :
const removeEmptyKeys = (values: IValues): any => {
Object.keys(values).map((key) => {
if (payload && payload[key] === "")
{ delete payload[key] } })
return values;
}

Vue 2.0, where to place local functions

Where do you correctly place local functions in vue 2.x?
I could just place them in the "methods" object, but I'd like them to be completely local to the instance if thats possible.
Sort of like this in Plain JS :
window._global = (function () {
function _secretInsideFunct(){
return "FooBar";
}
var __localObject = {
outsideFunct : function () {
return _secretInsideFunct();
}
}
return __localObject;
}());
..where _global._secretInsideFunct() wouldnt be accessible anywhere else but from inside the _global object.
In this specific case I want to make a function that creates an array object if it doesn't exist.. Something like:
function CreateOrSet (workArray, itemName, itemValue ){
var salaryRow = self.Status.Rows.find(r => r.recordID == itemName);
if (!salaryRow) {
salaryRow = { recordID: itemName, recordAmount: 0, recordName: "Løn" };
self.Status.Rows.push(salaryRow);
}
salaryRow.recordAmount = itemValue ;
}
..but a general approach for these cases is better :)
Now this function doesn't looks like a utility or helper function, but relate to a state, Status.Rows. If I were you, I will define it as close as to the state or the module that the state being used.
If the state will be used across the app, maybe I will define it in entry file, index.js or app.vue.
Or If you are using vuex, you can define it as an vuex action. So you may do something like this:
const store = new Vuex.Store({
state: {
status: {
rows: []
},
mutations: {
pushRow (state, salaryRow) {
state.status.rows.push(salaryRow)
},
changeAmount (state, id, amount) {
const salaryRow = state.status.Rows.find(r => r.recordID === id)
salaryRow.recordAmount = amount
}
},
actions: {
createRow (context, itemName) {
const salaryRow = { recordID: itemName, recordAmount: 0, recordName: "Løn" };
context.commit('pushRow', salaryRow)
}
}
})
You can put all of the code to a single action, it is just an idea, how you organize your code depend on your needs.

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

immutable.js & react.js setState clears prototype of the object

I have the following code in component:
constructor() {
this.state = Immutable.fromJS({
user : {
wasChanged : false,
firstName : false,
lastName : false,
address : {
street : false,
}
}
});
}
onEdit({target: {dataset: {target}}}, value) {
this.setState(function (prevState) {
return prevState.setIn(['user', target], value);
});
}
render() {
var user = this.state.get('user').toJS();
...
}
The problem is that when I try to update the value in onEdit I see that prevState has different prototype set. I don't understand why or what am I doing wrong. I see this in console
> Object.getPrototypeOf(this.state)
src_Map__Map {##__IMMUTABLE_MAP__##: true}
> Object.getPrototypeOf(prevState)
Object {}
After the state has been changed it goes to render where it of course can't find get function or anything from Immutable
Using react with addons 0.13.3.
Put it as a key on state.
this.state = {
data: Immutable...
};
Currently the reason you can't use an Immutable object as state is the same reason you can't do this.state = 7: it's not a plain JavaScript object.
Roughly the operation looks like this:
React.Component.prototype.setState = (changes) => {
batchUpdate(() => {
// copies own properties from state to the new object
// and then own properties from changes to the new object
var nextState = Object.assign({}, state, changes);
this.componentWillUpdate(...);
this.state = nextState;
queueDomUpdateStuff(this.render(), () => this.componentDidUpdate());
});
};
Components' state should be a plain JavaScript object; values of that object can be Immutable values. Supporting Immutable values as state is discussed in issue 3303.

Categories

Resources