-
-
Notifications
You must be signed in to change notification settings - Fork 350
Conversation
Computed, which are depend on component props, should be recalculated only if used props are changed, or new prop appears, or old prop disappears Test added
Achieve granular observale state/props Technique: Instead of having only 1 atom for whole props/state, we have N atoms for each property/state element, and extra one which track keys set change When new props object set, we detect key change and report changed for extra atom. Also we instrument props/state object and in-place change properties to getter+setter, which uses atoms and single value storage. Also, previous props/state object should be freezed with it values and de-instrumented from getters and setters, to get lifecycle methods work. Having extra atom, which detects keys change helps us to detect new props/state element appears.
Remove unused variable
Hmm, seems we can optimize things, if report to keysAtom only on new key appears, but not on old rey removed. |
Found solution without extra component wrapper. Done with clone-if-frozen technique, and this plays well with both state and props
Found solution without extra component wrapper. We can make further optimization, by tracking by extra atom only new keys, not removing, but this requires more work, and I don't know worth it is or not. I think that there could be done some optimizations in code which make storage observable/static. And, this code could be easily adopted to Proxy in future. I will test this patch on my project and give some feedback in next week. |
src/observer.js
Outdated
individualAtoms[key].reportObserved() | ||
return individualValues[key] | ||
}, | ||
set: function set(v) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can make property read-only (by defining only get
), this can help to make props read-only too (BC break with current react behaviour)
Hey @Strate , thanks! Will try to review in a few days |
Caveat: if we use reaction like this:
it will not react as before, it will only react on keys changed (BC break here) |
@Strate that works for normal observable objects neither, so I think that should be ok |
src/observer.js
Outdated
* In-place convert properties to getter+setter | ||
*/ | ||
function convertStorageToReactive(storage) { | ||
if (null == storage) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just if (!storage)
?
src/observer.js
Outdated
// we can do it safely inside componentWillMount (react won't warn us about it) | ||
// btw, state object can not be cloned here, (react warns about direct state modification), however | ||
// state object is not frozen, so we do not need to clone it to instrument | ||
if (Object.isFrozen && Object.isFrozen(storage)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, if we implemented this using Proxies, it would all be lot simpler I guess? I think that could be fine as well, only introduce this feature in the next mobx-react that accompanies MobX 5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunatelly, we can't wrap state to proxy here, because react will check this and warn (proxied object !== original object). But for frozen props, of course, it could be applied.
src/observer.js
Outdated
individualAtoms[key] = new Atom("this." + propName + "." + key) | ||
} | ||
const currentKeyValue = storage[key] | ||
delete storage[key] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip the delete, will probably make it slower, without adding value as it is immediately redefined
src/observer.js
Outdated
} | ||
Object.keys(storage).forEach(function(key) { | ||
if (!(key in individualAtoms)) { | ||
individualAtoms[key] = new Atom("this." + propName + "." + key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be simpler to just create a collection of observable.box
-es? That combines an atom with a value holder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, of course boxes would be better
src/observer.js
Outdated
* In-place convert getter+setter to plain property | ||
*/ | ||
function convertStorageToStatic(storage) { | ||
if (null == storage) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!storage
Yes, but right now it works differently for props & state, and this would be BC break. It hink we can just note this in UPGRADE guide |
src/observer.js
Outdated
currentValue = newValue | ||
if ( | ||
currentValueKeys.length !== newKeys.length || | ||
newKeys.filter(key => currentValueKeys.indexOf(key) >= 0).length !== |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use shallowEqual, its already somewhere in the package I think
Hey @Strate, were you going to process the above comments? If you could rebase it on the Edit: |
Hey @mweststrate |
Fix tests after merge with master
moved from atom + value to observable.box reused shallowEqual helper simplified empty storage checks
@mweststrate merged with master, also processed all above comments. |
Maybe it would be better to move for new mobx4 object api? 🤔 |
or maybe give the observer decorator an option param and let the user choose the behavior? |
@xaviergonz for what reason? |
oh, if you don't want to break backwards compatibility, or offer it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this work if you add props dynamically? this is, on the first render props has only one key and then in the future props has 3 keys for that same component
@xaviergonz yes, it works. There is a test for that case. |
@Strate awesome, I can't wait to see it on mobx itself 👍 |
Unfortunatelly, this PR is broken now, because of #496 |
So, we have a problem here. To get this PR work, I need to instrument props passed to component, but I can't do this because react warns that props are modified. I'm not sure that it is possible at all. |
can you pass to the constructor a modified props?
…On Fri, Jun 8, 2018, 12:19 Artur Eshenbrener ***@***.***> wrote:
So, we have a problem here. To get this PR work, I need to instrument
props passed to component, but I can't do this because react warns that
props are modified. I'm not sure that it is possible at all.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#436 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AGA77F5N8PILAXPubzptKpyC41FXxLsJks5t6k_DgaJpZM4Skjo8>
.
|
@xaviergonz I tried that solution for a while, but some tests still failing. And this solution need an extra wrapper for a component (which is performance gain) |
So finally, is PR ‘Achieve granular observale state/props’ implemented in newest release now? |
Is this still alive? Is the React warning a total roadblock? |
@bsmith-cycorp @hong1997 this PR is still pending, because of that warning. That warning could not be just ignored, because this can break some stuff (and it definetely break, because tests are failing in this PR) |
I do something like this to get reactive props import { extendObservable, isObservableProp, observable, reaction } from "mobx";
import { disposeOnUnmount } from "mobx-react";
import * as React from "react";
export abstract class ObsComponent<P = {}, S = {}> extends React.Component<P, S> {
@observable.shallow
obsProps = this.props;
constructor(props: P, context?: any) {
super(props, context);
const obsProps: any = this.obsProps;
disposeOnUnmount(
this,
reaction(
() => this.props,
(unobsProps: any) => {
const extend: any = {};
let extended = false;
for (const propName of Object.keys(unobsProps)) {
if (isObservableProp(obsProps, propName)) {
obsProps[propName] = unobsProps[propName];
} else {
extended = true;
extend[propName] = unobsProps[propName];
}
}
if (extended) {
extendObservable(obsProps, extend, undefined, { deep: false });
}
},
{
fireImmediately: true
}
)
);
}
} Then I extends from ObsComponent instead of Component and use reactions over this.obsProps (or use it on the render to get less useless renders) rather than this.props Although now that I think about it it would probably be better if it was transformed into a function like
I can make such function (plus another for state) and add it to a PR if somebody is interested |
The good thing of that approach is that it is not gonna break anything :) |
@xaviergonz yes, some kind like your solution implemented here, but with |
are you sure? it uses extends Observable when new properties are detected in props |
@xaviergonz yes, it extends observable, and if you will read prop after extendObservable, everything gonna be okay. Imagine this case:
Component initially renders as If you get rid of computed in that case everything gonna to be oka, because react re-renders component if new props come. |
Hmm, could be achieved by proxies (but I guess that's the same mobx5 is doing internally and that's why it works) @mweststrate is there a way to mark an observed property as "dirty" or "invalidate" it? On the worst case scenario I guess it is better to provide those methods for mobx5 users than not to provide them at all? |
@xaviergonz in this PR I solved that kind of problem by adding extra atom, which tracks keys change. You can check this PR for implementation details. |
And yes, there is many ways to achieve granular props, easies is use
but main goal of this PR is to get optimized behavior out-of-the-box, without need to rewrite whole app. |
Closing, as this gets less, not more relevant, with the direction hooks are taking |
@mweststrate could you please share some more info why this is no more relevant? |
For those landing on this ticket, here's another workaround: function createReactiveProps<T>(componentInstance: { props: T }): T {
let props = observable({});
Object.assign(props, componentInstance.props)
autorun(() => Object.assign(props, componentInstance.props))
return props;
} Usage: @observer class MyComponent extends React.Component<Props> {
@observable reactiveProps = createReactiveProps(this);
... |
@bsmith-cycorp looks like your workaround would work only with mobx@5, but mobx@4. Unfortunatelly, there still exists some runtimes where mobx@4 actual (react-native & old ios devices). And your solution includes a leak (non-disposed autorun) |
Particular fix for #380 #347
Achieve granular observale state/props
Technique:
Instead of having only 1 atom for whole props/state, we have N atoms for each property/state element, and extra one which track keyset change
When new props object set, we detect key change and report changed for extra atom. Also we instrument props/state object and in-place change properties to getter+setter, which uses atoms and single value storage. Also, previous props/state object should be freezed with it values and de-instrumented from getters and setters, to get lifecycle methods work.
Having extra atom, which detects keys change helps us to detect new props/state element appears.
Unfortunatelly, to make in-place instrument with props/state object, I forced to make a extra wrapper for Observer component, which calls tocreateElement
and un-freeze it props. This is definetely not good for performance, butReact.createElement
freeze objects :( I think this extra wrapper get this issue back: mobxjs/mobx#405 (and many tests are failed because of this)UPD: solution found
Comments are requested.