The following code is intended to print a reversed list of users as soon as a new user is added, but it doesn't work. The autorun is listening to a lazy calculated var (_userArrayRev), but how to enable the recalculation of that var? The autorun is executed only once, while I expect it to be run three times
And, why does MobX allow me to modify the observable userArray var in AddUser() when enforceactions (useStrict) is set to true?
import { useStrict, configure, autorun } from 'mobx';
import { observable, action, computed } from 'mobx';
configure({ enforceActions: true });
class Test {
#observable _userArray = [];
#observable _userArrayRev = undefined;
userCount = 0;
addUser() {
console.log("Adduser");
this._userArray.push('user' + this.userCount);
this.invalidateCache();
}
// returns reversed array
getUsersArrayRev() {
if (this._userArrayRev == undefined) {
// console.log("recalculating userArray");
// TODO: should be reversed
this._userArrayRev = this._userArray;
}
return this._userArrayRev;
}
invalidateCache() {
this._usersArrayRev = undefined;
}
}
var t = new Test();
autorun(function test () {
console.log("users: ", t.getUsersArrayRev());
});
t.addUser();
t.addUser();
I recommend you use computed instead of autorun. computed is more suitable in the case when you want to create readonly lazy variable based on observable objects.
Notice: I use slice() to return a normal array. Observable array is an object rather than an array, be careful of that.
import React from 'react';
import { render } from 'react-dom';
import { observer } from 'mobx-react';
import { observable, action, computed } from 'mobx';
class Test {
#observable _userArray = [];
#computed get userCount() {
return this._userArray.length;
}
#computed get usersArrayRev() {
return this._userArray.slice().reverse();
}
#action
addUser() {
console.log("Adduser");
const id = this.userCount + 1;
this._userArray.push(`user${id}`);
console.log("Reversed users: ", this.usersArrayRev);
}
}
#observer
class App extends React.Component {
t = new Test();
render() {
return (
<div>
{this.t.usersArrayRev.map(user => <div key={user}>{user}</div>)}
<button onClick={() => { this.t.addUser(); }}>Add user</button>
</div>
);
}
}
Code demo here:
Related
I have this situation but I don't know if it's ok to do something like this.
If I save an object inside the state of my component, can the object modify itself without using setState?
File A.js
export default class A {
constructor(value) {
this.value = value;
}
setValue(value) {
this.value = value;
}
}
File B_Component.js
import React from "react";
import A from "./A";
class B_Component extends React.Component {
constructor(props) {
super(props);
this.state = {
aObj = new A("foo")
}
}
bar = () => {
this.state.aObj.setValue("bar");
}
...render etc...
}
Basically this should modify the state of the component without using setState.
Is this correct or there may be problems?
Is this correct or there may be problems?
There may be problems. :-) If you're rendering that object's value, then you're breaking one of the fundamental rules of React, which is that you can't directly modify state. If you do, the component won't re-render.
Instead, you create a new object and save the new object in state:
bar = () => {
this.setState({aObj: new A("bar")});
}
Or if you want to reuse other aspects of the object and only change value:
bar = () => {
const aObj = new A(this.state.aObj); // Where this constructor copies all the
// relevant properties
aObj.setValue("bar");
this.setState({aObj});
}
The A constructor would be
constructor(other) {
this.value = other.value;
// ...repeat for any other properties...
}
Or you might give A a new function, withValue, that creates a new A instance with an updated value:
class A {
constructor(value) {
this.value = value;
// ...other stuff...
}
withValue(value) {
const a = new A(value);
// ...copy any other stuff to `a`...
return a;
}
}
Then:
bar = () => {
this.setState({aObj: this.state.aObj.withValue("bar")});
}
I have repetitive code that I do not know how to make DRY ( Don't Repeat Yourself ).
Here are two components "talking" via dispatch() and React's auto re-render.
this.map is repeated twice.
This module will dispatch actions on a click.
import React from 'react';
import { connect } from 'react-redux';
class Icon extends React.Component {
constructor(props) {
super(props);
this.map = {
paper: 'bg_paper.jpg',
light_wood: 'bg_wood.jpg',
graph: 'bg_graph.jpg'
};
}
flip () {
this.props.dispatch({type: 'updateIcon', bg_key: $A.nextKey(this.map, this.props.state.bg_key)});
}
render () {
const style = {
// ... snip
}
return (
<img id = 'bar_icon' onClick={this.flip.bind(this)} style={style} src='_images/sv_favicon.svg'/>
)
}
}
const mapStateToProps = state => {
return {
state: state.Icon
}
}
export default connect(mapStateToProps)(Icon);
while this component will auto re-render. It all works fine. I just want to make it DRY.
import React from 'react';
import { connect } from 'react-redux';
// ... snip
class FrameBody extends React.Component {
constructor(props) {
super(props);
this.map = {
paper: 'bg_paper.jpg',
light_wood: 'bg_wood.jpg',
graph: 'bg_graph.jpg'
};
}
render () {
const style = {
backgroundImage: 'url(' + '_images/' + this.map[this.props.state.bg_key] + ')'
};
return (
<div id='contents' style={style}>
</div>
)
}
}
const mapStateToProps = state => {
return {
state: state.Icon
}
}
export default connect(mapStateToProps)(FrameBody);
What can I do so that there are not two instances of this.map?
You can extract the logic of this.map out to a class function.
getBackgroundImageKey = () => {
const backgroundMap = {
paper: 'bg_paper.jpg',
light_wood: 'bg_wood.jpg',
graph: 'bg_graph.jpg'
}
return backgroundMap[this.props.bg_key]
}
Take a step further and add another function to return the URL and add string interpolation.
getBackgroundImageURL(){
const backgroundMap = {
paper: 'bg_paper.jpg',
light_wood: 'bg_wood.jpg',
graph: 'bg_graph.jpg'
}
return `url(_images/${backgroundMap[this.props.bg_key]})`;
}
Which will let you define the style like this
const backgroundImage = this.getBackgroundImageURL()
const style = { backgroundImage };
Well since you're already using Redux and dispatching an action to flip, why don't you move that logic there?
Keep the current image in the store so you can get it in connect, make your flip action creator a thunk that holds that "map" and decides what's the next image.
Instead of DRYness, your code lacks separation of concerns. The switch/Icon UI component would be much more reusable and terse if it only called a prop whenever the user clicks "flips". Connect this onFlip to the action creator I mentioned and you have the logic in one place, and the UI to interact in another.
following the Flux pattern I'm trying to update my component and pass some values (a string and a boolean in this specific case) via the store.
I could not find any non-hacky way to solve this yet i.e. using global vars in the Store and use a getter function in the Store which is called from the component on ComponentWillMount(), not a nice solution.
Here's a stripped down code example to show what im trying to achieve:
ExampleStore.js
import AppDispatcher from '../appDispatcher.jsx';
var displayimportError = false;
var importedID = '';
import axios from 'axios';
class ExampleStore extends EventEmitter {
constructor() {
super();
}
importId(id) {
let self = this;
// fetch data from BE
axios.get('foo.com').then(function(response) {
if (response.data && response.data.favoriteEntries) {
displayimportError = false;
}
self.emitChange();
}).catch(function(error) {
console.log(error);
displayimportError = true;
importedID = id;
self.emitChange();
// now update component and pass displayimportError and
// importedID.
// best would to component.receiveUpdateFromStore(param); but
// it's giving receiveUpdateFromStore is not function back
});
}
}
var favObj = new ExampleStore();
AppDispatcher.register(function(payload) {
var action = payload.action;
switch (action.actionType) {
case 'UPDATE_ID':
favObj.importId(action.data);
break;
}
return true;
});
export default favObj;
As mentioned in the Comment above the best solution in my eyes so far would be to call a function in the component from the store i.e component.receiveUpdateFromStore(param); and then update the component state within that function but even though they seem to be im/exported correctly to me it is returning receiveUpdateFromStore is undefined.
Any other idea how to solve this is appreciated.
//example component
import React from 'react';
import ReactDom from 'react-dom';
import ExampleStore from '../stores/ExampleStore.jsx';
class ExampleComponent extends React.Component {
constructor(props) {
super(props);
}
receiveUpdateFromStore(param) {
this.setState({'exampleText': param.text, 'exampleBoolean': param.bool});
}
render() {
return <div className="foo">bar</div;
}
}
export default ExampleComponent;
Any idea how to pass data from store to a component and update component state in a nice way?
I would hang your store state on the store class instance itself -- something like this.state.displayimportError = true -- and then have the component subscribe to the store:
import React from 'react';
import ExampleStore from '../stores/ExampleStore.jsx';
class ExampleComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
importError: ExampleStore.state.displayimportError,
};
}
componentWillMount() {
ExampleStore.on( 'change', this.updateState );
}
componentWillUnmount() {
ExampleStore.removeListener( 'change', this.updateState );
}
updateState = () => {
this.setState( state => ({
importError: ExampleStore.state.displayimportError,
})
}
render() {
return <div>{ this.state.importError }</div>
}
}
NOTE: Above code untested, and also using class properties/methods for binding updateState.
Here is the code -- pretty sure it is something about extendObservable that I just don't get, but been staring at it for quite a while now. When addSimpleProperty runs, it seems to update the object, but it doesn't trigger a render.
const {observable, action, extendObservable} = mobx;
const {observer} = mobxReact;
const {Component} = React;
class TestStore {
#observable mySimpleObject = {};
#action addSimpleProperty = (value) => {
extendObservable(this.mySimpleObject, {newProp: value});
}
}
#observer
class MyView extends Component {
constructor(props) {
super(props);
this.handleAddSimpleProperty = this.handleAddSimpleProperty.bind(this);
}
handleAddSimpleProperty(e) {
this.props.myStore.addSimpleProperty("newpropertyvalue");
}
render() {
var simpleObjectString =JSON.stringify(this.props.myStore.mySimpleObject);
return (<div>
<h3> Simple Object</h3>
{simpleObjectString}
<br/>
<button onClick={this.handleAddSimpleProperty}>Add Simple Property</button>
</div>);
}
}
const store = new TestStore();
ReactDOM.render(<MyView myStore={store} />, document.getElementById('mount'));
store.mySimpleObject = {prop1: "property1", prop2: "property2"};
This problem is brought up in the Common pitfalls & best practices section of the documentation:
MobX observable objects do not detect or react to property assignments
that weren't declared observable before. So MobX observable objects
act as records with predefined keys. You can use
extendObservable(target, props) to introduce new observable
properties to an object. However object iterators like for .. in or
Object.keys() won't react to this automatically. If you need a
dynamically keyed object, for example to store users by id, create
observable _map_s using
observable.map.
So instead of using extendObservable on an observable object, you could just add a new key to an observable map.
Example
const {observable, action} = mobx;
const {observer} = mobxReact;
const {Component} = React;
class TestStore {
mySimpleObject = observable.map({});
#action addSimpleProperty = (value) => {
this.mySimpleObject.set(value, {newProp: value});
}
}
#observer
class MyView extends Component {
constructor(props) {
super(props);
this.handleAddSimpleProperty = this.handleAddSimpleProperty.bind(this);
}
handleAddSimpleProperty(e) {
this.props.myStore.addSimpleProperty("newpropertyvalue");
}
render() {
var simpleObjectString = this.props.myStore.mySimpleObject.values();
return (
<div>
<h3> Simple Object</h3>
{simpleObjectString.map(e => e.newProp)}
<br/>
<button onClick={this.handleAddSimpleProperty}>Add Simple Property</button>
</div>
);
}
}
const store = new TestStore();
ReactDOM.render(<MyView myStore={store} />, document.getElementById('mount'));
Recently I started using mobx with react and mobx-react library.
I want to use functional React components to create my views.
I'd like to create a helper function, that takes selector function and Component, calls inject (with selector function as parameter) and observe on that Component - effectively connecting this component to mobx-react store (taken from Provider context) and providing only needed properties for this Component.
But I can't get it to work. Action is being dispatched, but views doesn't react to this change (store attributes does change, but Component doesn't react to this change).
Here's my helper function:
import { observer, inject } from 'mobx-react';
export function connect(selectorFunction, component) {
return inject(selectorFunction)(observer(component));
}
here's my Component:
import React from 'react';
import { connect } from 'utils';
const selector = (stores) => {
return {
counter: stores.counterStore.counter,
double: stores.counterStore.double,
increment: stores.counterStore.increment
};
};
const Counter = ({ counter, double, increment }) => {
return (
<div className="counter">
<p>{ counter }</p>
<p className="double">{ double }</p>
<button onClick={increment}>+1</button>
</div>
);
};
export default connect(selector, Counter);
and here's my store:
import { observable, computed, action } from 'mobx';
export default class Counter {
#observable counter = 0;
#action
increment = () => {
this.counter++;
}
#computed
get double() {
return this.counter * 2;
}
}
(Not showing Provider and other simple stuff, but it is set up properly).
Thanks! Every answer is much appreciated.
Looking at Mobx's documentation, it looks like your selector is doing things a bit wrong. It should return an object with stores, and not an object with values from stores. Try returning the actual counterStore instead:
const selector = (stores) => {
return {
counterStore: stores.counterStore
};
};
And use it like this in your component:
const Counter = ({ counterStore: { counter, double, increment } }) => {
return (
<div className="counter">
<p>{ counter }</p>
<p className="double">{ double }</p>
<button onClick={increment}>+1</button>
</div>
);
};