Replies: 25 comments
-
I don't think it's actually possible to access props with a class store or at least I don't know-how. The problem is that the class store doesn't share closure with the component. What you can do is utilize const Comp = ({ propA, propB }) => {
const source = useAsObservableSource({ propA, propB })
const [store] = React.useState(() => new ClazzStore(source))
} class ClazzStore {
constructor(source) {
this.source = source
}
@computed
get something() {
return `${this.source.propA} / ${this.source.propB}`
}
} Fairly verbose if you ask me, but I don't think there is some other "magical" way. The const Comp = ({ propA, propB }) => {
const store = useLocalStore((source) => ({
get something() {
return `${source.propA} / ${source.propB}`
}
}, { propA, propB }) // useAsObservableSource is baked in
} Frankly, I don't know why is everyone so hyped about class stores 😆 |
Beta Was this translation helpful? Give feedback.
-
Because contrary to current popular belief, classes are a fine choice for this. Maybe something like this? interface BaseStore<T> {
props:T;
}
function useClassStore<T, U extends BaseStore<T>>(Klass: {new(t:T):U}, props: T) {
let source = useAsObservableSource(props);
let [state] = useState(() => new Klass(source))
return state;
} I'm assuming that the contents of the source reference returned on the first render will be updated every time useAsObservableSource is called? With hooks, you can never tell what the exact semantics are... |
Beta Was this translation helpful? Give feedback.
-
Yea I suppose such hook will work. If you are asking about including such a hook to the library, I am don't think it's a good idea as it's very opinionated. At the moment you have a store that has a different constructor argument, you cannot use that. That can be important in case of dependencies on other stores. Besides, it's just 3 lines of code, no special handling or logic. Anybody who needs that can just copy that.
You are assuming right. With hooks, you can't tell much, but with MobX it's very important to have a stable reference so it's safe to assume you get that.
Would love to learn why. They are just more verbose in my eyes and require special handling due to decorators. I don't see any real benefit except it's defined "outside of the component". But you can as well have a custom hook for each store that utilizes |
Beta Was this translation helpful? Give feedback.
-
You can also simply update whats necessary every render: const [store] = React.useState(() => new Store())
store.setDep1(props.dep1);
store.setDep2(props.dep2); |
Beta Was this translation helpful? Give feedback.
-
@urugator That sounds easier to you? :) We have MobX here and relying on imperative updates sounds fairly awkward. Besides, such operation is a side effect and as such should be in |
Beta Was this translation helpful? Give feedback.
-
@FredyC It works exactly the same as const [source] = React.useState(() => observable({}))
runInAction(() => {
Object.assign(source, props);
}); It's impossible to get "reactive props" without imperative side effect (copying props to observables). We can do this because Mobx is synchronous, so once you call the setter (or |
Beta Was this translation helpful? Give feedback.
-
Yea, but isn't also against "don't change state during a render"? Moreover that MobX is a synchronous and changing state in the middle of rendering sounds like something must go wrong. And yet it obviously works, at least now, but I wonder if that will still work in Concurrent mode 🤔
The awkwardness in my eyes comes from the fact that you must adapt your store (and do it for each one) to accept some outside values. In the case of |
Beta Was this translation helpful? Give feedback.
-
It's not kosher and hardly something to recommend.
I am not advocating use of classes for local component state (and neither |
Beta Was this translation helpful? Give feedback.
-
I am using MobX local store merely for its convenience as I don't need to bother with listing deps in effects and callbacks. That's a major selling point for me. I am not sure what "complications" you are mentioning. On the contrary, things are complicated with immutable state. |
Beta Was this translation helpful? Give feedback.
-
You need to bother with listing deps to
I have no issues with |
Beta Was this translation helpful? Give feedback.
-
The only reason I'm considering this (mechanical) refactor is because the "new" React is the only one getting new features (for example, there is no ability to access multiple contexts from the old react, while useContext can be invoked multiple times). I'm absolutely fine with continuing to use classes otherwise; in addition, the new state management hooks provide significantly worse experience compared to MobX so I don't think they're an option. For reference, the fully mechanical refactor is as follows: Assuming the component MyComponent
There is however the issue of double-rendering: https://codesandbox.io/s/aged-snow-5u146 Try changing the props of the component by incrementing "s.a"; two renders will be triggered, one by react due to a change in props, another due to useObserver being triggered by that change I'm not sure how to avoid that one. By the time we get new props, we're already in a render cycle, so we can't really avoid one. In fact even the React docs recognize that this is not possible using hooks: https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-shouldcomponentupdate so it seems a more elaborate wrapper will be necessary to get the old functionality. |
Beta Was this translation helpful? Give feedback.
-
@urugator the reason we use local observable and computed instead of local component state and "useMemo" is because they are an excellent fit. Typically components will receive a somewhat complex MobX model objects via props (for example a document item in a list of document items); however they need better refined / transformed data (a view if you will) suitable for presentation (defined via computeds, for example compute item summary string name from several item props) with additional stateful options of that particular view component (observables, e.g. whether the item details are expanded or not) for that view that can be modified (via actions like expandDetailedView triggered by a button) Using observable and computed ensure that changes in the model will result with minimal and efficient recomputation of the view data; if the view consists of multiple pieces, it can also ensure that the smallest amount of DOM nodes re-render and update. useMemo and friends are absolutely inadequate and underpowered in comparison - they are not worth even considering. |
Beta Was this translation helpful? Give feedback.
-
Yes this is disadvantage of hooks, there is no way to bailout (I wonder whether we will use generators one day, yielding the effects instead of calling hooks).
I used a HOC like:
Just keep in mind that reactive props is nothing but a state change inside derivation: class PseudoComponent {
@observable local;
@observable props;
@computed a() {
this.local + this.props.x;
}
this.render = computed(props => {
// changing state, breaking one-way data flow
runInAction(() => {
this.props = props;
})
return this.a;
}, {
equals: diffVdom,
})
} By moving |
Beta Was this translation helpful? Give feedback.
-
So far, hooks have been nothing but a disadvantage :) I have a feeling that they might end up being a huge failure: they are very easy to write but very hard to read (and especially to understand the semantics of what runs when) which seems to me like a recipe for disaster.
In TypeScript classes are the only construct that at the same time represent a type and a runtime object. This gives them many advantages, one of which is that they can enable easy, type-safe dependency injection by representing both the interface, the implementation and the DI token (e.g. you could supply them automatically via emitDecoratorMetadata by reading design:type or design:paramtypes which only work for classes) They can also just replace interfaces + type-safe DI tokens without the implementation: class ActuallyAnInterface {
field1!: string;
field2!: stirng;
methodReturningString!: () => string;
} The "easymode" classes are still the most important ones, because for most of them you don't need to separate the interface from the implementation until needed. In our "framework", through react context, we access the injector, which provides the application singletons as well as component level transients: hfour/h4bff#57 You can read more about the framework here: https://hfour.github.io/h4bff/#/ although the building blocks and the docs on how they fit together are not fully complete yet. |
Beta Was this translation helpful? Give feedback.
-
So I think I managed to avoid the double-updating issue by making the props mutable and turning off the staleness notification for the reaction temporarily while assigning the new props: https://codesandbox.io/s/trying-to-use-hooks-794py This is absolutely fine, since the reaction is already "scheduled to run" - we're re-rendering the entire component after all. Not yet sure whether its a good idea to pass on the observable props directly - I think it might be better to keep react props object semantics but implement another hook called edit: props are now immutable; useObservableProps hook provides a MobX observable reference of them which is already created by |
Beta Was this translation helpful? Give feedback.
-
@urugator & @FredyC I believe the above technique separates the "props receiving" phase from the "computing the VDOM" phase nicely. When the props change, mobx gets out of the way since the render is going to happen anyway and there is no need for reactions; in all other cases, mobx takes care of re-renders. If I had access to mobx internals I would describe this as "reaction is already scheduled to run" but without that bit disabling the event was my best idea.I believe this implementation mirrors the original mobx-react API and behavior more closely. Do you think there is a place for |
Beta Was this translation helpful? Give feedback.
-
@spion I am kinda failing to follow it here and what exactly are you proposing. How is it different from |
Beta Was this translation helpful? Give feedback.
-
Its different in that the component wrapper itself runs the hook to create an observable version of the props, and the useObservableProps only provides the mutable object that was already created by the wrapper. So you cannot use a different observable source. What it does give you, however (that useLocalState doesn't) is the ability to prevent double-rendering. With It has to be this way because the reaction needs to be disabled during the props update, and the props update needs to happen outside of the tracking region of the reaction otherwise disabling doesn't work I tried implementing useAsObservableSource that temporarily disables the reaction, however if that's done during the tracking stage it messes up the state of the reaction: https://codesandbox.io/s/trying-to-use-hooks-n469q Click on a+=1 to update the props of the inner component, then on counter += 1. The second click will not trigger a render of Add. If you do the two steps again, counter+=1 will trigger a re-render the second time. Not sure why this happens (probably because its lowestNewObservingDerivationState gets messed up during tracking so onBecomeStale doesn't get called next time) |
Beta Was this translation helpful? Give feedback.
-
Can you draft how would the let currentProps: any = null;
function useObservableProps<T>(_props?: T): T {
if (currentProps != null) return currentProps;
else throw new Error('useObservableProps not available for non-observer component');
} And btw, the I seem to have lost what is all the reason for it. Some kind of micro-optimization to avoid one extra re-render? I thought we were talking about props in class components in here :) |
Beta Was this translation helpful? Give feedback.
-
Its not a micro-optimization if every single prop update causes two calls to the render function, is it? I'm trying to write a hook version that gets similar behavior to the class based implementation of mobx-react; that includes:
|
Beta Was this translation helpful? Give feedback.
-
@FredyC The global variable cannot be in an incorrect state, since its not possible for concurrent react to interrupt the section where its available or run it in parallel with other sections: currentProps = props; // this is where the global variable becomes available
reaction.current.track(() => {
try {
rendering = fn(p);
} catch (e) {
exception = e;
}
});
currentProps = null; // this is where its no longer available Concurrent react can only postpone the full rendering of child components, it cannot interrupt a standard function call. BTW thanks for pointing me to the updated code - I'll mess around with it and see if I can get it to do what I need :) |
Beta Was this translation helpful? Give feedback.
-
You know, I am fairly convinced that with MobX it doesn't happen that often to have props changing like crazy. Usually, you would be passing some stable store there. Sure, couple props might be needed to change from time to time, but imo it's not that often as it might seem. And if a component is really that convoluted to have several sources of re-render, it might be worth to rethink that and split up.
Doesn't it also mean it can render several components at a time? I haven't really examined it that much, but "concurrent" sounds like "parallel" to me. I think that automatically wrapping all props to observable might be overkill. It would be done no matter if you actually use those props. As such I don't think it should be the default behavior, but opt-in. There is a hidden limitation. The Also, I am kinda confused. In The name Lastly, it feels somewhat opinionated. Personally, I am using I am not saying it cannot become part of |
Beta Was this translation helpful? Give feedback.
-
Yeah that part is suboptimal :( The issue is you need to either pass the props or the type of the props - otherwise typescript wont know what type of object to return. Ordering rules don't apply for this "non-hook" so its not quite a hook, agreed. I thought it would be less confusing if it was presented as a hook My first version was nicer because it simply presented a mutable, observable props object as the argument so there was no need of useObservableProps. Its quite clean, but potentially a huge change of behavior if the props object is passed as a whole anywhere, and it violates the "frozen-in-time snapshot" concept that hooks try to present. I don't necessarily expect this to become a part of mobx-react or mobx-react-lite as it is - I wanted to bounce off a few ideas about observable props and class based stores and see if I can get help coming up with some API that approximates the class API but also makes sense in a hooks context. Its not easy! To be honest I am surprised to hear that people aren't using mobx state in class components - was everyone using setState instead? Seems like such a natural extension for UI state like e.g. filter toggles, dropdown expansion, currently selected items etc... is everyone keeping that state outside the components too? |
Beta Was this translation helpful? Give feedback.
-
The setState isn't inherently bad for simple stuff really. The class components are a bigger burden to treat that state properly imo. Duplicating code, no easy way to share common stuff, forgetting to clean up and a bunch of others. Personally, I still sometimes opt for I never liked decorators in classes and in the essence classes alone. It just did not feel right. So I was rather using |
Beta Was this translation helpful? Give feedback.
-
I think we have exhausted the topic here with loads of useful information. For further questions, please open a new issue. |
Beta Was this translation helpful? Give feedback.
-
the issue is that (for the mechanical refactor to hooks) the class store needs access to an observable version of the props that autoupdates as new props arrive so it can have computed values (and/or actions) that depend both on the state and the props. AFAICT useLocalStore ensures that the props being received are a mutable mobx object being continuously updated, or am I mistaken?
Originally posted by @spion-h4 in mobxjs/mobx-react#722 (comment)
Beta Was this translation helpful? Give feedback.
All reactions