Closed danielkcz closed 5 years ago
Alternatively (I think this has been proposed somewhere before), it could be possible to separate the hooks, and have it like this: here is an alternative version (not 100% sure it is better, but it achieves the same.
useObservable
can be called in two ways, with a function, meaning it get's initialized once, or with an object, which means that the returned observable is kept up to date. (Still has the potential destructuring risk though):
function Counter({ multiplier }) {
const observableProps = useObservable({ multiplier });
const counter = useObservable(() => ({
count: 1,
inc() {
this.count++;
},
get multiplied() {
return this.count * observableProps.multiplier;
}
}));
return useObserver(() => (
<div>
{counter.multiplied}
<button onClick={() => counter.inc()}>Inc</button>
</div>
));
}
Demo: https://codesandbox.io/s/mmjw9k6p3x
This also means that reactions can be cleanly created by using the observableProps
from the closure, like: useEffect(() => autorun(() => console.log(observableProps.thing)), [])
Not sure if the two overloads shouldn't actually be two hooks, just can't find decent names atm :)
I am not sure what to think about this yet, feels too convoluted and I cannot imagine where I would use it. Is it a really good thing to have observable props? I mean when props change, the component will re-render anyway. Isn't it ultimately slower to do that extra computation there?
Besides, your implementation does not consider updates to those props. That's what I would have been expecting as a user. Currently, it would just capture initial props in the observable and never rerun.
@xaviergonz Was trying to solve this sometime before, but it never felt worth the hassle.
Either way, this will need really good documentation and explanation. Not sure if it's worth it. Ultimately it might be easier to let people solve it userland based on their actual needs with the help of some recipes and articles from us.
Yep I did something like that here
https://github.com/xaviergonz/mobx-react-component
One thing I had to change to make it work though was to change the implementation of useObserver so it would create a new reaction on each render, or else the mutation done to the observable props object inside the function would queue a new render, thus ending in double renders ( https://github.com/xaviergonz/mobx-react-component/blob/master/src/shared/useMobxObserver.ts )
Is it a really good thing to have observable props? I mean when props change, the component will re-render anyway.
Yes, but you could set up reactions based on those props (and computed properties too) that will need to run when they change. Recreating reactions with useEffect with a deps array for example isn't as simple and there are quite a few cases where it doesn't work like it should (see : my previous post)
I would like to focus on preparing a website dedicated to MobX in React that would show several recipes and DIY solutions.
@FredyC I would love to see some recipes put up to share the knowledge.
I've got a large codebase which is mobx / react. I want to start exploring using hooks in this codebase without rewriting the whole shebang. I understand that's totally supported which is great.
We set up a big store which we use to dependency inject our components with the provider
/ inject
approach. It's a nice pattern.
We want to do the same with hooks. createContext
and useContext
make that possible which is great.
However, good guidance on how to combine this with stores that contain, computed
, observable
and action
s would be helpful.
Particularly it's not clear how to combine these with observer
and useEffect
. I've managed to get it working but only by splitting into 2 components. I suspect there's better ways.
I would certainly โค๏ธ to see some recipes. Thanks for all your work. Mobx remains amazing and I'm super grateful. ๐ป
I came to a realization last night after reading @mweststrate 's notes and exploring the tests in this project. mobx's observable
has quite some built-in power I wasn't making use of around get fn() computeds and being able to create actions. My current line of thinking after realizing this and refactoring two apps that make good use of mobx to try it out is that using plain mobx with plain react hooks (useState, useEffect, useMemo, useCallback) is actually the simplest/clearest. I think the route of useObservable, useComputed, is going to be problem with hooks in general (not just here) as the simplicity of creating these abstractions is so tempting, but often they are over/premature optimizations that actually hurt more than help. As one quick example, useComputed taking a fn and external deps, I think is more clearly expressed and understood in plain form:
const MyComponent = (props) => {
useObserver();
const [state] = useState(() => observable({
foo: props.foo || 'Foo', // lets assume for the sake of the example foo comes in from plain props
bar: 'Bar',
get formatted() {
return `${this.foo} and ${this.bar}`;
}
}));
// if our props.foo changes, we just need to update our observable state:
useEffect(() => {
state.foo = props.foo
}, [ props.foo ]);
return <div>{state.formatted}</div>
};
To me, this turned out to feel far more straightforward - it just does exactly what you expect and told it to do. I've updated https://github.com/joenoon/mobx-react-hooks to remove all utility hooks, updated the tests to this style, simplified the readme, etc. The one difference over there is the useObserver
is my macro form, not the function form in this library. I wonder if we'll come full circle eventually and find a way to make observer
"just work" with hooks!
@joenoon Just few notes:
Transfering props to local state (observable or not) is usually anti-pattern in react.
When doing so in useEffect/componentDidUpdate
rendering logic needs to take into account a situation where props and state are not synchronized yet.
useLayoutEffect
may be preferred so that the "non-synchronized output" is never shown to user.
@johnnyreilly Please follow #115
@JabX
Yes, but you could set up reactions based on those props (and computed properties too) that will need to run when they change. Recreating reactions with useEffect with a deps array for example isn't as simple and there are quite a few cases where it doesn't work like it should (see : my previous post)
Ok, I guess in some sparse situations it might be useful, but as @urugator said, it's also tricky to keep observable props in sync properly. So what's the "better evil" in here? I would rather recreate the reaction instead of bothering with synchronization.
Personally, I no longer see much of the benefits of using MobX for local states. Don't get me wrong, MobX still wins heavily when sharing the observable state with other components, but otherwise, it feels too cumbersome.
Just for a comparison the contrived example from https://github.com/mobxjs/mobx-react-lite/issues/94#issuecomment-482572299 rewritten without MobX. It's easy to read and reason about. No surprises about desynchronized props.
function Counter({ multiplier }) {
const [count, setCount] = React.useState(1)
const inc = () => setCount(i => i + 1)
const multiplied = count * multiplier // or useMemo for heavier calc
return (
<div>
{multiplied}
<button onClick={inc}>Inc</button>
</div>
);
}
I would rather recreate the reaction instead of bothering with synchronization.
You want the reaction's effect
to run when data
function yields different result, this can happen in two cases:
The problem is that the change of deps recreates the reaction, but it won't fire the effect, even if data
fn yielded different result.
You can pass fireImmediately
to fire effect on deps change, but than you have an opposite problem, it fires even though the result of data
fn didn't change.
So what we actually want is not to recreate the reaction on deps change, but just to notify an existing reaction about a possible change. Observable props solves this, but to me it seems like a workaround rather than a solid solution. Not that I would have any alternatives, I tend to think there isn't a reasonable way to handle this.
Thanks @FredyC !
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.
FYI I found an issue for this: https://github.com/facebook/react/issues/14088 but it's unfortunately pretty stale. It's actually worse than I thought, because anything, like React.memo
which is pretty common, would have this problem. It's super easy to see it in vscode (note the second one doesn't get checked):
import React from 'react';
function useTester() {}
const MyComponentChecked = () => {
useTester();
if (1) return null;
useTester(); // eslint caught this
return null;
}
const MyComponentBroken = React.memo(() => {
useTester();
if (1) return null;
useTester(); // eslint missed this
return null;
})
You can pass
fireImmediately
to fire effect on deps change, but than you have an opposite problem, it fires even though the result ofdata
fn didn't change.
Cannot you just keep a boolean flag in a useRef
to know if a recreated reaction should fireImmediately or not? I mean when you create the first reaction, it won't fire, wait for deps to change, recreate reaction and fire away as you know that deps has changed.
Need to separate non-observable deps from observable ones is surely somewhat annoying. And it's true that the same problem applies even when not using local observables. But converting all props because of that seems like overkill ๐ค
I mean when you create the first reaction, it won't fire, wait for deps to change, recreate reaction and fire away as you know that deps has changed.
But you don't want to fire immediately when deps changed. When deps changed, you want the data
fn to run, compare the result with a previous one and then if the result differs you want to fire the effect.
It's not like there isn't any solution to it, but imo not a reasonable one. A solution which is conceptual and provides noticible benefits for added complexity.
@urugator
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 usinguseObserver
would also have to (?) return an element, which makes it sort of a "component" with a lifecycle bound to consumer. Eg:
I've actually just used this pattern now and works great. Basically, I've moved a data layer into a hook and it depends on some observables. I can just wrap a whole hook body into useObserver
and component itself does not need to bother about it at all.
export function useMapOrders() {
return useObserver(() => {
const {
queryCategories: categories,
branchesFilter: branches,
} = useOrderQueryControl<RoA<TMapOrder>>(orderListCategories)
const { data } = useQOrderMapOrders({ branches, categories })
return { orders: data.orders }
})
}
So I am definitely against hiding useObserver
as some low-level API. You can never achieve the same with HOC or a component variant.
Is it a really good thing to have observable props? I mean when props change, the component will re-render anyway. Isn't it ultimately slower to do that extra computation there?
The problem in essence is not the re-render here, the problem is that autoruns / computeds don't reflect the change in props correctly, if they use them. Which means that they don't reflect a proper value
Besides, your implementation does not consider updates to those props.
Sorry, that was missing in the pseudo code indeed. The sandbox implementation does take care of it.
My current line of thinking after realizing this and refactoring two apps that make good use of mobx to try it out is that using plain mobx with plain react hooks (useState, useEffect, useMemo, useCallback) is actually the simplest/clearest.
I think I agree more and more with this line of thinking. The downside is that it doesn't solve all of the use cases in current mobx-react (such as props propagating to computeds), but maybe it is "good enough" for now
The most important clue is to use const [state] = useState(() => observable({ })
There is a caveats that need to be warned for though:
useState
initializerJust for a comparison the contrived example from #94 (comment) rewritten without MobX. It's easy to read and reason about. No surprises about desynchronized props.
The point of course is that it was a contrived example, just showing the syncing pattern. The case doesn't justify using MobX. The real life use case would be when there are complex view models, for example having a visual editor like Google presentations, where the view models hold the temporarily state needed for drag anchors, alignment rulers, etc etc.
@FredyC: Interesting pattern! So the consumer of useMapOrders
doesn't have anything mobx-ish?
Maybe the solution for now is to just stick to useObserver
only, and don't try to mimic the full set possibilities of @observer class
. And await what patterns arise. For example, should reactions be created in hooks components, or should people either keep reactions outside components, or use the React solution for these kind of problems?
But otherwise, I think this solution is the closest we got so far, as it behaves semantically the same for computeds / reactions + props as before....
@FredyC: Interesting pattern! So the consumer of
useMapOrders
doesn't have anything mobx-ish?
Indeed. The component is just a dumb renderer of data obtained from that hook. It's a bit stretch because that hook is not used anywhere else which feels slightly dirty. I made it mostly because of tests so I don't need to be mocking out google maps components.
Sorry, that was missing in the pseudo code indeed. The sandbox implementation does take care of it.
I assume you are talking about this one? https://codesandbox.io/s/mmjw9k6p3x
๐ฎ ๐ ๐ I am fairly amazed how simple the hook is and yet it seems to be working nicely. I think you are right, it's a good solution indeed. Personally, I would go with a name useStore
(or similar) for the variant with init function. Just to make it clear that it won't be magically refreshed upon non-observable change.
And that also leads me to another annoying issue. More than often it happens to me that VSCode auto-complete sticks useObservable
instead of useObserver
. Let's think about a different name that's not so similar please :) I am completely blank today, any ideas?
@FredyC, yeah, I didn't think about the names yet, useStore
sounds great! Maybe:
useStore(() => something)
useObservableState
useObservableRef(data)
I think these patterns can be explained quite well. These are gotchas, but I don't think they are worse than the many gotchas in the native hooks themselves :)
Edit: useObservableRef
better signals the data is not supposed to be mutated, compared to: useObservableState
useObservableState
is still fairly similar to useObserver
, but slightly easier to spot for sure.
It would be great if we can come up with ESLint rules over time to cover for these rules. Given that even TypeScript is turning toward away from TSLint, it's good to have it handled in one place. But that's surely music of the future :)
So in the summary, I think we have agreed to indeed remove useDisposable
and useComputed
and introduce two new hooks instead of useObservable
(names pending). Who is feeling for the PR? :) At least some kick-off.
const observableProps = useAsObservableSource(props)
?
@FredyC The pattern is the same thing in principle - a rendering logic which doesn't compose over conditions and cycles. It's not generally usable as a way to decouple observer logic from consumer.
Just a couple of thoughts:
useAsObservableSource will modify (update) an observable which is observed by the reaction set by useObserver, therefore using it will always enqueue a force update every time it is used if any prop is changed, thus ending in a second un-needed rendering every time a prop changes
There are a few ways to tackle this (AFAIK):
About the reference implementation of useAsObservableSource, just want to point out that Object.assign would not be enough, since deleted properties would not be deleted from the observable obj (just something to bear in mind)
Also I was at first worried that useAsObservableSource would only work with objects, but then I realized that if anybody needs to make a primitive observable (e.g. a number coming from a context) it can be just wrapped in an object, so no big deal.
About useObservableState, a question: Would any functions on the object be by default actions? If yes, how to make "view" functions? If no, how to mark the actions? How to use computed with custom equals, etc? Basically I guess the question is, will decorators be supported?
About the reference implementation of useAsObservableSource, just want to point out that Object.assign would not be enough, since deleted properties would not be deleted from the observable obj (just something to bear in mind)
Why would you need that? You are passing the same shape of an object in there every time. Unless you would have a condition and change it, but why would you do that? Would be better to have a separate useAsObservableSource
if needed, but hooks are generally scared of conditions.
E.g
<C a={5} b={6} /> // Object.assign(p, {a: 5, b: 6}) -> p = {a: 5, b: 6}
next render
<C a={5} /> // Object.assign(p, {a: 5}) -> p = {a: 5, b: 6} // b is kept, but should not be there
@xaviergonz If prop is not passed, it turns into undefined
and Object.assign
will set it as such. Why it should be a problem? I've modified an original demo to show it. If you mean something else, can you show it in the demo, please?
Ah, I didn't know react would set any past used props to undefined automatically, nevermind that one then :)
Wait actually it doesn't if you don't use destructuring, see here: https://codesandbox.io/s/926kqk8npw
suggestion for useStore, give it two possible overloads
const store = useStore(() => {
x: 5
}) // uses observable over the object internally
const store = useStore(() => observable({
x: 5
})) // if observable is already used (for example to use decorators), just use the object
// same applies for the first array item if an array is returned, in case the array one is used in the end
Wait actually it doesn't if you don't use destructuring, see here: https://codesandbox.io/s/926kqk8npw
Ok, that's an interesting observation. Although I am not sure if it's worth it to employ some elaborate checking for that use case and slow down everyone else. It's not a that hard rule to follow to use destructuring if it's usual for you to omit some props over renders. Consider that React is declarative, so it's more likely you would explicitly set something to undefined
than going an extra length of conditional rendering with different props set.
suggestion for useStore, give it two possible overloads
I don't follow. Do you realize that useStore(() => observable({ ... }))
is exactly the same as useState(() => observable({ ... }))
(except you have to destructure return value)?
@xaviergonz
(1) Good catch on the removal of properties. However, I think in general this is such an edge case (different amount of properties), that we also could consider a warning or throw instead of diffing? Edit: Note that this is only a problem as well if props
are passed on entirely, if they are passed to useAsObservableProps
like { a, b, c }
explicitly, this is also no problem
(2) For the double overloads: observable(object)
is already a no-op for anything that is already observable, so we get that for free :)
(3) for the double renders, as optimization tip, it could be possible to use <Observer>
instead of useObserver
, which creates a separate component. If that is the case, than we could do something like <Observer pure>{() => rendering }</Observer>
, which assumes that the closure is observable-only based. I am not sure though that the double renders will actually occur in the current setup, since the reaction runs immediately but the changes are propagated sync. Testing will show what happens
Started at least with the least joyful part, an initial draft of the docs :sweat_smile: in #130. @johnnyreilly would you mind taking a look at them? I'm curious whether your use case / questions would be addressed sufficiently
A style I've been using for a couple years with Mobx is this. (Used to use extendObservable on class components, but these are just functional component).
// Mobx model or MST
class Cart {
@observable itemCount = 0
constructor(cart) {
this.itemCount = cart.itemCount
}
}
function App({ dataFromServerFromParentComponent }) {
const state = useObervable({
cart: new Cart(dataFromServerFromParentComponent),
})
return <div>{state.cart.itemCount}</div>
}
// Mobx Store or MST
class Store {}
function App({ dataFromServer }) {
const store = useRef(new Store(dataFromServer)).current
return <div>{store.posts[0].title}</div>
}
It's easier to convey to teammates to just use Mobx all around instead of a back and forth between react state and Mobx(except for very simple things(a toggle) or our component library), especially since we have models already defined with a lot of functionality. So I think there is very much a need to have local Mobx hooks. It's such a paradigm shift from an OO world to a functional and plain JS object world. I've found co workers tend to pickup on the OO programming a lot easier. IE: A post has many comments. A comment has a description and character count.
Another thought is you typically don't need a global global store. Just a global store to that tree of components(page or screen)
We use a lot of view models and tend to load data in our component then hand it off to a view component that then creates a view model(shown above). This allows us to use methods on the view model to do pretty much everything. Very rarely do we rely on a local function in a component. I think this is a very powerful pattern and I like the idea of useLocalStore.
With suspense data loading eventually coming this should be a pattern that people embrace. Let the component do loading instead of store doing data loading.
const store = new GlobalStore()
function App() {
useEffect(() => {
store.loadDataFromServer()
return () => store.reset()
})
return <div>{store.cart.itemCount}</div>
}
@xaviergonz @FredyC another option to prevent double renders if needed, would be to allow deps to be passed to useOberver(fn, deps?)
. Did a similar thing in rval and that works quite neat so far
@xaviergonz @FredyC another option to prevent double renders if needed, would be to allow deps to be passed to useOberver(fn, deps?). Did a similar thing in rval and that works quite neat so far
If it comes to that I'd make it the other way around, with a list of things that should not trigger a reaction (usually only the stuff that is returned by useAsObservableSource).
Which makes me think... if mobx reactions had some way to know what caused them (maybe using something similar to getDependencyTree?) then maybe the useObserver reaction could filter out objects generated by "useAsObservableSource" (which could have some kind of "marker" to distinguish them), and if after filtering these there are none left then decide to skip force update?
Btw, I found another way to skip double renderings by using two reactions in round robin, where the "old" one is "reset" after the rendering is finished. However for it to work properly it still requires the whole render to be wrapped into it... https://github.com/xaviergonz/mobx-react-component/blob/master/packages/mobx-react-component/src/shared/useMobxObserver.ts
Investigated a bit more, the essence of the problem is that useAsObservableSource
writes to observable values, which schedules useObserver
, but, through setState
. So that will always schedule a second render. (calling setState
during render triggers another one).
Now there is no (obvious) way in which useObserver
can know that it was triggered form "its own" useASObservableSource
, or that useAsObservableSource
can look ahead an not cause a reaction, specifically for it's related useObserver
.
However, there is a trivial solution to the problem: use <Observer>
instead. It behaves exactly as we want! Because the life-cycle of the rendering is detached from the life-cycle of the owner, there is no problem at all. See also the tests in this commit: https://github.com/mobxjs/mobx-react-lite/pull/130/commits/cc7985c1f5e867841e46cee1754a9ce158c848c8
I think we can update the docs in #130 to warn for this. But the more I'm working with useObserver
, the more it seems we should simply push for <Observer>
over useObserver
as best practice. Conceptually it just makes more sense to me. The reactivity of useObserver
triggers the whole component to be reprocessed, event though it's "scope" is purely about re-rendering, and it shouldn't be affecting effects, create new callbacks, etc etc.
Why not drop useObserver and observer then? (unless observer can wrap the returned element into an Observer, but I think that'd not work since the observable values are already read and put into props by then)
Feels more like a workaround to be recommending Observer
, but I understand it's coming from a limitation of useState
. Even if there would be built-in forceUpdate
, it would probably act the same way.
However, keep under consideration that not all components are about rendering based on observable changes, it can have observable within the body, eg. GraphQL
const MyComponent = () => {
return <Observer>{() => {
const { data, loading } = useQuery({ value: observableValue })
return <div>...</div>
}}</Observer>
For once it feels awkward and I am not even sure if it's possible to use hooks inside a render prop...
Personally, I would rather suffer from double re-render that shooting myself in the foot with this awkward code.
@xaviergonz
Why not drop useObserver and observer then?
Please drop this discussion about dropping these out. Each of them has its use case and benefits.
But useQuery
shouldn't be nested in useObserver
either (hooks aren't
supposed to be nested afaik)? So in either case, one would be expected to
factor out a component (although the the obvious thing to do here is to
pull up useQuery to the body of MyComponent)
On Wed, Apr 17, 2019 at 4:12 PM Daniel K. notifications@github.com wrote:
Feels more like a workaround to be recommending Observer, but I understand it's coming from a limitation of useState. Even if there would be built-in forceUpdate, it would probably act the same way. However, keep under consideration that not all components are about rendering based on observable changes, it can have observable within the body, eg. GraphQL
const MyComponent = () => {return
{() => { const { data, loading } = useQuery(...) return ...}}For once it feels awkward and I am not even sure if it's possible to use hooks inside a render prop...
โ 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-484107250, or mute the thread https://github.com/notifications/unsubscribe-auth/ABvGhHgsLgQ_NqmmVXRoEGjO4AoXU17Kks5vhyvVgaJpZM4cELwd .
But
useQuery
shouldn't be nested inuseObserver
either (hooks aren't supposed to be nested afaik)?
Well, it shouldn't but if values that are being passed to the query are coming from observables, it's kinda must. I wouldn't like to be "refetching" based on useEffect
, been there, it's ugly us hell. Thing is that since useObserver
is synchronous and pure (almost), it doesn't matter it's wrapped around other hooks. It works just fine.
Alternatives are to use observer
(not a fan) or tackle the observability in a parent and pass that through the props, but in some complex scenarios, it hurts more than it helps.
Interesting, never run into this, but probably because when state is external, I also keep the data fetching typically external, instead of in hooks. Anyway, I've just added a tip on the docs, and I think we can further leave it as is. Otherwise we might be deciding to much things on an emperical study of 4 people.
I think we can basically release the PR as is, and address both further best practices and optimizations later down the road. I think this is definitely more than minimally viable to have standardized hooks
I haven't followed this new AsSource stuff yet and will need to catch up. I'll just chime in here re/ useObserver
observer
<Observer>
.
<Observer>
I think should be discouraged/deprecated since it creates huge gotchas unless its understood it should be used from a non-observable component to wrap an observable-component-that-didnt-use-observer, which itself should just be discouraged, since now the developer has to figure out what needs to be rendered within an <Observer>
and what doesn't. I really don't understand what benefit it has at all.
I prefer the macro version of useObserver
which has additional benefits, but for the sake of the conversation useObserver
here as the entire component return is good too. I think with some naming convention we could even eventually have our own eslint rules. Imagine something like:
Update:
Alternatively to the macro/useObserver
, if we can just get observer
to work correctly with linting and provide the same cleanliness of the render tree as useObserver (see https://github.com/mobxjs/mobx-react-lite/issues/94#issuecomment-482782020 - all HOCs currently break linting, even React.memo
) I think that is the nicest approach as it matches how React.memo
works which is widely accepted, and no need for a macro. Combined with naming convention and eslint rules for mobx hooks, the result could be nice and clean:
// good
const MyComponent = observer((props) => {
const x = useMobxSomething(...);
return <div>...</div>;
});
// eslint error
const MyComponent = (props) => {
const x = useMobxSomething(...); // eslint should catch this... "using a mobx hook outside of observation!"
return <div>...</div>;
};
I really like the new api better without those extra hooks. things are easier to reason about now.
also:
the more it seems we should simply push for
<Observer>
over useObserver as best practice. Conceptually it just makes more sense to me. The reactivity of useObserver triggers the whole component to be reprocessed, event though it's "scope" is purely about re-rendering, and it shouldn't be affecting effects, create new callbacks, etc etc.
this! +1
Also:
regarding nested hooks within useObserver
and other related discussion in this thread and linked threads:
I have no problem with nesting hook (should work just fine if nested hook is always registered) but
I don't understand why it is considered cleaner than adding an extra <Obsever>
node in the tree. I think it's the extra Obsever node is actually cleaner as it properly document the pipeline
As I understand it:
<Observer/>
= new tree node + useObserver on whole base compobserver()
and <Observer>
are better end-user abstractionsuseObserver seems like the right building block to create custom wrappers with various features like memo, context, etc. by handling the 'reactive part' without doing more
Since I got a couple confused emojis on my post above, here is a codesandbox:
https://codesandbox.io/s/rlv79ly214
The first two are the ones I'd like to discourage since there can be subtle breakage as demonstrated. This is a simple contrived example, but when things get more complicated this only happens more often. These patterns are all over the docs :/
The third, UseObserverFullWrap
works as expected, but notice the eslint rule of hooks warning. Its "fixable" with extra noise, but in general I don't like the extra level of indentation on this way anyway.
The fourth, observer
works as expected. And it actually DOES trigger the hook rules with the named function as shown, so that is nice (at least on codesandbox, haven't verified locally). It's a little extra noise to duplicate the name of the function, but not that bad. I haven't checked this one in react tools to see if it creates HOC chaos or not.
If observer
checks all the boxes (expected behavior, cleanest, rules work, no tree noise), then I think it would be wise to converge on that. The other ways are just too easy to break and I'm still unaware of a benefit that couldn't be achieved by breaking a component down smaller if really needed in a very specific use case.
Since I got a couple confused emojis on my post above, here is a codesandbox:
It was more of the frown from me than confusion when you wanted to remove Observer
despite what @mweststrate said just a https://github.com/mobxjs/mobx-react-lite/issues/94#issuecomment-484085373 above.
However, with today fresh eyes I realized, that Observer is effectively a one-liner. It could make sense to remove it and people can easily implement it on their own. The main "garbage" about that component is allowing for "render/children" and checking if you use it correctly. That's just sick. It could easily become an optimization advice.
The observer
still has some value as it's not so easy to implement on your own, so we should definitely keep that one. And useObserver
is a basic building block so we cannot get rid of it like that. Especially after the #121 is resolved, it will become very valuable as a future proof concept.
I don't understand why it is considered cleaner than adding an extra
<Obsever>
node in the tree. I think it's the extra Obsever node is actually cleaner as it properly document the pipeline
I would argue that using hooks inside render prop callback is documented :) Surely you won't find much of the examples of that anywhere. When you write a custom hook you effectively call one hook in other. It's just not that apparent because it's separated. We can agree both are not-standardized approaches yet.
Hm, I might have found a reason why NOT to recommend using Observer
as the main pattern. I don't yet fully understand why that example is not working really, but it's worrisome for sure.
I think you found the reason why not to use hooks in the middle of nested functions instead. As clearly specified here, bold and all:
Only Call Hooks at the Top Level. Donโt call Hooks inside loops, conditions, or nested functions.
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
I really think we shouldn't make design decisions on "what if hooks are used in useObserver / Observer callbacks", as it does violate the rules, so it isn't an actual use case. Even if it does work fine for you; we can't promote it to the community.
That being said, the behavior you are observing is correct, the contents observer component itself is reconciled, so the state isn't initialized again. If you would live that useState to the top of the component, the behavior would be the same.
that Observer is effectively a one-liner
So are a lot of MobX api's. It's not the LoC that matters, but whether the abstraction makes sense and is clear. In this case for both useObserver
and Observer
the same clear rule applies: don't use hooks in them, so it isn't a distinctive features. Even when it works from a technical POV.
So my current pro list is (might not be complete, this is a long thread)
useObserver
Observer
useAsObservableSource
Probably forgot some on the useObserver
side, feel free to add
@mweststrate are the cases in my codesandbox not valid or not a concern or am I missing something? I'd happily change my opinion on this in a heartbeat if I'm missing something.
Sorry, might have missed it, which one :)?
On Thu, Apr 18, 2019 at 9:12 AM Joe Noon notifications@github.com wrote:
@mweststrate https://github.com/mweststrate are the cases in my codesandbox not valid or not a concern or am I missing something? I'd happily change my opinion on this in a heartbeat if I'm missing something.
โ 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-484383717, or mute the thread https://github.com/notifications/unsubscribe-auth/AAN4NBEVYDM6VPIEZVLSPWLPRANMFANCNFSM4HAQXQOQ .
Sorry, found it indeed, wasn't paying attention
counter.count
is read outside a reactive context, which indeed isn't supposed to react. This and similar cases are extensively described in https://mobx.js.org/best/react<Observer>
:)To clarify on 1, nobody is tracking the result
assignment, since it is not part of any observer component. If in contrast, that would be in-lined in the callback, or done in the callback, or stored as expression (result = () => stuff
) that would all address the issue. But in your current setup no recalculation happens if the outer component doesn't render.
This is one of the reasons why "derive, don't cache" is prime in the MobX philosophy. const result
is a local cache introducing unnecessary local state. (I get that this is contrived, but when running in weird issues with MobX, it is usually because ppl deviate from the philosophy)
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.