Closed danielkcz closed 5 years ago
useComputed: never saw much use for it to begin with tbh
useDisposable:
the problem I see with useEffect (besides not returning the early disposer) is that without some guideline it can be easily misused, e.g (with reaction):
so I think it is still good to keep it there just for the sake of "what's the best way to use a mobx reaction/autorun etc within hooks?"
but that's the point again, the user shouldn't have to worry about it.
useObservable: I think it could be just be a useReference, so maybe that one could be a goner
On a side note, I wonder why react doesn't offer a useReference with a lazy initializer That's actually what I'd use as useObservable, giving it a new class (but probably I'd name it something different like useLazyRef)
or well, I just guess useState is good enough (even though it will re-render twice on mount)
edit: actually it doesn't rerender twice, just checked, that's nice
the problem I see with useEffect (besides not returning the early disposer) is that without some guideline it can be easily misused, e.g (with reaction):
Well, the useEffect
can be easily misused even without MobX :) It's definitely the biggest gotcha in React Hooks, but with the help of articles like The One it's getting to be more understood. Note that current useDisposable
does not protect you from those mistakes because it allows you to pass deps anyway.
It's great you listed those gotchas, haven't even thought about those. Would be a great source when explaining them in the FAQ.
so I think it is still good to keep it there just for the sake of "what's the best way to use a mobx reaction/autorun etc within hooks?"
If anything, there could be a separate package with such specific utilities as useReaction
or useAutorun
, but the primary focus should be on documenting and explaining that those are not needed for MobX in React.
On a side note, I wonder why react doesn't offer a useReference with a lazy initializer
I am sure I've seen some explanation from Dan somewhere, but cannot seem to find it 😖
Note that current useDisposable does not protect you from those mistakes because it allows you to pass deps anyway.
Well, actually deps are still required (sadly) in case you (for example) need to use a state or prop inside the effect code, or else they will be stale :-/ (and that will require the reaction to be recreated just because of the weird way react handles deps, which sucks)
(makes me wonder if the eslint rule that warns about missing stuff in the deps array of useEffect would also warn about other hooks, if so then that would certainly be a big reason -not- to have it as a separate hook, yet have a guideline somewhere)
Somehow I have the feeling react and mobx in the end want to do the same thing but in totally opposite directions. It feels like "do it the mobx way or the react way, but if you try to mix them it will be harder" (but I guess that also used to apply with class components and setState)
I have the feeling how I'd actually use mobx inside react with hooks 90% of the time would be something like (for internal state)
const Component = memo(function (props) {
const mobxProps = useObservableProps(props); // to use props the mobx way
const [data] = useState(() => observable({
x: 5,
// some actions like setX, some computeds // might use mobxProps
});
// mobx effects
useDisposable(() => reaction(() => controller.x, () => { ... }) // might use mobxProps
return useObserver(() => { ... })
})
so maybe an actually useful hook would look something like
const Component = memo(function (props) {
const obsProps = useMobxProps(props);
const state = useMobxState(() => {
x: 5
});
// obsProps used inside computed inside the class would just work (tm)
const views = useMobxViews(() => {
get xPlus1() {}, // computeds
viewFunc(foobar) // non action functions
});
const actions = useMobxActions(() => { // only needed in strict mode really
// setX
});
useMobxEffects(() => [
// no need to pass props.x to some stupid deps array, no need to re-create the reaction
reaction(() => state.x === obsProps.x, () => {...});
]);
return useObserver(() => { ... })
})
or alternatively
class ComponentState {
// data, views, actions, etc
}
const Component = memo(function (props) {
const obsProps = useMobxProps(props);
// obsProps used inside computed inside the class would just work (tm)
const [state] = useState(() => new ComponentState(obsProps))
useMobxEffects(() => [
// no need to pass props.x to some stupid deps array, no need to re-create the reaction
reaction(() => state.x === obsProps.x, () => {...});
]);
return useObserver(() => { ... })
})
that's why I thought the proposal I made to make a hook to turn props into an observable was key to break out from react silly state management for good (and actually the result of that is what I'd pass by default to the inner component when using observer, to better mimic what observer does in mobx-react)
As much as nice this looks at first, the real component could get ugly really quickly in my opinion. There is too much cognitive overhead entrusted into the component that should be mainly about rendering stuff. It also creates a shadowy assumption that you need all these hooks to have MobX in React and that's something I want to avoid for sure.
Nah, any complex state should be constructed out of the component, ideally in the Context. For a simple local state, the React is good on its own. I wouldn't call it silly for sure. It's just different.
const Component = memo(function (props) {
const obsProps = useMobxProps(props);
const state = useMobxState(() => {
x: 5
});
const views = useMobxViews(() => {
get xPlus1() {}, // computeds
viewFunc(foobar) // non action functions
});
const actions = useMobxActions(() => {
// setX
});
useMobxEffects(() => [
reaction(() => state.x === obsProps.x, () => {...});
]);
return useObserver(() => { ... })
})
What would you say to at least write a hook to make props an observable ref?
I think it is just this though (at least to imitate what mobx-react does):
function useObservableProps(props) {
const [obsProps] = useState(() => observable.ref(props));
if (obsProps.get() !== props) { obsProps.set(props); }
return obsProps.get();
}
Why? :) I had no need for that so far. If you do, then it's perfectly fine to keep it in your codebase.
This issue is about removing stuff, not about adding more of it, keep that in mind ;)
fair enough ;)
Nah, any complex state should be constructed out of the component, ideally in the Context. For a simple local state, the React is good on its own. I wouldn't call it silly for sure. It's just different.
I have a ton of components that use complex state effectively, with a lot of computed values to minimize rendering. Not all components are simple, and we should provide ways to enable people to do their stuff, as long as they're not trivial or redundant.
Using useRef
for anything else than an actual ref is cumbersome at best, so I'm all for having useful utilities that wrap it. useObservable
fills a need that you can't replicate with any other solution (well, I guess you could stick with a class component), and I'd even argue that its API should be complete, with "lazy" initialisation and especially with decorator support (the second parameter). And no, useState
doesn't cover the same usages at all. useReducer
exists in the standard API and that's what it replaces. I am totally fine with building the entire state of all my components with one call to useObservable
in each and keep working like I've been for the past couple of years with local observable state.
useComputed
is maybe a bit too simple and you can do the same thing with useObservable
and a getter. I have no problem with removing it, but I'd probably use it here and there if it stays.
useDisposable(() => autorun(() => …))
is the same as useEffect(() => autorun(() => …), [])
(well, except for the part where it doesn't return the disposer). While they both look like each other, the empty array is key (especially when using autorun that runs immediately) and for me it's a big no no, especially since not specifying explicit dependencies is (one of) the main draw of using MobX, so I don't want to think about it ever. Since useDisposable
in itself doesn't simplify that much the API, I'd be more in favor of having distinct useAutorun
, useReaction
hooks that have the exact same API than their regular counterpart, with the added benefit of being disposed at unmount (ie without double functions). Also, that would mean that a useAutorun
would run during (before?) the first render, as it should (like with @disposeOnUnmount
).
But for me the real redundant hook is the useObserver
one, especially since observer
is more predictable (see #97) and Observer
is more flexible (you can have several independent reactions that don't trigger the main render). If the only advantage of using useObserver
is having a cleaner React tree, it's not worth it, especially since forward refs solve most of the issues of doing this like that. I get that useObserver
is the "original" API from which everything else is based, but it could simply stay as internal.
I have a ton of components that use complex state effectively, with a lot of computed values to minimize rendering. Not all components are simple, and we should provide ways to enable people to do their stuff, as long as they're not trivial or redundant.
I am curious how do you minimize rendering with useObservable
? Do you realize that even the useObserver
will rerun a whole component on change because it uses useState
underneath? The only way to minimize rendering within bounds of a single component is use of the <Observer>
in the rendered tree.
Using
useRef
for anything else than an actual ref is cumbersome at best, so I'm all for having useful utilities that wrap it.useObservable
fills a need that you can't replicate with any other solution (well, I guess you could stick with a class component), and I'd even argue that its API should be complete, with "lazy" initialisation and especially with decorator support (the second parameter).
Sure, I am not against the idea of having a separate package with such utilities. Could be even a multiple hooks for simple cases and for the complex ones. The core idea is to have a mobx-react
package which can be used purely for observing components. Anything extra (including how to create observable) should be aside because it will always be opinionated.
But for me the real redundant hook is the
useObserver
one, especially sinceobserver
is more predictable
I guess it's personal preference, but useObserver
feels more apparent. When there is HOC around the component and tend to miss it. And there is also automatic memo
for the observer
which burned me a couple of times already. Lastly, the observer
(and useObserver
) cannot see anything in render prop pattern which is rather misleading and source of ugly bugs.
While they both look like each other, the empty array is key (especially when using autorun that runs immediately)
I don't think it's such a big deal. If you forget to use []
in the useEffect
call, it only hurts a performance slightly. Later you learn about it and use it. Also sometimes the autorun
or reaction
depend on non-observable variable and in that case you should specify dependency. I am convinced it's better to be explicit than trying to be clever.
well, except for the part where it doesn't return the disposer
Do you possibly have some use case for it? It's the biggest part of that code which doesn't seem that useful really.
Either way, as I said above, I really want to start with preparing some documentation site before touching any code. It's a long run. Still waiting for some recommendation on what to use? I am no designer, so I would prefer something with a useful template that can be tweaked later. I looked at Gatsby templates and doesn't seem like a good choice for docs.
I am curious how do you minimize rendering with
useObservable
? Do you realize that even theuseObserver
will rerun a whole component on change because it usesuseState
underneath? The only way to minimize rendering within bounds of a single component is use of the<Observer>
in the rendered tree.
Well, not directly with observables, rather with computed values and actions declared inside. I mean, that doesn't change what we've been doing for quite a while with class components and decorators (which is why I also want decorator support)
Sure, I am not against the idea of having a separate package with such utilities. Could be even a multiple hooks for simple cases and for the complex ones. The core idea is to have a
mobx-react
package which can be used purely for observing components. Anything extra (including how to create observable) should be aside because it will always be opinionated.
They're not complex, very useful and not at all opinionated. Not to mention that they don't take a lot of space inside the package. Not providing them in the base package will confuse people because they cover basic usages that are already built in for classes. If anything, we should aim for parity between the two if we want people to switch (I know I want that, at least)
I guess it's personal preference, but
useObserver
feels more apparent. When there is HOC around the component and tend to miss it. And there is also automaticmemo
for theobserver
which burned me a couple of times already. Lastly, theobserver
(anduseObserver
) cannot see anything in render prop pattern which is rather misleading and source of ugly bugs.
Well, observer has worked with a HoC-like API for as long as I've been using MobX and it's been fine ever since. I don't see the point of introducing a new API that will only confuse people into which one they need to choose. Especially if it's less capable than the original.
I don't think it's such a big deal. If you forget to use
[]
in theuseEffect
call, it only hurts a performance slightly. Later you learn about it and use it. Also sometimes theautorun
orreaction
depend on non-observable variable and in that case you should specify dependency. I am convinced it's better to be explicit than trying to be clever.
I have never written a reaction that has a dependency on a non observable prop, how would it even work? Omitting the empty array is a big deal if using autorun because your reaction will run after each render, in addition to the places it should already run.
well, except for the part where it doesn't return the disposer
Do you possibly have some use case for it? It's the biggest part of that code which doesn't seem that useful really.
No, I was just stating that fact for the sake of being exact. I don't think there is a valid use case for an early disposal, especially if we can use useEffect to do it.
They're not complex, very useful and not at all opinionated.
Since we are discussing if there should be lazy-init and/or dependencies, it is opinionated as long as we cannot agree on the same variant. It shouldn't either exist at all or it should be different hooks. I don't like the idea of mixing everything into one bulk with overloads. It should be clear from the code at first glance how is that observable constructed and if it depends on something. Having different names for such utils usually serves the best.
Not providing them in the base package will confuse people because they cover basic usages that are already built in for classes.
Note that current mobx-react
does not provide any sort of utility like that and I am not aware every of some complaints about it. There is the mobx-state-tree
for advanced cases (my preference) or the form with class & decorators. I've already seen several questions comparing these two. Adding the third one into the mix makes it even worse in my opinion.
if we want people to switch
Um, switch to what? Soon the mobx-react
V6 comes out and this package becomes pretty much obsolete unless someone really wants to avoid dragging class component support along.
Well, observer has worked with a HoC-like API for as long as I've been using MobX and it's been fine ever since. I don't see the point of introducing a new API that will only confuse people into which one they need to choose. Especially if it's less capable than the original.
Less capable how? It's a low-level API sure, but it has exactly the same capabilities and it's slightly more verbose and apparent from the code. The HOC tends to be rather hidden.
I have never written a reaction that has a dependency on a non observable prop, how would it even work?
Consider for example you need a conditional reaction that depends on a prop value. Since you cannot just call the hook conditionally, you have to put that condition inside the reaction. And that means the reaction needs to be disposed and recreated when dependant prop changes. Otherwise the reaction would be always seeing closure value when it was created first time.
@Jabx you might want to check out https://github.com/xaviergonz/mobx-react-component I just created it so I'm open for ideas
This is super interesting thread! Hope to catch up on it later this week. Need to prepare talks and workshops really hard atm 🙈 [image: image.gif] [image: image.gif]
On Tue, Mar 26, 2019 at 10:53 PM Javier Gonzalez notifications@github.com wrote:
@JabX https://github.com/JabX you might want to check out https://github.com/xaviergonz/mobx-react-component I just created it so I'm open for ideas
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-react-lite/issues/94#issuecomment-476867837, or mute the thread https://github.com/notifications/unsubscribe-auth/ABvGhDRtbQ5O6Qp6ipny35HdnucqR23iks5vapblgaJpZM4cELwd .
They're not complex, very useful and not at all opinionated.
Since we are discussing if there should be lazy-init a/or dependencies, it is opinionated as long as we cannot agree on the same variant. It shouldn't either exist at all or it should be different hooks. I don't like the idea of mixing everything into one bulk with overloads. It should be clear from the code at first glance how is that observable constructed and if it depends on something. Having different names for such utils usually serves the best.
IMO the hooks should have the same API as the original function (observable, autorun, reaction…), and as a hook useObservable should allow for "lazy" initialisation. That's all, and that's what I meant by saying "unopinionated": this is a 1-to-1 translation.
Not providing them in the base package will confuse people because they cover basic usages that are already built in for classes.
Note that current mobx-react does not provide any sort of utility like that and I am not aware every of some complaints about it. There is the mobx-state-tree for advanced cases (my preference) or the form with class & decorators. I've already seen several questions comparing these two. Adding the third one into the mix makes it even worse in my opinion.
Well, I can use @observable
and @computed
in class components. Sure, that's not a mobx-react thing, but these are the most basic things I use MobX for in my components today. And since hook-based components can be stateful, then we need something that allows us to declare MobX state in that. useObservable can do everything (as long as it mirrors the observable API), its API is already known (though I suspect not used all that much) and it's a good fit, especially considering that React itself provides useReducer for the same usage.
if we want people to switch
Um, switch to what? Soon the mobx-react V6 comes out and this package becomes pretty much obsolete unless someone really wants to avoid dragging class component support along.
Switch to using hooks in general. There are a lot of places in my codebase where hooks make a lot of sense (useContext
is killer), but I have to wait for official and stable support from mobx-react to migrate.
Well, observer has worked with a HoC-like API for as long as I've been using MobX and it's been fine ever since. I don't see the point of introducing a new API that will only confuse people into which one they need to choose. Especially if it's less capable than the original.
Less capable how? It's a low-level API sure, but it has exactly the same capabilities and it's slightly more verbose and apparent from the code. The HOC tends to be rather hidden.
Well, "less capable" wasn't maybe the right thing to say. I was referring to the gotchas there are when using useObserver instead of observer (you have to declare observable state inside its closure instead of at the root on the component), and to the fact you can only have one by component compared to independent <Observer>
blocks (well you can have several but they'd all result in the rerendering of the whole component, so that's not very useful).
I have never written a reaction that has a dependency on a non observable prop, how would it even work?
Consider for example you need a conditional reaction that depends on a prop value. Since you cannot just call the hook conditionally, you have to put that condition inside the reaction. And that means the reaction needs to be disposed and recreated when dependant prop changes. Otherwise the reaction would be always seeing closure value when it was created first time.
Props are observable in class components, so reactions and computed values just worked fine there. This isn't possible with hooks, so I guess you have a point there. Actually this is a major problem since it's a regression in functionality between class and hooks that we can't solve. Specifying props dependencies by hand while observable ones are automatic will be confusing, especially because up until then i didn't have to. I am not sure that we can do anything do solve this though.
IMO the hooks should have the same API as the original function (observable, autorun, reaction…), and as a hook useObservable should allow for "lazy" initialisation. That's all, and that's what I meant by saying "unopinionated": this is a 1-to-1 translation.
That sounds like your opinion 😆 And I tend to disagree. So you see, there are multiple opinions, thus it's opinionated and cannot exist in its current form 😎
And since hook-based components can be stateful, then we need something that allows us to declare MobX state in that.
I am still not convinced of this. There is close to none performance gain from it compared to React.useState
for simple cases. Perhaps I am doing it all wrong, but so far it worked great to have MST in Context for anything "heavier" and for local component state React works just fine and it does not suffer from the need to have observer
there. As long as we cannot provide reliable checks if you did not forget to use it, it's a fairly risky API and can cause strange bugs. I got burned by that couple of times already.
I was referring to the gotchas there are when using useObserver instead of observer (you have to declare observable state inside its closure instead of at the root on the component)
This is actually something we are trying to solve in #97 and I think it's getting somewhere. If that works out, it will become much more powerful as you won't ever need to think again about including observer manually somewhere.
Specifying props dependencies by hand while observable ones are automatic will be confusing, especially because up until then i didn't have to. I am not sure that we can do anything do solve this though.
@xaviergonz Was kinda trying to solve this with some useObservableProps
hook in the past, but it felt rather bad back then. I am sure we can figure out something eventually. The Macros are a big promise here as it can unburden a from a lot of manual tasks (= human errors).
And since hook-based components can be stateful, then we need something that allows us to declare MobX state in that.
I am still not convinced of this. There is close to none performance gain from it compared to React.useState for simple cases. Perhaps I am doing it all wrong, but so far it worked great to have MST in Context for anything "heavier" and for local component state React works just fine and it does not suffer from the need to have observer there. As long as we cannot provide reliable checks if you did not forget to use it, it's a fairly risky API and can cause strange bugs. I got burned by that couple of times already.
Computed values are love, life and everything else. Being able to update state without triggering an update when it's not needed can be a huge performance gain, especially when dealing with expensive renders like with lists. Or managing children state in the parent because the parent might need to update it (let's say a list with selectionnable items, where you can toggle each element separately or toggle all of them at once), where you can do all of this without rerendering the list a single time. You can't do this with pure React unless specifying a ton of sCU handlers by hand, that still don't exist for hooks btw.
I don't necessarily want to use external state for this, it can work but it's a bit more ceremony than I'd like (especially if I need context, why would I need context for a simple self contained component that renders a few children?).
Maybe the thing that we (I?) need is a simple hook that allows us to instantiate anything lazily on the first render, and then never touch it again while simply getting back the thing at each render. Something like an immutable ref, without the wrapping object and business with the initialisation. This is what useObservable already does, but there is no reason for it to be limited to an observable object. It could be anything. Such hook shouldn't come from mobx-react since it's way more generic than that though.
Anyway, we are debating over something that is totally trivial when using a class, so maybe that's a sign that hook components aren't really the end of it all... ? (please React team please reconsider bring hooks, and by hooks I mean useEffect and useContext, to classes... that would solve all of our (my?) problems…)
I've been thinking about a version of useObservable that was basically like below, with the same argument signature of useState, but different return signature (only need value):
function useObservable(inValOrFn) {
return useState(() => {
const val = typeof inValOrFn === 'function' ? inValOrFn() : inValOrFn;
return observable(val);
})[0];
}
I have found uses for wanting lazy observable creation, even if it some cases its just because it feels cleaner knowing I'm not re-running things that will never result in a change.
I like the current useComputed implementation and I haven't seen the downsides that have been mentioned. Maybe this is because its being used outside of useObserver with patterns that make the component and expectations harder to reason about (Observer component, etc).
I'm personally not feeling anything lost with the move to hooks - in fact quite the opposite. I'm hopeful #97 might even make it so much more simple when compared to classes/decorators/hocs/Observer-render-children.
It does seem like with useComputed/useObservable and other mobx-related things we can come up with that at one level they are really small and trivial and people could just implement them in their own codebase. On the other hand if everyone starts having their own slightly varying implementations that has other downsides. It could just be a phase because hooks are still newish. A place (either this repo or a sibling repo) for common hooks where we can fine-tune them would have value to me.
I wasn't really concerned about performance in any apps. Even without optimizations React is fast enough and we haven't heard from any customer saying "it's slow". That makes me kinda biased in here I suppose.
Maybe the thing that we (I?) need is a simple hook that allows us to instantiate anything lazily on the first render
Yea, what @joenoon said, you already have that lazy init with useState
. The exported useObservable
might feel way too magical and people don't really know what is it doing. If we could instead tell them to do const [obs] = React.useState(() => mobx.observable())
, they would have a complete control and understanding what's happening there. They can even choose to use observable.box
or observable.map
if they like.
I really want to start making a site dedicated to MobX in React. Explain these recipes instead of giving them some prebaked (and opinionated) solution.
A place (either this repo or a sibling repo) for common hooks where we can fine-tune them would have value to me.
That certainly something I am considering along with that site. Some kind of reference implementation for basic cases and have recipes to let people make their own for advanced stuff.
Observer is more flexible (you can have several independent reactions that don't trigger the main render).
The only way to minimize rendering within bounds of a single component is use of the Observer
Observer
has one disadvantage, which curiously enough doesn't seem to be mentioned anywhere.
It always re-renders together with parent, because it always receives new children (the closure) and due to the way it's implemented, it unnecessarily runs prop comparison (unless the callback isn't inlined, which is rarely the case).
You can't do this with pure React unless specifying a ton of sCU handlers by hand
There is definitely a lot more ceremony (well arguably, because you don't need a library), but far from specifying a ton of sCU. The state needs to be normalized so that the modification of item doesn't modify the array:
{
itemList: [0,1,2],
items: {
0: { selected: false },
1: { selected: true },
2: { selected: false },
},
}
I share some scepticism about useObserver
with @JabX. Not sure whether it should (not) be publicly exposed, but I think the observer
should be preferred as there are less opportunities for introducing an error.
The utility hooks are obviously opinionated (proven by the comments and issues). They are also very trivial to implement, mostly a sugar without meaningful logic. I think there isn't enough value in them to be part of the library.
As for the recipes website, I think the main concern (aside of usability) should be the ease of contribution. Probably not suitable, but iodide caught my attention recently.
As for the recipes website, I think the main concern (aside of usability) should be the ease of contribution. Probably not suitable, but iodide caught my attention recently.
Thanks, but that doesn't seem suitable indeed. I am currently thinking about GitBook, it seems it got revamped. Not that have been working with previous versions, but it does seem to have some interesting appeal. I am not sure about ease of contribution there though.
I share some scepticism about
useObserver
with @JabX. Not sure whether it should (not) be publicly exposed, but I think theobserver
should be preferred as there are less opportunities for introducing an error.
Well, on the contrary, if we succeed with #109, it could be a far better option simply because it will guard you against using observable within a component without having an observer
which can be an ugly source of bugs.
@urugator
Observer
has one disadvantage, which curiously enough doesn't seem to be mentioned anywhere. It always re-renders together with parent, because it always receives new children (the closure) and due to the way it's implemented, it unnecessarily runs prop comparison (unless the callback isn't inlined, which is rarely the case).
Are you sure about that behavior? Can you open a new issue with a relevant example, please? I am not using that component much so I did not notice it really. Ideally, if you have some suggestion on how to fix that, go ahead with PR. Wouldn't be enough to just wrap it into React.memo
same as the observer
?
Having thought and fiddled a bit about all of this, I'm okay with using useState
with observable
(like const [obs] = useState(() => observable(…))
), because it's not that much more than a specialized hook that will never satisfy everybody, and it can do everything everybody needs from an observable object. But I still think that using state in a function is a hack and that classes still have their uses, but people will still disagree I guess.
Unless we can find a way of building reactions that can react to props, I guess we're fine with useEffect
for reactions (even with the empty array), since it's a known pattern that's used for all kinds of side effects. I'd still like to have something more integrated than:
const [disposer] = useState(() => autorun(() => …));
useEffect(() => disposer, []);
to have autoruns (or reactions with fireImmediately in general) run during first render and not after it, like we do today with @disposeOnUnmount. I think there is maybe something the library can provide to help with that, be it the one utility hook that we have. disposeOnUnmount is a precedent for this, so it wouldn't look out of place.
useComputed is trivial to reimplement (even without hooks), so it can go.
useObserver is still redundant (even <Observer>
kinda is, as seen in the above issue), and using macros is definitely not something to encourage IMO.
to have autoruns (or reactions with fireImmediately in general) run during first render and not after it,
I am not sure why would you need that. It might even cause weird behavior when you expect the UI to be already rendered, but it isn't. Go ahead if you feel safe with it though :)
useObserver is still redundant (even
<Observer>
kinda is, as seen in the above issue), and using macros is definitely not something to encourage IMO.
Funny, I find the observer
redundant and useObserver
to be necessary ;) Well, since you are promoting classes and discouraging macros, I think I got a good picture about your coding habits. No offense :) You are just missing out a good stuff ;)
I find useObserver
to be the critical piece (especially with the macro syntax, but that is icing on the cake) and I think we can do away with <Observer />
and observer
in this hooks-specific library, or at least strongly discourage using them. I can't see a reason to use them that can't be done simpler with useObserver. Is there a case?
No offense :) You are just missing out a good stuff ;)
Isn't the excessive amount of excitement a reason for this issue's existence?
Is there a case?
Everything outside of useObserver
runs outside of tracking context, it's trivial to introduce staleness. Such bugs are impossible with observer
. The worst case scenario is, that you are passing plain values instead of observables, falling back to standard react behavior.
Everything outside of
useObserver
runs outside of tracking context, it's trivial to introduce staleness. Such bugs are impossible withobserver
. The worst case scenario is, that you are passing plain values instead of observables, falling back to standard react behavior.
That's what I mean by is there a case... is there a case for intentionally doing reactive things outside useObserver? I haven't come across one in the apps I'm working on which is why I ask.
Because if there is not, we should just make this all a lot simpler and not have Observer or observer, and show the useObserver pattern (not the one where it is the last return, but the one where it is the only statement).
With the macro in #109 useObserver
is basically a 1:1 of what the observer
HOC does, just in hook-style: https://github.com/mobxjs/mobx-react-lite/pull/109/files#diff-04c6e90faac2675aa89e2176d2eec7d8R154
class Foo extends React.Component {
render() {
return ...;
}
}
const Bar = observer(Foo)
vs
// regular version
const Foo = () => useObserver(() => {
return ...;
});
vs
// macro version
const Foo = () => {
useObserver();
return ...;
};
I believe this is what most people don't realize yet...Instead of thinking about useObserver
as a hook that should appear the last in a component it can be actually the very first.
// regular version
const Foo = () => useObserver(() => {
return ...;
});
There is one slight difference though. The observer
wraps the component into a React.memo
. useObserver
cannot do that. That means the component is re-rendered (and useObserver
executed) even if props don't change. Sure, you can wrap the component with React.memo
by yourself which is nice that you are not forced to it. But it's definitely less developer friendly in that matter.
const Foo = React.useMemo(() => useObserver(() => {
return ...;
}));
We could as well expose something like createObserved
which would wrap these two tasks together. Suddenly it gives a feeling it's like observer
HOC without unnecessary component nesting 😎 But it feels like another utility that can be introduced in userland if someone likes it so let's not get carried away.
const Foo = createObserved(() => {
return ...;
});
Edit: This also has another advantage. The ESLint won't complain about it because there is no use
prefix. With the example above, the linter will complain as you cannot call hooks inside a callback.
Could (or should) macro wrap a component to memo
as well?
Correct me if I'm wrong but React.memo
is the function component equivalent of PureComponent.
I haven't used it much but maybe should. But React.useMemo
is something entirely different - a generic way to memoize something when using hooks. So would it just be this (which seems pretty good as-is)?
// regular version
const Foo = React.memo(() => useObserver(() => {
return ...;
}));
// macro version
const Foo = React.memo(() => {
useObserver();
return ...;
});
@joenoon Everything you've said is correct :) That's the silver lining that you can decide if you want to optimize the component. The observer
forces you to do it.
to have autoruns (or reactions with fireImmediately in general) run during first render and not after it,
I am not sure why would you need that. It might even cause weird behavior when you expect the UI to be already rendered, but it isn't. Go ahead if you feel safe with it though :)
It's not a question of feeling safe or whatever, my reaction might need to run during first render because it initializes some state that I need to render the component, that can be updated later on using the same initialization. I am not pulling this example out of my hat or anything, I have working code today that does this and it's more convenient that way, especially since one of the promises of hooks is to reduce the multiplicity of component* hooks in classes.
Well, since you are promoting classes and discouraging macros, I think I got a good picture about your coding habits. No offense :) You are just missing out a good stuff ;)
How am I supposed to react to that? Aren't we literally on the thread discussing of the "good stuff"? If anything, you are missing on the good stuff by refusing to even consider classes to write certain kinds of components or for certain patterns that are definitely easier to write and understand with it. I am not "favoring" classes, I want to use hooks like most people, but there are no shortage of examples over the internet of trivial things to express with classes that are a nightmare to reason with with hooks, especially around state in general.
I just wish the React team weren't blind to this and would decide to go back on a few points (multiple contexts in classes please...)
@joenoon
is there a case for intentionally doing reactive things outside useObserver?
But that's exactly the problem. useObservable
allows you to access observables unintentionally outside of reactive context, increasing a surface area for severe bugs for no good reason.
Whats more severe problem in a state management library than something becoming stale? Without any clue from where the problem originates.
This can't ever happen with observer
, because you simply can't access observables outside of reactive context.
a lot simpler and not have Observer or observer
You can also make it simpler in the same way by not exposing useObserver
If you want to do a comparison be fair:
const Foo = observer(() => {
return ...;
})
// VS
const Foo = () => useObserver(() => {
return ...;
});
@urugator I don't follow what "fairness" are you talking about in that example? It's the same as @joenoon has shown and it's in my examples as well. Or is there really something I am not seeing? Please be more specific.
But that's exactly the problem.
useObservable
allows you to access observables unintentionally outside of reactive context, increasing a surface area for severe bugs for no good reason.
Sure, it's a problem if you learn wrong patterns. Having useObserver
in the middle of the component body can surely introduce bugs. But, if you learn it's actually ok to wrap entire component body into it, there is no problem anymore. Or you can just use macros (once polished) to guard that for you and the problem is no more.
It's definitely an initial mistake coming from this package. We originally thought that useObserver
is more like low-level API. Turns out it's much more and it's a valid opponent to observer
.
It's the same as @joenoon has shown
No, he's inappropriately comparing class based component to functional component, exaggerating the difference.
it's in my examples as well
Afaik createObserved
is exactly how current observer
is implemented. There isn't any unnecessary component nesting.
Having useObserver in the middle of the component body can surely introduce bugs.
The hooks were introduced to solve an incomposability of classes. The presented pattern is non-idiomatic as it's not composable (technically is, but practically isn't).
it's a problem if you learn wrong patterns
Or you could try to design the API in way which disallows/discourages wrong patterns.
I simply fail to see why useObservable
seems better:
So why should I use it or recommend to anyone? Only because it's hook and hooks are cool? That doesn't make sense...
So why should I use it or recommend to anyone? Only because it's hook and hooks are cool? That doesn't make sense...
I am not saying it's a silver bullet. It's a choice.
Personally, I was never a fan of HOC as it's something "outside" the component. It can easily escape your attention. Many times I've either forgot to remove it because it wasn't needed anymore or forgot to add it. For me, the useObserver
is just more apparent and feels like a part of the component. Sure, it suffers the same issues, it's just more visible.
it's not shorter to write.
Consider following in TypeScript world
export const MyComponent = observer<IProps & { children: React.ReactNode }>((props) => {
// this is NOT so extreme case, blame bad typing, but implicit children is not there :(
})
export const MyComponent: React.FC<IProps> = (props) => useObserver(() => {
// in some cases the useObserver is indeed shorter ;)
// and it's not so drown in there imo
}
it requires learning some patterns or using macros.
Learning is part of the process, you had to learn rules of hooks as well. They even need a linter to avoid obvious mistakes. It becomes natural after a while.
it doesn't provide any optimizations
Arguably as it's NOT always great to have "implicit" optimization, sometimes it's better to have a power of choice.
it's error prone. it is a hook, but actually doesn't provide any benefit of hooks
We can agree on these I suppose, tradeoffs are everywhere.
There is one thing we have been planning with macros and I still want to pursue that eventually. It would turn useObserver
into a powerful weapon. Idea is that you would have to useObserver
to be able to access and use hooks that are somehow working with observables. That way you would be completely safeguarded against a misuse. @joenoon Had proof of concept at one moment but then decided to pursue a different direction.
const { useObservable, observerHooks } = useObserver();
const foo = useObservable({foo: 1});
const mainStore = useMainStore(observerHooks);
. @joenoon Had proof of concept at one moment but then decided to pursue a different direction.
Yes, it's definitely do-able, but I started seeing lots of downsides to that approach:
My current state of thinking and current state of the PR is since people are OK with and understand observer
, they should equally be ok with useObserver
macro, since its exactly the same scenario - if you don't use it, it won't work... if you do use it, it will work. This also makes typings a breeze which are critical to me.
The macro has two other benefits related to eslint:
observer(props => ...break a hook rule)
you will not be warned - you wont even know and think you've done everything correctly until one day you realize its not analyzing these for you.useObserver(() => ...useX())
you are breaking a rule of hooks as per the current linting (calling a hook inside of a callback).Whether or not these are rule problems or real problems or somewhere in between is a different discussion and there are workarounds for useObserver
in its current form by using a named function, since we know its called synchronously. But the nice part about the macro is it lets you write normal simple hooks style and keep all the linting intact.
Finally found the time to read the above thread and #97. I think at this point I agree with @urugator that useObserver
is not the best thing to expose, it has some edge cases and doesn't solve anything <Observer>
doesn't.
For state, I noticed that I use the pattern const [obs] = useState(() => observable(…))
a lot. Long story short, this is what I would propose at this moment. Shoot!
const state = useObservable(() => someObject, decorators? | deps?, deps?)
observable()
will automatically be called on the return value. extendObservable
. () => new ViewModel(props.car)
deps
to make sure a fresh observable object is created if certain props changeuseComputed
, useAction
etcuseObservable(someObject)
could be supported as well for convenience, but I think using a thunk should be recommended, as the relation to props will become less error prone, which is important for computeds and actions depending on bothuseReaction(fn, fn, deps, options)
, useAutorun(() => something, deps, options)
and useWhen(fn, fn, deps, options)
would be quite neat api's, although additional runs when deps
change, might be a bit little confusing. I definitely could live with just sticking to useEffect
as well, as shown below, as saving that single additional () =>
could be easily done in user land if it annoys people enoughI think using observer
or Observer
are pretty great in their own way, and useObserver
is not that great from composition perspective (as-last, and hooks-nesting or both not optimal as discussed in #97), and wrapping on the outside doesn't yield real benefits over observer
? (unless I missed something in the above thread). Auto applying memo
hasn't caused any bug report as far as I can remember in the last 3 years, there seems to be no real need to bail out of that. In immutable architectures the optimization might cost more than it saves (if the props always change, which is why it isn't the default), but in MobX this is much less so, since properties typically don't change that much as object references are stable, meaning that memo hits cache in the far majority of the cases, as soon as anything observable is passed in.
Note that, if return <Observer>{() => rendering}</Observer>
is considered to be to much typing, we could also make the api more convenient like return observed(() => rendering)
. (or: track
/ tracked
/ observing
) That is almost the same as useObserver
. Except, it composes a bit more cleanly inside a rendering tree, as the following would be valid without triggering lint rules. Example component
const PageRenderer = ({ page, maxTitleLength }) => {
const pageView = useObservable(() => ({
selected: false,
get displayTitle() {
return page.title.substr(0, maxTitleLength)
},
toUpperCase() {
page.title = page.title.toUpperCase
}
}),
[ page, maxTitleLength ]
)
useEffect(() => reaction(
() => page.toJSON(),
data => saveToServer(data)
),
[ page ]
)
return (
<Section>
<h1>{observed(() => pageView.displayTitle)}</h1>
<h2>{observed(() => page.subTitle)}</h2>
<button onClick={pageView.toUpperCase}>Uppercase</button>
</Section>
)
}
(N.B. Obviously one observed
could have been used here)
I think at this point I agree with @urugator that
useObserver
is not the best thing to expose, it has some edge cases and doesn't solve anything<Observer>
doesn't.
It would feel extra weird wrapping a whole component code to the <Observer>
. It's a very unusual pattern. The hook is slightly better on that (especially with macro) imo. Personally, I hate to see any observer mentioned in react devtools. Call it OCD if you like :)
- Because props are not observable, one might need to specify
deps
to make sure a fresh observable object is created if certain props change
And I think this is more than wrong. It means it would reset whole state when a single value change and possibly losing some other state. It's more close to the useMemo
behavior than a useState
and without any control when an update happens (no access to the previous state).
and useObserver is not that great from composition perspective
On the contrary, only the useObserver
can be part of the custom hook and make it observable on its own without relying on the component being observable. How is that for a composition perspective? :)
but in MobX this is much less so, since properties typically don't change that much as object references are stable, meaning that memo hits cache in the far majority of the cases, as soon as anything observable is passed in.
Not sure how is that related. If you have MobX stores in the Context (or even with useObservable), there can still be a bunch of props that change (especially scalar ones). Not that it's bad to have memo
there, but it's not such a win all the time. With useObserver
I can actually decide when I want to optimize.
as the following would be valid without triggering lint rules
Mentioning that, the lint rules goes totally blind (for a whole component) if you use observer
HOC. Haven't really investigated if it's an issue on their side or something in here, but it's a fact. The useObserver
does not suffer from that and with a named callback, it works just fine.
Btw, as much as observed
looks nice, it's just another sugar for the same thing. If we would be keeping <Observer>
, it's too much API choices already. Besides, it does not solve the issue when you have observable within a function body. With GraphQL it's very common to have this.
export const LogoutWidget = () => useObserver(() => {
const { auth } = useRoot() // MST
const { data, loading } = useQLogoutWidget(
{ id: auth.userId }, // this is observable
{ skip: !auth.isAuthenticated }, // and this
)
let displayName
if (loading) {
displayName = i18n.t`Not logged in`
} else {
displayName = `${data.user.firstName} ${data.user.lastName}` // this too
}
return (
<Link to="/logout">
<div>{displayName}</div>
</Link>
)
})
const state = useObservable(() => someObject, decorators? | deps?, deps?)
If we would want to match react's hooks behavior, user would always have to provide at least empty deps array: const state = useObservable(() => {a:'x'}, [])
Perhaps it's a bit too overloaded with functionality.
On the contrary, only the useObserver can be part of the custom hook and make it observable on its own without relying on the component being observable. How is that for a composition perspective? :)
Do you have an example? useObserver
returns an element, therefore a custom hook using useObserver
would also have to (?) return an element, which makes it sort of a "component" with a lifecycle bound to consumer. Eg:
function App() {
// Note the hook limitations - no conditions/loops etc - are they suitable for something like this?
const header = useHeader(headerProps);
const body = useBody(bodyProps);
const footer = useFooter(footerProps);
return el(React.Fragment, null,
header,
body,
footer,
)
}
useObserver
returns an element
Are you sure? It returns any output from a callback you pass in there (and wraps it into a Reaction). It doesn't have to be an element.
I don't have any viable example right now, but I know it works.
Obviously you can return whatever, but it seems to be designed for rendering. I am just asking how it can be composed in practical way.
The deps array argument for useObservable
would be a good reason to have a dedicated hook for creating observable objects (as opposed to using useState
), but I share both @FredyC and @urugator's opinions about it, being that resetting the entire state at each dep change might seem extreme, and that it doesn't share the same behaviour than other deps array arguments.
I still think that there's a great use case for that in computed properties (that might/will be declared inside useObservable
), so maybe the deps array could only be used to reset those properties (if that's even possible or make sense)? Maybe the real hook we need in the end is useComputed
and not useObservable
?
The more I think about it, the more I think useEffect
isn't the right way to attach reactions to components, because:
autorun
would only run for the first time after the initial render (with no easy way to trigger it "before"). Also, since when
uses autorun
internally, in the case where its predicate is initially true, the component would have be rendered twice to get a result that we could have gotten at the first render. Skipping useless renders like these can sometimes result in noticeable performance improvements, so it's not always "cheap" to render (especially if they're easily avoidable). I'd agree that it's the least problematic issue among those exposed here though.reaction
(that is registered as a dep) would not trigger the reaction, unless it has the fireImmediately
option which might not be what the user needs, in addition to being a bit confusing (especially to someone used to observable props in components).when
callback could be executed several times, up to after each prop (registered as a dep) change, since useEffect
wouldn't know about the semantics of when
.There might be ways to circuvent these problems in userland, but I think that providing the solutions in the library is a good way to help people avoid shooting themselves in the foot.
It would feel extra weird wrapping a whole component code to the
<Observer>
It's a small downside, but I think it is one of the least concerns personally. Actually, it might also have benefits, as we can soon leverage the React devtools to show the dependency tree of an observer component
It means it would reset whole state when a single value change and possibly losing some other state.
You are right, it's weird. Crunched on it a couple of days, and this is stupid. Better proposal below :)
Mentioning that, the lint rules goes totally blind (for a whole component) if you use observer HOC
I hope that is a temporarily bug? And I think it could be prevented if needed by lifting observer
to the export statement (if applicable), which also fixes the display name.
Anyway, just parking the observer
/ Observer
/ useObserver
for a little bit now, I did crunch about the useObservable
a bit, and I think the essence of the problem is that unlike in mobx-react@5, props and state are not observable, meaning that computed functions and other things don't act properly. But I think I've found a solution, so please shoot!
Basically, useObservable
should take the following form:
useObservable(props, props => observableObject)
This fixes a few problem:
deps
so it is much more mobx-y. A quick exampe (from my previous comment):
const PageRenderer = ({ page, maxTitleLength }) => {
const pageView = useObservable({ page, maxTitleLength} /* or just props */, ({ page, maxTitleLength }) => {
const pageView = {
selected: false,
get displayTitle() {
return page.title.substr(0, maxTitleLength)
},
toUpperCase() {
page.title = page.title.toUpperCase
}
}
// start some effects as well if desired
const disposer = reaction(
() => page.toJSON(),
data => saveToServer(data)
)
return [pageView, disposer] // just returning pageView would be ok as well, and any function returned would be assumed to be a disposer
}) /* no deps! */
return useObserver(() =>
<Section>
<h1>{pageView.displayTitle}</h1>
<h2>{page.subTitle}</h2>
<button onClick={pageView.toUpperCase}>Uppercase</button>
</Section>
))
}
In principle it is even possible to combine React state and props into this object, if you also want to react to React state, like:
function WeirdComponent(props: { multiplier: number }) {
const [count, setCount] = useState(0)
const counterDoubler = useObservable({...props, count}, ({ count, multiplier }) => ({
get doubleCount() {
return count /*state*/ * multiplier /*prop*/
}
}))
useObserver(() => <div>{counterDoubler.doubleCount} </div>)
}
To simplify things the following overloads should also be possible
useObservable<P, T>(props: P, (observableProps: P) => [T, ...IDisposer[]] | T): T
useObservable<T>(() => T): T
useObservable<T>(T): T // convert plain object into an observable
Implementation wise, it could be roughly something like:
function useObservable(props, fn) {
const [observableProps] = useState(() => observable(props, { deep: false }))
const [baseResult] = useState(() => fn(observableProps))
Object.assign(observableProps, props) // update observable with latest props
useEffect(() => () => {
if (Array.isArray(baseResult))
baseResult.slice(1).forEach(disposer => disposer())
}, [])
return Array.isArray(baseResult) ? baseResult[0] : baseResult
}
Shoot!
is allways called with the same (observable) props object, and safely use it in computes.
I don't follow, computed/reaction in your example don't depend on some observable props
object - the props are destructured in args... If you update this observable props object it won't notify these computeds/reactions, does it?
Btw reactions created in useState
callback are also a subject of that custom GC (actually not sure if GC workaround is applicable here, since these reactions can do whatever and therefore should be disposed immediately).
If you update this observable props object it won't notify these computeds/reactions, does it?
Your correct, got carried away in enthousiasm. But without destructuring it does work as expected, demo: https://codesandbox.io/s/4lv0z7j159
reactions created in useState callback are also a subject of that custom GC
Correct again :) We could either hook into that GC mechanism, or instead create thunks that create the actual reactions in a useEffect
, like:
useObservable(props, props => {
const createSaveEffect = () => reaction(
() => page.toJSON(),
data => saveToServer(data)
)
return [pageView, createSaveEffect]
}
Update
After a lengthy discussion, we have agreed on the removal of
useComputed
anduseDisposable
. TheuseObservable
hook will be renamed touseAsObservableSource
and meant primarily for turning props/state data into observable. These can be then safely used in a new hookuseLocalStore
which will support lazy-init and serve as the main way of constructing observables within a component.Check out a comment with an initial proposal: https://github.com/mobxjs/mobx-react-lite/issues/94#issuecomment-482533778
Time has come to start considering this. There have been some discussions lately that got me convinced these utilities should not have been in a package in first place. I think we got carried away in here, most likely because it felt good to have some custom hook 😎 Ultimately, people might get the wrong idea that to use MobX in React they need to use these specific hooks.
I think it's better to do this sooner than later before people start using these utilities too much and would need to painfully migrate later.
As the first step, I would like to focus on preparing a website dedicated to MobX in React that would show several recipes and DIY solutions. I would like to hear some recommendations on what toolkit would be the best candidate for such a site. Gatsby?
The idea of some separate package with utility hooks is not eliminated, but it should go hand to hand with new docs to thoroughly explain the concepts and not to just blindly use the hook without fully understanding implications.
useComputed
The most controversial and not even working properly. After the initial pitfalls, I haven't used this anywhere. Is someone using it successfully?
useDisposable
As @mweststrate discovered today, there is not much of the benefit to this as the
React.useEffect
does exactly the same. The only difference is access to early disposal function. Personally, I haven't used for anything just yet. I would love to hear use cases if there are any. I am almost ashamed I haven't realized this before and just blindly used it 😊useObservable
Probably the most useful out of these three and the most discussed (#72, #7, #22, #69) also. It's clear that it's not only confusing but in its current form, it's wrong altogether. Personally, I have rather used
React.useState
for a component local state which is so easy and doesn't require anyobserver
. There is not much performance gain anyway unless<Observer />
is used. For the shareable state, it seems better to just use Context and build such a state in a way people like. Also, it makes a little sense to be resetting the whole MobX state based on props change.