Two way binding computed property with mappers - javascript

I use vuex in my Vue 2 project.
I have this HTML element and I try to implement two way binding:
<input v-model="message">
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
inside get and set I want to use mappers so the code will look cleaner:
computed: {
message: {
get () {
return ...mapState("obj", ["message"])
},
set (value) {
...mapMutations("obj/updateMessage", value)
}
}
}
But I get errors on two rows:
return ...mapState("obj", ["message"]) - Expression expected.
...mapMutations("obj/updateMessage", value) - Declaration or statement expected.
How can I use mappers inside get and set?
UPDATE:
mapMutations and mapState are imported to the component.

You will need to import them first, as you did
import { mapState, mapActions } from 'vuex'
Import actions tho, and not mutations. Indeed, only actions are async and the flow should always be dispatch a action > commit a mutation > state is updated.
Then, plug them where they belong
computed: {
...mapState('obj', ['message']),
// other computed properties ...
}
methods: {
...mapActions('obj', ['updateMessage']),
// other methods ...
}
Then comes the interesting part
computed: {
message: {
get () {
const cloneDeepMessage = cloneDeep(this.message)
// you could make some destructuring here like >> const { id, title, description } = cloneDeepMessage
return cloneDeepMessage // or if destructured some fields >> return { id, title, description }
},
set (value) {
this.updateMessage(value)
}
}
}
As you can see, I would recommend to also import cloneDeep from 'lodash/cloneDeep' to avoid mutating the state directly thanks to cloneDeep when accessing your state.
This is the kind of warning that the Vuex strict mode will give you, that's why I recommend to enable it in development only.
The official docs are not super explicit on this part (you need to read various parts of them and mix them all together) but it's basically a good way of doing things IMHO.

Related

Why does my vuex state change on changing my component level state even if I have not binded the vuex state to any html input element?

