Skip to content
This repository has been archived by the owner on Dec 31, 2020. It is now read-only.

[Improvement] useObserver hook #800

Closed
elderapo opened this issue Nov 14, 2019 · 11 comments
Closed

[Improvement] useObserver hook #800

elderapo opened this issue Nov 14, 2019 · 11 comments

Comments

@elderapo
Copy link

I believe the way mobx-react works with react hooks could be drastically improved. I think a single hook could remove most of the mobx-react-lite API.

The idea is to pass store or any observable to useObservable hook which should return proxied observable object. Then in the proxy get handler we can track "observable accesses" to determine when component should be rerendered. I managed to create a simple proof of concept:

const useObservable = <T extends object>(store: T): T => {
  const forceUpdate = useForceUpdate();

  const reaction = useRef<Reaction | null>(null);
  if (!reaction.current) {
    reaction.current = new Reaction(`useObservable`, () => {
      forceUpdate();
    });
  }

  const dispose = () => {
    if (reaction.current && !reaction.current.isDisposed) {
      reaction.current.dispose();
      reaction.current = null;
    }
  };

  useUnmount(() => {
    dispose();
  });

  const proxiedStore = useMemo(
    () =>
      new Proxy(store, {
        get(target, key: keyof typeof target) {
          let val: unknown;

          if (reaction.current) {
            reaction.current.track(() => {
              val = target[key];
            });
          }

          return val;
        }
      }),
    [store]
  );

  return proxiedStore;
};

It works, however, I don't know much about how mobx works internally so there might be a better way of doing it. As I said this is POC so there are most likely unhandled edge cases and improvements to be made.

I have also created a simple demo for anyone interested.

@danielkcz
Copy link
Contributor

danielkcz commented Nov 14, 2019

Hm, another attempt to get rid of the observer. I have seen so many around that have failed that I am already kinda skeptical it can be done :)

This approach won't work in IE11 due to lack of Proxies, that's the fact so we cannot really recommend it as a viable solution.

It would need to build more around the behavior of useObserver from the next branch of mobx-react-lite to cover for memory leaks with Concurrent mode.

Besides a weirdness of double proxying (MobX 5 does that), I don't see a reason why this couldn't work, but I sure more experienced people will spot something :)

Edit: Ah, one possible weakness. What be surely a problem is nested properties in the store. You would need to deeply iterate the whole store, find primitives in there and wrap those. Considering some stores might be somewhat large, doing that all the time in component might be somewhat expensive.

Another point is that if you need to use multiple stores, you would need to wrap each one of them into this hook before the use.

@mweststrate
Copy link
Member

mweststrate commented Nov 14, 2019 via email

@elderapo
Copy link
Author

@FredyC, proxies are already required in mobx@5 so it this shouldn't be an issue.

Also, this hook could accept single observable const state = useObservable(someObservable) or array of them const [state1, state2] = useObservable(store1, store2). Or maybe useObservable and useObservables... not sure.

@mweststrate, as I said this was a quick POC. For this to work in the real world it would most likely require the use of deep proxy so mobx will be able to keep track of nested observable items even if the whole object (object passed to hook) is not observable itself.

Regarding the nested components issue. I think it could be solved by checking in proxy getter when in the execution process react is and based on that track value access or not.

@elderapo
Copy link
Author

@mweststrate Just so we're on the same page. With passing stores to child you mean exactly something like this?

const Parent = () => {
  const state = useObservable(observableState);

  console.log("rendering parent");
  return <Child state={state}></Child>;
};

type ChildProps = {
  state: ObservableState;
};

const Child = (props: ChildProps) => {
  console.log("rendering child");
  return <div>child counter: {props.state.count}</div>;
};

I am not sure what should be the expected outcome here. The easiest solution would be to advise users not to pass observed objects (results of useObserver) to children components and instead use useObserver in child components like:

const Parent = () => {
  console.log("rendering parent");
  return <Child state={observableState}></Child>;
};

type ChildProps = {
  state: ObservableState;
};

const Child = (props: ChildProps) => {
  const state = useObservable(props.state);

  console.log("rendering child");
  return <div>child counter: {state.count}</div>;
};

@danielkcz
Copy link
Contributor

I wonder what it would take to somehow utilize existing proxies in mobx itself. If we would be able to attach some sort of listener that would fire whenever some observable from that object is accessed, it would be super easy to just re-render component after that.

@elderapo
Copy link
Author

If something like this was possible with mobx@5 then writing useObservable hook would be super trivial.

const state = observable({
  count: 100
});

const proxy = mobx.proxy(state); // <-- not sure about the name

proxy.on("read", () => {
  console.log("read");
});

proxy.on("write", () => {
  console.log("write");
});

proxy.value.count; // should trigger read
proxy.value.count++; // should trigger write

state.count; // should trigger read
state.count++; // should trigger write

proxy.reset();

state.count; // should not trigger read
state.count++; // should not trigger write

Of course, it would require nested proxy to handle cases like this:

class CounterStore {
  @observable
  public count: number = 100;

  @action.bound
  public incrementCount(): void {
    this.count++;
  }
}

const state = {
  a: {
    b: {
      c: new CounterStore()
    }
  }
};

mobx.proxy(state)

@mweststrate
Copy link
Member

mweststrate commented Nov 14, 2019 via email

@elderapo
Copy link
Author

I created repo so it's easier to follow the progress for anyone interested.

I've managed to implement MobxProxy with deep proxy and it seems to be working. At the moment it's kind of messy because I don't really know mobx internals. There are probably lots of improvements to be made here.

Implementation of useObserver is fairly simple and straightforward. Example uses cases can be found here.

Regarding playrules and edgecases. After a brief look at mobx-react and mobx-react-lite repos I couldn't find much info about them. Care to point me to the specific issues?

@elderapo
Copy link
Author

I've created a new repo and published it to npm as mobx-react-better-use-observable. Simple example:

import { observable, action } from "mobx";
import { useObservable } from "mobx-react-better-use-observable";
import * as React from "react";

class CounterStore {
  @observable
  public count: number = 100;

  @action.bound
  public increment(): void {
    this.count++;
  }
}

const store = new CounterStore();

const CounterDisplayerWithoutHOF = () => {
  const { count, increment } = useObservable(store);

  return (
    <div>
      <h2>Counter(without observer): {count}</h2>
      <button onClick={increment}>increment</button>
    </div>
  );
};

More examples/edge cases can be found here (source).

Implementation of MobxProxy might need a rewrite because it got a little bit messy. Will try to add some hooks/react tests later.

@danielkcz
Copy link
Contributor

I believe this has been implemented by useLocalStore already. Open a new issue if you think there is still something else to this proposal.

@lock
Copy link

lock bot commented Jun 24, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 24, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants