I'm just getting started with Mobx in a react-native project and am having trouble understanding how to perform changes on a observed object.
Changing the object reference via the setWorkingObject action function in my store properly renders the UI, however if I just want to change a single property within this object, how do I cause a render?
My "store":
export default class MyStore {
constructor() {
extendObservable(this, {
workingObject: null
});
}
}
My "container":
class Container extends Component {
render() {
return (
<Provider store={new MyStore()}>
<App />
</Provider>
);
}
}
and my "component", which uses a simple custom input component (think of it like Checkbox) to perform changes to a property of my workingObject
class MyClass extends Component {
...
render() {
const {store} = this.props;
return
<View>
...
<RadioGroup
options={[
{ title: "One", value: 1 },
{ title: "Two", value: 2 }
]}
onPress={option => {
store.workingObject.numberProperty = option.value;
}}
selectedValue={store.workingObject.numberProperty}
/>
...
</View>
}
}
export default inject("store")(observer(MyClass));
I can't figure out why this doesn't work, in fact it looks very similar to the approach used in this example
Any other tips/critique on how I've implemented mobx welcome
The problem is that only existing properties are made observable at the time the workingObject is first assigned.
The solution is to declare future properties at the time of assignment, ie:
// some time prior to render
store.workingObject = { numberProperty:undefined };
First, you don't want to set initial value to null. Second, adding properties to observable object after it was created will not make added properties observable. You need to use extendObservable() instead of assigning new properties directly to observable object. Another solution is to use observable map instead.
in your store:
extendObservable(this, {
workingObject: {}
});
in your component:
extendObservable(store.workingObject, {numberProperty: option.value});
I recommend using Map in this case:
extendObservable(this, {workingObject: new Map()});
in your component:
store.workingObject.set(numberProperty, option.value);
Related
I am having a data object that consists of properties unrelated to vue/the UI and data that describes the state. Now I only want the state to be reactive but I need the entire object initially in the component. I need that vue not modifies the other properties because it messes with another library accessing the other properties. (expects array but gets observer)
Is it possible to only make part of an object reactive?
class Game {
constructor() {
this.untrackedProperty = ...;
this.state = {
these: "",
should: "",
be: "",
reactive: ""
}
}
}
// vue component
<script>
export default {
data: function() {
return {
gameState: null
}
},
created() {
this.game = new Game();
this.gameState = this.game.state;
}
}
</script>
Something like that.
I meant it as "That's how I think it should work - it doesn't, but I think it describes pretty well my intentions"
How about passing into your component 2 values:
state this will be tracked
untrackedProperty this will not be tracked
Instead of passing Game as a whole object, separate those 2 values
Let's say that I have two forms, each related to a seperate mobx store. One form is for Client info (first name, last name etc), and the other for Employee info. Each form obviously has multiple inputs that update the observables in the related store.
In this example I have an action in each store that takes an event and based on the name, updates the value:
#action handleInputChange = (e) => {
this[e.target.name] = e.target.value
}
Is there a way to abstract this action into a helper file, something that would contain common actions, instead of retyping this again and again?
Thanks in advance, I'm pretty new to this as you can imagine.
There are several ways to handle the question. In my project, I just wrote an HOC(Higher-Order Component) to do that.
export default function asForm(MyComponent, formDataProp) {
return #observer class Form extends Component {
// constructor, etc.
updateProperty(key, value) {
this.props[formDataProp][key] = value;
}
// some other functions like double click prevention, etc.
render() {
return (
<MyComponent
{...this.props}
updateProperty={this.updateProperty}
// some other props
/>
);
}
};
}
Then use the HOC like this:
#observer
class UserForm extends Component {
render() {
const { updateProperty, userInfo } = this.props;
return (
<div className="form-wrapper">
<YourInputComponent
name="name"
updateProperty={updateProperty}
value={userInfo.name}
// other props
/>
</div>
);
}
}
UserForm.propTypes = {
userInfo: PropTypes.instanceOf(UserInfo),
updateProperty: PropTypes.func.isRequired,
};
export default asForm(UserForm, 'userInfo');
I am not sure if this solution violates the rule that you should not assign values to props.
How to achieve 2 way data binding for input in dojo 2?
handleChange = (e) => {
this.setState({ textValue: e.target.value });}
<Input name='title' defaultValue={this.state.textValue} placeholder='title...' onChange={this.handleChange} />
I know this is how we do in React but don't know how to achieve in dojo 2.
In fact React supports only one-way binding, and your example illustrates it well. You need to update state, to re-render react component.
And as far as I understood from dojo2 docs and tutorials, there is almost same approach under the hood. Take a look here
Dojo 2 is built around unidirectional, top-down property propagation where it is the parent widget’s job to pass properties down to its children. In fact, a child widget has no direct reference to a parent widget! When a property changes, widgets are re-rendered (using an efficient virtual DOM) to reflect the updated state.
And it may look like this:
private _addWorker() {
this._workerData = this._workerData.concat(this._newWorker);
this._newWorker = {};
this.invalidate();
}
You change data and call invalidate() to re-render widget.
This is the solution to achieve 2 way data binding in Dojo 2.
InputWidget:-
interface IInputProps {
value: string;
onChange(event: Event): void;
}
export class InputWidget extends WidgetBase<IInputProps> {
private _onChange (event: Event) {
event.stopPropagation();
this.properties.onChange && this.properties.onChange((event.target as HTMLInputElement).value);
}
protected render(): DNode {
const { value } = this.properties;
return v('input', {
key: "input",
type: "text",
value
onchange: this._onChange
});
}
}
InputWrapper widget:-
export class InputWrapper extends WidgetBase<IWrapperProps> {
private inputValue: string = '';
protected inputValueChanges(value: string) {
this.inputValue = value;
this.invalidate();
}
protected render(): DNode {
<div>
{w(InputWidget, {onchange: this.inputValueChanges, value: this.inputValue })}
<span>Input Value:- {this.inputValue}</span>
</div>
}
}
This is the solution to achieve 2 way data binding in Dojo 2.
Hope this will be helpful! :(
Consider I have a dump component, use for display user's information.
There're 2 ways to pass user information to component:
1):
class UserItem extends PureComponent {
// pass user's properties
const {
userName, userEmail,
} = this.props;
// same
render() {
return (
<View>
<Text>{userName}</Text>
<Text>{userEmail}</Text>
</View>
);
}
}
2):
class UserItem extends PureComponent {
// pass a user object
const {
userName, userEmail,
} = this.props.user;
// same
render() {
return (
<View>
<Text>{userName}</Text>
<Text>{userEmail}</Text>
</View>
);
}
}
So which way I should use? I will list down some pros & cons that I know in 2 ways:
1)
pros:
the logic is very easy to understand, there're no "magic" things happen, the component display whatever you pass in.
can use the power of PureComponent by shallow compare state & props (https://reactjs.org/docs/shallow-compare.html) so that the component only get re-render whend needed
cons:
I have to typing many parameters passing, like <UserItem userName={user.name} userEmail={user.email}> (unless you use spreading operator ...user, but you will pass all the object properties)
I cannot use object method inside the component. for example, my User model has a method user.totalMoneys(), because there're some lazy calculated properties in my object.
2)
pros:
passing looks simple: <UserItem user={user}>
can use object method inside UserItem
cons:
I cannot use the benefit of PureComponent, I have to compare my own,
I thing the best to go is the 1, i often see the spread operator used for this, btw you can do this in the parent to avoid passing useless props :
class UserParent extends Component {
const { uselessProp1, uselessProp2, ...usefullProps } = this.props;
render() {
return (
<View>
<UserItem {...usefullProps} />
</View>
);
}
}
How do I access $refs inside computed? It's always undefined the first time the computed property is run.
Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.
I had to trick Vue to update the computed property once the component was mounted.
Vue.component('my-component', {
data(){
return {
isMounted: false
}
},
computed:{
property(){
if(!this.isMounted)
return;
// this.$refs is available
}
},
mounted(){
this.isMounted = true;
}
})
I think it is important to quote the Vue js guide:
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
It is therefore not something you're supposed to do, although you can always hack your way around it.
If you need the $refs after an v-if you could use the updated() hook.
<div v-if="myProp"></div>
updated() {
if (!this.myProp) return;
/// this.$refs is available
},
I just came with this same problem and realized that this is the type of situation that computed properties will not work.
According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):
"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"
So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.
For example, in my case I have a button that need to be disabled when there is no selected row in my ref table.
So, this code will not work:
<button :disabled="!anySelected">Test</button>
computed: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
What you can do is replace the computed property to a method, and that should work properly:
<button :disabled="!anySelected()">Test</button>
methods: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
For others users like me that need just pass some data to prop, I used data instead of computed
Vue.component('my-component', {
data(){
return {
myProp: null
}
},
mounted(){
this.myProp= 'hello'
//$refs is available
// this.myProp is reactive, bind will work to property
}
})
Use property binding if you want. :disabled prop is reactive in this case
<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>
But to check two fields i found no other way as dummy method:
<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
{{data.submitButton.value}}
</button>
methods: {
checkIsValid(email, password) {
return email || password;
}
}
I was in a similar situation and I fixed it with:
data: () => {
return {
foo: null,
}, // data
And then you watch the variable:
watch: {
foo: function() {
if(this.$refs)
this.myVideo = this.$refs.webcam.$el;
return null;
},
} // watch
Notice the if that evaluates the existence of this.$refs and when it changes you get your data.
What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.
data() {
return {
childComps: [] // reference to child comps
}
},
methods: {
// method to populate the data array
getChildComponent() {
var listComps = [];
if (this.$refs && this.$refs.childComps) {
this.$refs.childComps.forEach(comp => {
listComps.push(comp);
});
}
return this.childComps = listComps;
}
},
mounted() {
// Populates only when it is mounted
this.getChildComponent();
},
computed: {
propBasedOnComps() {
var total = 0;
// reference not to $refs but to data childComps array
this.childComps.forEach(comp => {
total += comp.compPropOrMethod;
});
return total;
}
}
Another approach is to avoid $refs completely and just subscribe to events from the child component.
It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.
Parent component:
<script>
{
data() {
return {
childFoo: null,
}
}
}
</script>
<template>
<div>
<Child #foo="childFoo = $event" />
<!-- reacts to the child foo property -->
{{ childFoo }}
</div>
</template>
Child component:
{
data() {
const data = {
foo: null,
}
this.$emit('foo', data)
return data
},
emits: ['foo'],
methods: {
setFoo(foo) {
this.foo = foo
this.$emit('foo', foo)
}
}
}
<!-- template that calls setFoo e.g. on click -->