I have a vue store which has the following
store.js
import Vue from 'vue'
import Vuex from 'vuex'
const state = {
supplementStore: {}
}
const actions = {
getDataFromApi ({dispatch, commit}) {
APIrunning.then(response => {
commit('SET_SUPPLEMENT', response)
})
}
}
const mutations = {
SET_SUPPLEMENT (state, data) {
state.supplementStore= data
}
}
const foodstore = {
namespaced: true,
state,
actions,
mutations
}
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
foodstore
}
})
My vue component looks like this
Supp.vue
<template>
<input type="checkbox" v-model="supps.logged">
</template>
<script>
import {mapState, mapActions} from 'vuex'
import store from './store'
export default {
data () {
return {
supps: []
}
},
mounted () {
this.supps = this.supplementStore
},
computed: {
...mapState('foodstore', ['supplementStore'])
}
}
</script>
As you can see I have a component level state called supps which is assigned the value of supplementStore (which is a vuex state) as soon as it is mounted.
mounted () {
this.supps = this.supplementStore
},
supplementStore gets its value from the the API and it is a JSON object which looks like this
supplementStore = {
logged: true
}
Therefore, when my Supp.vue component is mounted my local state supps will become
supps = {
logged: true
}
supps is binded to an input field of type checkbox (Supp.vue) using the v-model directive.
What I want to achieve:
When I toggle the checkbox, supps.logged should toggle between true and false but, supplementStore.logged should remain unchanged (since I have not binded it to my input field).
What I observe in my Vue Devtools:
When I toggle the checkbox, both supps.logged AND supplementStore.logged are toggling in sync i.e both of them are toggling in sync between true and false, whereas I want only supps.logged to get toggled.
Can anyone help me?
In Javascript, object is passed by reference. (This is a reasonably good explanation => https://medium.com/nodesimplified/javascript-pass-by-value-and-pass-by-reference-in-javascript-fcf10305aa9c)
To avoid this problem, you can clone the object when assigning to supps.
mounted () {
this.supps = { ...this.supplementStore } // cloning the object using Javascript Spread syntax
},
Have you tried Object.assign instead? In JS objects are passed by reference. Assigning one to a variable will cause the original one to change if the variable is changed inside.
To clone an object, you may try this:
// this.assignedObj = new object.
// this.obj = original object.
this.assignedObj = Object.assign({}, this.obj);
JSFiddle: https://jsfiddle.net/mr7x4yn0/
Edit: As you can see from the demo, Vue.set or this.$set will not work for you (probably).
The data I was receiving from the API into supplementStore was in the form of an array of objects:
supplementStore = [
{
"logged": true
},
{
"logged": false
}
]
And as Jacob Goh and Yousof K. mentioned in their respective answers that objects and arrays get passed by reference in javascript, I decided to use the following code to assign the value of supplementStore to supps inside my mounted() hook:
mounted () {
let arr = []
for (let i = 0; i < this.supplementStore.length; i++) {
let obj = Object.assign({}, this.supplementStore[i])
arr.push(obj)
}
this.supps = arr
}
Now when I toggle my checkbox, supplementStore.logged remains unchanged while supps.logged toggles between true and false just the way I wanted.

How to loop over components object in the template?

Usually we define in a Nuxt.js component something like this:
<script>
export default {
components: {
// components
}
data() {
return {
// key/value data
}
},
methods: {
// method definitions
}
}
</script>
Is there a way to read the components object as we read data() and methods ?
This is because I have several components and I want to loop on them to refactor parts of my code.
You can get Component data by using $options.
Try this.
created() {
console.log(this.$options.components)
}
it returns an object, keys are the component names, values are the contructors.
codepen - https://codesandbox.io/s/yk9km5m0wv

How to rewrite this js class without decorators

I want to use mobx without using decorators. Usually I use decorate from mobx package but in this particular case, I could not find a way to make it work.
Original code :
import { observable } from 'mobx'
import { create, persist } from 'mobx-persist'
class Order {
#persist('object')
#observable
currentOrder = null
}
What I tried :
import { observable, decorate } from 'mobx'
import { create, persist } from 'mobx-persist'
import { compose } from 'recompose'
class Order {
currentOrder = null
}
decorate(Order, {
currentOrder: compose(persist('object'), observable),
})
The error comes from persist telling serializr decorator is not used properly.
Any idea why this is different from above and does not work ?
TL;DR
Property Decorators requires a very specific composition implementation.
Solution Demo:
Full Answer
Property Decorators are basically a function of the form:
(target, prop, descriptor) => modifiedDescriptor
So, in order to compose two Property Decorators you need to pass the 1st decorator's result as a third argument of the 2nd decorator (along with target and prop).
Recompose.compose (same as lodash.flowRight) applies functions from right to left and passing the result as a single argument to the next function.
Thus, you can't use Recompose.compose for composing decorators, but you can easily create a composer for decorators:
/* compose.js */
export default (...decorators) => (target, key, descriptor) =>
decorators.reduce(
(accDescriptor, decorator) => decorator(target, key, accDescriptor),
descriptor
);
Then we use it in order to compose observable and persist("object").
/* Order.js */
import { observable, decorate, action } from "mobx";
import { persist } from "mobx-persist";
import compose from "./compose";
class Order {
currentOrder = null;
}
export default decorate(Order, {
currentOrder: compose(
observable,
persist("object")
)
});
[21/8/18] Update for MobX >=4.3.2 & >=5.0.4:
I opened PRs (which have been merged) for MobX5 & MobX4 in order to support multiple decorators OOB within the decorate utility function.
So, this is available in MobX >=4.3.2 & >= 5.0.4:
import { decorate, observable } from 'mobx'
import { serializable, primitive } from 'serializr'
import persist from 'mobx-persist';
class Todo {
id = Math.random();
title = "";
finished = false;
}
decorate(Todo, {
title: [serializable(primitive), persist('object'), observable],
finished: observable
})
An easier solution is to have
class Stuff {
title = ''
object = {
key: value
}
}
decorate(Todo, {
title: [persist, observable],
object: [persist('object'),observable]
})
No need to install the serializr package. The above functionality is built into mobx persist.

Optimizing React-Redux connected PureComponent

I'm have a very frustrating time trying to optimize my React-Redux application.
I have a header component that is reloading on every change to the redux store. My header component is a PureComponent
I have installed why-did-you-update, and it tells me:
Header.props: Value did not change. Avoidable re-render!
This is my Component:
export class Header extends PureComponent {
logout() {
// this.props.logout();
}
signup = () => {
this.props.history.push(urls.SIGNUP)
}
render() {
console.log("=============== RELOADING HEADER")
return (
<div>
<HeaderContent
logout={this.logout.bind(this)}
signup={this.signup.bind(this)}
user={this.props.user}/>
</div>
)
}
}
export function mapStateToProps(store) {
// EDITTED: I initially thought this was causing the problem
// but i get the same issue when returning a javascript object
//const u = loginUserFactory(store);
const u ={}
return {
user: u,
}
}
export function mapDispatchToProps(dispatch) {
return {
logout: function l() {
dispatch(authActions.logout())
}
}
}
export function mergeProps(propsFromState,propsFromDispatch,ownProps) {
return {
// logout: function logout() {
// propsFromDispatch.logout()
// },
...propsFromState,
...ownProps
}
}
let HeaderContainer = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{pure: true}
)(Header)
export default withRouter(HeaderContainer);
Header.propTypes = {
history: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
logout: PropTypes.func.isRequired,
}
I have verified that the console.log indicating that the render function is called prints every time the redux store is changed.
If I uncomment the function in merge props whyDidYouUpdate complains that the function caused the re-render.
The re-renders are significantly impacting the performance of my app. I considered writing my own shouldComponentUpdate() function, but read that it is a bad idea to do deep equals in that function for performance reasons.
So what do I do?
EDIT:
This is the code in Login User Factory. Initially I thought this was the problem, but when I remove that code I still get the same issue.
const loginUserFactory = state => {
const u = getLoginUser(state);
const isLoggedIn = !_.isEmpty(u);
const location = getLocation(state);
return {
get id() { return u.id },
get groupNames() { return u.group_names },
get avatarSmall() { return u.avatar_small },
get name() { return u.name },
get email() { return u.email },
// my goal was to localize these methods into one file
// to avoid repeated code and
// to make potential refactoring easier
get location() { return location},
get isHost() {return u.type === "host"},
get isBooker() {return u.type === "booker"},
get isLoggedIn() { return isLoggedIn },
}
}
export default loginUserFactory;
I guess that loginUserFactory() creates a new user object every time it gets called which is every time the store gets updated thus always passing a new user object to your component that is not equal to the previous one.
Also your Header doesn't do anything with the user except passing it further down the tree. You should instead connect the HeaderContent component and only map the properties of the user object to it, that it actually needs, e.g. the name.
In general mapStateToProps() should never have any side effects. It should only filter/sort/calculate the props for the connected component given the state and the own props. In the most trivial cases it does nothing more than returning a subset of properties from the store.
You're using bind in your click handlers. Big no-no! Each rerender will create a completely new instance of the function when you bind inside the handlers. Either bind in a constructor or turn your click handler methods into arrow functions.
handleClick = () => {
}
// or
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}
Also, do not implement any manipulations or algorithms in mapStateToProps or mapDispatchToProps. Those also trigger rerenders. Place that logic somewhere else.

