implement lazy initialization to useObservable #72
Conversation
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 am not too excited about this. I am still kinda convinced it opens up to bad practices. The heavy state should not be in the component like this as it gets lost on unmount. The Context is a much better candidate imo as you usually don't want to be prop drilling it.
The prop drilling is a problem especially because you cannot simply pass down a single prop from that observable as it would not be observable anymore. This is very different from useState
which allows it. People might get burned by it a lot.
Honestly, there are so few people asking for this that I am not too keen merging it right away, but rather wait. I would like to see some real uses where having initializer is necessary. Personally, I had no need for it so far just yet so I am curious what are people doing that they need it.
test/useObservable.test.tsx
Outdated
const TestComponent = () => { | ||
const obs = useObservable(() => ({ | ||
x: 1, | ||
y: 2 |
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.
Please add a getter and action here just to be safe on the type side. I tried what inferrence problem there is without overloading and it mostly works except that getter in the above test.
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.
added tests for action and getter in new commit.
3c1b248
to
f2597fa
Compare
I don't know how to respond to your use-scene doubt. In my opinion, https://reactjs.org/docs/hooks-reference.html#lazy-initial-state And I don't think this mr is much relevant to prop drilling. I respect your opinion. You can decide if this mr is necessary. If you need it in the future, at least I provide a way to solve the type infer problem. |
The The second huge difference is that if you want to pass observable state down, you have to grab a whole thing. Sure, you can pass down a single value, but it won't be observable anymore. With Context approach, you don't need to think about it because you always have a whole state which you don't need to split to smaller pieces. It's easier to track down its use. That leads me to the point that If we openly support the possibility to have a heavy observable state, it might lead to bad patterns where you have some big bulk of data where it's not supposed to be. Consider, that nothing like that was possible with Ultimately, I still want to wait a bit for more people asking for such a feature. If you know some other users of this lib, you can direct them here for their opinion. Or make some unbiased poll on twitter or something. Also, I would like to see proper documentation that warns people about this kind of misuse which is really hard to debug without deep knowledge of MobX. Are you willing to take responsibility for answering questions of users who get entangled in this problem? PS: Would you care to share your actual use case? Note that observable class object with 3 or 5 properties can hardly be considered heavy :) That's just premature optimization without measurement imo. |
If a user would pass an observable prop down, and find dependency track not work. It's not due to a complex observable. It is because he doesn't understand the mechanism of I don't have any actual use case. I just wanna align to We can wait for more comments about this : ) |
We are already getting a single mutable object instead of a tuple
Sure, it's just my assumption based on that it's unlikely to have a use for a complex state within a single component. With a simple state, it's less likely to get burned by that imo. |
I think I have a pretty solid use case in general: ViewModels, for example: type Circle {
x: number, y: number, radius: number
}
const CircleEditor = ({ circle }: { circle: Circle }) => {
const viewModel = useObservable(() => ({
isSelected: false,
dragVector: [ circle.x + 100, circle.y + 100 ],
get color() { this.isSelected ? "red" : "blue" }
// bunch of event handlers / methods
},
[circle] // we want to create a new view model if a different circle is passed in
)
useObserver(() => /* render stuff based on circle and circleVieModel */)
} I think lazy initialization has 2 benefits here:
On a contra argument, one could very well argue that this is a non-mobx specific pattern, and a generic hooks would be better here, which would make it look like: |
Well, not that I would be convinced that adding two numbers together is so slow it needs to be lazy, but dependencies is a good thing. However, you have a problem here. What happens to a previous state? I don't know all the rationale for that example, but let's say The way it is declared, it would make sense to do the reset, but I am sure there are cases where losing the previous state could be problematic. One might argue that for a state you want to keep you would just declare a separate observable which would work, but it seems odd to me. I think we should keep |
Also, consider we have We shouldn't probably provide too much utility hooks that feel very similar. It only adds to confusion which one to use. |
Imho there shouldn't be function createPoint(x = 0, y = 0) {
return { x, y }
}
function Point00() {
const point = useObservable(createPoint);
}
function Point11() {
const point = useObservable(() => createPoint(1,1))
} Even with context, the "heavy" state still needs to be initialized somewhere: function App() {
const appState = useObservable(createAppState);
return <Provider value={appState}>/* ... */</Provider>
}
Actually I've been thinking whether the value should't be always boxed
IIRC the initial null value is one of the reasons why Not a fan of the dependencies/memoization idea. |
@urugator I have a feeling we are walking in circles in here. It's still about theoretical and contrived examples of "heavy" state but do you have some real example for that? I am repeating myself, but if someone really wants to be hunting every millisecond of the performance, it's always possible to simply not use this helper at all. It shouldn't be a silver bullet for every use case. With function App() {
// the App won't probably render anytime soon, why to bother with lazy init?
const appState = React.useRef(createAppState()).current;
return <Provider value={appState}>/* ... */</Provider>
} |
I don't see what seems to be so contrived/theoretical. The use case is exactly the same as for
Because it's less bothering than making assumptions about the surroundings (how often the component re-render or how "heavy" the state actually is) and dealing with ref/useState workarounds. |
I don't think it's right to compare it to In overall I think I would rather provide some recipes/faq for tackling complex scenarios and rather promote DIY approach than creating a super universal (and complicated) API. It only makes adoption harder when a newcomer gets overwhelmed by a bunch of similar options. Simple and straightforward is usually a better option imo. People should learn that MobX is not some magical land that needs a bunch of utilities before it can be used. It's a similar case with Provider/inject. They got used for such hand-holding, that even the idea of using Context without any abstraction feels weird to some of them. In practice, I've used |
To me these seem to be arguments against |
Sure, that can be as well. Along with a fresh discovery that
I've said that above ... #72 (comment) |
I have a use-case where I'm catching up on this thread and seeing maybe there are ways to avoid useObservable all together which I'll look into. |
Ok, that's certainly an interesting approach. The The tricky part is what do you do when query decides to update? I mean Anyway, it's getting more to the point that one universal |
I am going to close this since we are considering removal. It might still come in handy if we ever decide to make a separate utility package. |
ok |
Finally I solved the problem of typescript inference.
The code is simple. Just accept a initial function.