mobxjs / mobx-react-lite

Lightweight React bindings for MobX based on React 16.8 and Hooks
https://mobx.js.org/react-integration.html
MIT License
2.13k stars 90 forks source link

Get rid of (almost) all utility hooks #94

Closed danielkcz closed 5 years ago

danielkcz commented 5 years ago

Update

After a lengthy discussion, we have agreed on the removal of useComputed and useDisposable. The useObservable hook will be renamed to useAsObservableSource and meant primarily for turning props/state data into observable. These can be then safely used in a new hook useLocalStore 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 any observer. 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.

mweststrate commented 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 :)

danielkcz commented 5 years ago

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.

xaviergonz commented 5 years ago

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 )

JabX commented 5 years ago

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)

johnnyreilly commented 5 years ago

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 actions 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. ๐ŸŒป

joenoon commented 5 years ago

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!

urugator commented 5 years ago

@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.

danielkcz commented 5 years ago

@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>
  );
}
urugator commented 5 years ago

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.

johnnyreilly commented 5 years ago

Thanks @FredyC !

joenoon commented 5 years ago

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;
})
danielkcz commented 5 years ago

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.

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 ๐Ÿค”

urugator commented 5 years ago

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.

danielkcz commented 5 years ago

@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 using useObserver 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.

mweststrate commented 5 years ago

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:

Just 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....

danielkcz commented 5 years ago

@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?

mweststrate commented 5 years ago

@FredyC, yeah, I didn't think about the names yet, useStore sounds great! Maybe:

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

danielkcz commented 5 years ago

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.

mweststrate commented 5 years ago

const observableProps = useAsObservableSource(props)?

urugator commented 5 years ago

@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.

xaviergonz commented 5 years ago

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):

  1. wrap the internal implementation of useAsObservable in some kind of "useSkipForceUpdate" hook that will signal the useObserver reaction that any triggered reactions inside that chunk of code should not result in a force update (however the only way I've found for this to work is to use an ugly global)
  2. re-create a new reaction for each useObserver execution (so it only tracks new observable updates), while dispose of the old reaction at the end of the render (so computeds are not uncached upon disposal of the old reaction). However for this to work useObserver must wrap useAsObservable
  3. learn to live with double renders and be happy?
  4. somehow add to mobx some kind of "run this in an action but do not trigger reactions", which would be like 1 but without the ugly global I guess.

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?

danielkcz commented 5 years ago

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.

xaviergonz commented 5 years ago

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
danielkcz commented 5 years ago

@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?

https://codesandbox.io/s/zl23z052x4

xaviergonz commented 5 years ago

Ah, I didn't know react would set any past used props to undefined automatically, nevermind that one then :)

xaviergonz commented 5 years ago

Wait actually it doesn't if you don't use destructuring, see here: https://codesandbox.io/s/926kqk8npw

xaviergonz commented 5 years ago

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
danielkcz commented 5 years ago

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)?

mweststrate commented 5 years ago

@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

mweststrate commented 5 years ago

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

drewhamlett commented 5 years ago

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>
}
mweststrate commented 5 years ago

@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 commented 5 years ago

@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

mweststrate commented 5 years ago

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.

xaviergonz commented 5 years ago

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)

danielkcz commented 5 years ago

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.

mweststrate commented 5 years ago

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 .

danielkcz commented 5 years ago

But useQuery shouldn't be nested in useObserver 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.

mweststrate commented 5 years ago

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

joenoon commented 5 years ago

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>;
};
rvion commented 5 years ago

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

rvion commented 5 years ago

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:

useObserver 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

joenoon commented 5 years ago

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.

danielkcz commented 5 years ago

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.

danielkcz commented 5 years ago

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.

https://codesandbox.io/s/8x4z866kq9

mweststrate commented 5 years ago

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

  1. shorter to write
  2. sounds hooky
  3. no wrapper component (which some prefer)

Observer

  1. existing abstraction existing in mobx-react
  2. can be applied where useObserver can't (e.g. in render callbacks of class based comps)
  3. wrapper component (which some prefer)
  4. syntactically more clear: return a component call from render, instead of the output of a hook. Doesn't need the strange 'as last rule' as returning components is always the last thing in a component.
  5. Doesn't suffer from double renderings icmb with useAsObservableSource
  6. Makes it more clear that the render part has it's own life-cycle / tracking. (example 1 and 2 of @joenoon's sandbox below)

Probably forgot some on the useObserver side, feel free to add

joenoon commented 5 years ago

@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.

mweststrate commented 5 years ago

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 .

joenoon commented 5 years ago

https://codesandbox.io/s/rlv79ly214

mweststrate commented 5 years ago

Sorry, found it indeed, wasn't paying attention

  1. This is working as expected, the 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
  2. Same
  3. This is basically the implementation of <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)