Pass prop as module name when mapping to namespaced module

I'm trying to pass the store module namespace via props to a component. When I try and map to getters with the prop, it throws this error,
Uncaught TypeError: Cannot convert undefined or null to object
If I pass the name as a string it works.
This Works
<script>
export default {
props: ['store'],
computed: {
...mapGetters('someString', [
'filters'
])
}
}
</script>
This does not work
this.store is defined
this.store typeof is a String
<script>
export default {
props: ['store'],
computed: {
...mapGetters(this.store, [
'filters'
])
}
}
</script>
I used this style utilising beforeCreate to access the variables you want, I used the props passed into the component instance:
import { createNamespacedHelpers } from "vuex";
import module from '#/store/modules/mymod';
export default {
name: "someComponent",
props: ['namespace'],
beforeCreate() {
let namespace = this.$options.propsData.namespace;
const { mapActions, mapState } = createNamespacedHelpers(namespace);
// register your module first
this.$store.registerModule(namespace, module);
// now that createNamespacedHelpers can use props we can now use neater mapping
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
}),
// because we use spread operator above we can still add component specifics
aFunctionComputed(){ return this.name + "functions";},
anArrowComputed: () => `${this.name}arrows`,
};
// set up your method bindings via the $options variable
this.$options.methods = {
...mapActions(["initialiseModuleData"])
};
},
created() {
// call your actions passing your payloads in the first param if you need
this.initialiseModuleData({ id: 123, name: "Tom" });
}
}
I personally use a helper function in the module I'm importing to get a namespace, so if I hadmy module storing projects and passed a projectId of 123 to my component/page using router and/or props it would look like this:
import { createNamespacedHelpers } from "vuex";
import projectModule from '#/store/project.module';
export default{
props['projectId'], // eg. 123
...
beforeCreate() {
// dynamic namespace built using whatever module you want:
let namespace = projectModule.buildNamespace(this.$options.propsData.projectId); // 'project:123'
// ... everything else as above with no need to drop namespaces everywhere
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
})
}
}
}
Hope you find this useful.
I tackled this problem for hours, too. Then I finally came up with one idea.
Add attachStore function in a child vue component. A function nama is not important. Any name is ok except vue reserved word.
export default {
:
attachStore (namespace) {
Object.assign(this.computed, mapGetters(namespace, ['filters']))
}
}
When this vue component is imported, call attachStore with namespace parameter. Then use it at parent components attributes.
import Child from './path/to/child'
Child.attachStore('someStoresName')
export default {
name: 'parent',
components: { Child }
:
}
The error you're encountering is being thrown during Vue/Vuex's initialization process, this.store cannot be converted because it doesn't exist yet. I haven't had to work with namespacing yet, and this is untested so I don't know if it will work, but you may be able to solve this problem by having an intermediary like this:
<script>
export default {
props: ['store'],
data {
namespace: (this.store !== undefined) ? this.store : 'null',
},
computed: {
...mapGetters(this.namespace, [
'filters'
])
}
}
</script>
That ternary expression will return a string if this.store is undefined, if it isn't undefined then it will return the value in this.store.
Note that there is also a discussion about this on Vue's Github page here: https://github.com/vuejs/vuex/issues/863
Until Vue formally supports it, I replaced something like
...mapState({
foo: state => state.foo
})
with
foo () {
return this.$store.state[this.namespace + '/foo'] || 0
}
Where namespace is passed to my child component using a prop:
props: {
namespace: { type: String, required: true }
}

Categories

Resources