I'm trying to understand deep observability in MobX.
In particular, in the following code I'd like the autorun to be called every time I run setCommentCountForPost, but currently it isn't.
How should I fix this code? And, observable on a property of Post is enough to activate the autorun when I read the list in which the post is contained? (as I'm doing in the autorun)
I'm using MobX 5.
Edit: I discovered the code is working properly if I use the following call inside the autorun: console.log(toJS(p.getPosts()));.
This is interesting, but why, and how should I do if I only want to call getPosts()?
This is the code
import { useStrict, configure, autorun } from 'mobx';
import { toJS, observable, action, computed } from 'mobx';
configure({ enforceActions: true });
class Post {
#observable commentCount = 0;
setCommentCount(c) {
this.commentCount = c;
}
}
class PostList {
#observable _posts = {};
#action createPost(id) {
this._posts[id] = new Post();
}
#action setCommentCountForPost(id, c) {
this._posts[id].setCommentCount(c);
}
getPosts() {
return this._posts;
}
}
let p = new PostList();
p.createPost(1);
autorun(function test () {
console.log(p.getPosts());
});
p.setCommentCountForPost(1, 22);
MobX tracks property access, not value
in your example, the autorun function only tracking the _posts, but not the property of _posts, so if you change the _posts value the tracking function will worked
console.log(toJS(p.getPosts())) worked bacause of the toJS function in order to convert the observable value to normal value , it access the property of _posts.
if you hope the p.getPosts() worked, you should iteration access the property of _posts.
Related
I have gone through the definitions of the Pure and Impure Javascript functions in the ReactJs Official Docs.
Pure functions are ones that do not attempt to change their inputs, and always return the same result for the same inputs.
Example
function sum(a, b) {
return a + b;
}
Impure function is one that changes its own input.
Example
function withdraw(account, amount) {
account.total -= amount;
}
Now, can somebody tell me, how can I mistakenly make functions impure in React/Redux, where pure functions are required?
React and Redux both need pure functions coupled with immutability to run in a predictable fashion.
If you don't follow these two things, your app will have bugs, the most common being React/Redux not able to track changes and unable to re-render when your state/prop changes.
In terms of React, consider the following example:
let state = {
add: 0,
}
function render() {
//...
}
//pure function
function effects(state,action) {
//following immutability while updating state, not directly mutating the state.
if(action == 'addTen') {
return {...state, add: state.add + 10}
}
return state;
}
function shouldUpdate(s) {
if(s === state){
return false
}
return true
}
state = effects(state, 'addTen')if(shouldUpdate(state)) {
render();
}
The state is held by the state object which has only added property. This app renders the app property. It shouldn't always render the state when anything happens but should check whether a change occurred in the state object.
Like so, we have an effects function, a pure function which we use to affect our state. You see that it returns a new state when the state is to be changed and returns the same state when no modification is required.
We also have a shouldUpdate function which checks using the === operator whether the old state and the new state is the same.
To make mistakes in terms of React, you can actually do the following :
function effects(state,action) {
doRandom(); // effects should only be called for updating state.
// Doing any other stuff here would make effects impure.
if(action == 'addTen') {
return {...state, add: state.add + 10}
}
return state;
}
You can also make mistakes by setting the state directly and not using effects function.
function doMistake(newValue) {
this.state = newValue
}
The above should not be done and only effects function should be used to update the state.
In terms of React, we call effects as setState.
For Redux:
Redux's combineReducers utility checks for reference changes.
React-Redux's connect method generates components that check reference changes for both the root state and the return values from mapState functions to see if the wrapped component actually needs to re-render.
Time-travel debugging requires that reducer be pure functions with no side effects so that you can correctly jump between different states.
You can easily violate the above three by using impure functions as reducers.
Following is taken directly from redux docs:
It's called a reducer because it's the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue).
It's very important that the reducer stays pure. Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation.
Simply said the state cannot be mutated. A new instance of the state should be returned every time there is a change so
This code is not correct :
const initialStates = {
items: ['item1']
}
export const ItemMaster = (state = initialStates, action) => {
switch (action.type) {
case TYPES.ADD_ITEM:
{
state.items.push(action.item)
return state
}
default:
return state
}
}
This code when written as a pure function below, This returns a new instance of the array it does not modify the actual array itself. This is the reason you should use a library like immer to handle immutability
const initialStates = {
items: ['item1']
}
export const ItemMaster = (state = initialStates, action) => {
switch (action.type) {
case TYPES.ADD_ITEM:
{
state = {...state,items:state.items.concat(action.item)}
return state
}
default:
return state
}
}
You could make pure functions impure by adding API calls or writing codes that result in side effects.
Pure functions should always be on point and self-explanatory, and should not require you to refer 3 or 4 other functions to understand what's going on.
// Pure Function
function USDtoEUR(USD, todayRate) {
return USD * todayRate;
}
// Impure Function
function USDtoEUR(USD) {
const todayRate = getTodayRate();
return USD * todayRate;
}
In case of React / Redux
const mapState = async state => {
const { data } = await whatDoINeed()
let mappedState = {}
if (data.needDolphin) {
mappedState.dolphin = state.dolphin
}
if (data.needShark) {
mappedState.shark= state.shark
}
return mappedState;
}
// Or for Redux Reducer
// Bad
{
setData: (state, payload) => {
const set = whatToSet()
return {
...state,
set.dolphin ? ...{ dolphin: payload.dolphin } : ...{},
set.shark ? ...{ shark : payload.shark } : ...{},
}
}
}
// Good
{
setData: (state, payload) => {
return {
...state,
// Just send only the things need
// to be sent
...payload
}
}
}
This should not be done. Everything a connect function or reducer function needs must be supplied through argument or written within its function. It should never get from outside.
Maybe I have misunderstood what a getter is in Vuex, but say I have a getter that gets the size of a DOM element, a div for example. I would do something like this :
const getters = {
getContainerWidth (state) {
return document.getElementById('main-content').clientWidth;
}
}
Now when I start my app, all the getters seem to be run straight away. What if the div isn't available at startup? How do I rerun a getter?
I run the getter like this at the moment :
import store from '#/store'
store.getters['myModule/getContainerWidth']
I thought maybe this would work :
store.getters['myModule/getContainerWidth']()
But since store.getters is an object containing properties and values, and the values not being functions, I can't rerun them.
Any ideas?
Getters should depend on state field to be reactive. It you want to observe clientWidth changes - it does not work.
If you want to use it like function then just return function from getter:
const getters = {
getContainerWidth (state) {
return () => {
let container = document.getElementById('main-content');
return container ? container.clientWidth : 0
};
}
}
and use it like getContainerWidth()
I'm a begginer in React and would like to figure out how to modify values get using props.
f.e:
I have a MobX GameStore.tsx with #observable values:
export class GameStore {
#observable money = 0;
#observable CPS = 0;
#observable taskCodeLines = 0;
#observable taskCodeLinesTarget = 10;
...
#observable staffFrontEndCount = 4;
#observable staffFrontEndStartCost = 100;
#observable staffPHPCount = 2;
#observable staffPHPStartCost = 250;
}
Now I want to have a few StaffMember objects in Staff class:
render() {
return(
<div className="staff">
<ul className="staff-list">
<StaffMember job="Front End Developer" count={ gameStore.staffFrontEndCount } startCost = { gameStore.staffFrontEndStartCost } />
<StaffMember job="PHP Developer" count={ gameStore.staffPHPCount } startCost = { gameStore.staffPHPStartCost } />
</ul>
</div>
);
}
I pass down a data like name of this objects and some values. And now I want to modify some of them, like:
#observer
export default class StaffMember extends React.Component<any, any> {
#computed get increaseStaffCount() {
return this.props.count;
}
#action hireStaff() {
let cost = this.props.startCost * 1.4 * (this.props.count + 1);
if (gameStore.money >= cost) {
gameStore.money -= cost;
this.props.count += 1; // It's illegal because props data is read-only
this.countCPS();
}
}
How can I do this? Is this OK to create logic like above?
How should I create instances of classes in react and build a generic methods for them?
Thanks for help ;)
React does not allow the modification of props values over the course of a component's life. And there are currently two ways it has gotten around the need to change the value of props.
Load it into a state
Utilize Redux
On the first item, as xSkrappy said before, you can load the props into a Component's state, which can be updated over the course of a component's life, adding this method inside the Component in the following manner:
componentDidMount() {
this.setState({ count: this.props.count })
}
This creates a local state in the component that is equal to the prop value that was passed down to the component from its parent. And you can begin to change it from there.
You can also use the componentWillReceiveProps lifecycle method to re-render the component when the props value changes in its parent component, like such:
componentWillReceiveProps(nextProps) {
if(nextProps.count !== this.props.count) {
this.setState({ count: nextProps.count })
}
}
The second method involves utilizing Redux, a state container that can be used in React applications. Its pattern involves creating a store where the state of the entire application can be managed, and any given component can be connected to that store and receive that state as props.
While utilizing Redux is a lot more complex than the first option given, in the end you are given a lot more freedom because you can make your count value accessible to any component in your application!
Sadly implementing Redux is too lengthy a process to just detail in this answer, so I'll direct you to what I think is a good guide to refactoring your application to use Redux, should you wish to go with this option
The answer to that would be after passing the props inside StaffMember put it inside a state then from there you can modify the state :)
In ReactJs, Props are immutable so you can't modify it. Instead of using Props You can use State. State are mutable you can modify it. Or, you can use Redux concept as per your requirement.
For ex:- First make a state
this.state = {
usersList:[]
};
then you can add modification in your state like this
componentDidMount() {
this.setState({ usersList: this.props.count})
}
I have a model object representing "players" in DB. in it's implementation there is an array of players, which i would like to bind to from different VM's in my app. for example:
import {Players} from './models/players';
import {inject, BindingEngine} from 'aurelia-framework';
#inject(Players,BindingEngine)
export class App {
constructor(playersProvider,bindingEngine) {
this._playersProvider = playersProvider;
this._bindingEngine = bindingEngine;
this._subscription = this._bindingEngine.propertyObserver(this,this._playersCount)
.subscribe(this.objectValueChanged);
}
async activate() {
await this._playersProvider.initialize();
this._playersCount = this._playersProvider.players.length;
}
objectValueChanged(newVal,oldVal) {
console.log("new : " + newVal + ", old val : " + oldVal);
}
deactivate() {
this._subscription.dispose();
}
}
unfortunately, when a change is made to the players array (from other parts in the app) the change is not reflected in _playersCount property. e.g. - UI label bound to this property is not refreshed, and objectValueChanged never gets called.
U have the same issue in a different VM with a collectionObserver on the same array.
any help?
Have you tried to declare _playersCount in the constructor before subscribing to it?
Also the synthax does not seem correct, it should be according to this article:
import {BindingEngine, inject} from 'aurelia-framework';
#inject(BindingEngine)
class MyClass {
constructor(bindingEngine) {
this.bindingEngine = bindingEngine;
this.observeMe = 'myvalue'; // the property is first initialized in the constructor
let subscription = this.bindingEngine
.propertyObserver(this, 'observeMe') // <= you wrote this._bindingEngine.propertyObserver(this,this.observeMe)
.subscribe(this.objectValueChanged);
// Dispose of observer when you are done via: subscription.dispose();
}
objectValueChanged(newValue, oldValue) {
console.log(`observeMe value changed from: ${oldValue} to:${newValue}`);
}
}
The async keyword might affect the behaviour.
If it still does not work, you can use an event aggregator to broadcast the change.
I'm using React in combination with MobX. I use a store with an observable array (conversations) and would like to offer a sorted version of this array as a computed property. When adding a new conversation, the computed property sortedConversations is evaluated before the conversation is added to the array. In the small example below, 'Reordering conversations' is always logged before 'Added conversation'. Am I doing something wrong?
class Store {
...
#observable conversations = [];
addConversation(conversation) {
this.conversations.push(conversation);
console.log('Added conversation');
}
#computed
get sortedConversations() {
console.log('Reordering conversations');
return _.orderBy(this.conversations.slice(), ['lastUpdated'], ['asc']);
}
}
You are not doing anything wrong. The MobX API looks like regular JavaScript, but every time an observable is updated, all its observers are updated synchronously under the hood. This will not be an issue in this case, but you could wrap the contents of addConversation in a transaction:
addConversation(conversation) {
transaction(() => {
this.conversations.push(conversation);
console.log('Added conversation');
});
}
You could also make the addConversation into an action which also is a transaction:
#action
addConversation(conversation) {
this.conversations.push(conversation);
console.log('Added conversation');
}