Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How far does observable(new MyClass()) reach? #5

Open
seivan opened this issue May 18, 2022 · 1 comment
Open

How far does observable(new MyClass()) reach? #5

seivan opened this issue May 18, 2022 · 1 comment

Comments

@seivan
Copy link

seivan commented May 18, 2022

Coming from Mobx, we have a pattern of having nested stores that are observables and passing them via props.
Prefer that over using providers/hooks as that's a contract a component has in its type signature.
The exception to that rule is Themes, but that's not important.
All components define what they need to be passed to in order to be constructed.

The stores could look like this (prefer class based approach)

class ApplicationStore {
   todoStore: TodoStore
   constructor() { 
     this.todoStore = new TodoStore(this) 
  }
}
class TodoStore {
  private _todos: Map<string, Todo>
  private _applicationStore: ApplicationStore
  constructor(applicationStore: ApplicationStore) {
    this._applicationStore = applicationStore  
    this._todos = new Map()
    this._todos.set(new Todo("sample", this))
  }
}


class Todo {
  private _store: TodoStore
  constructor(body: string; store: TodoStore ) {
    this._store = store
  }

It seems like from testing.
I only need to do
const store = observable(new ApplicationStore())
However, I did notice that in the constructors of the various nested stores, like for instance

    this._todos.set(new Todo("sample", this))

That particular Todo isn't observable, meaning editing its text, does not trigger re-renders.

Could you explain how observable() works? Does it go deep and make everything observable on an instance and its fields, and the future objects added to its lists?

From testing, creating and adding a new Todo to the map inside TodoStore automatically reflects in the list.

(todos is a memoized Array.from(this._todos.values()))

      {props.todoStore.todos.map(todo => {
        return <TodoItem todo={todo} key={todo.id} />;
      })}

But also editing the Todo within that component will reflect as well, and work just like mobx does with memo, in that only the component that edits will render and nothing else. It's impressive how close to Mobx you've gotten in terms of dev UX but also supporting the new concurrency rules.

@selsamman
Copy link
Owner

Observable will create a proxy for the object passed to it. It cascades to all properties of the object and replaces any that refer to other objects with a proxy. The proxy itself will monitor mutations and if an unproxied object is stored in a proxied object it will create a proxy for it. It has special handling to extend this to maps, arrays and sets such that when they are mutated any objects being stored in them will be proxied. That is meant to cover all cases of extending observations to referenced properties (except for the arrow example in issue #6 which I will explain separately).

Given that it is not clear to me why in your example this code would create a Todo that is not proxied and thus not cause re-rendering when the todo is edited:

 this._todos.set(new Todo("sample", this))

I took the code you provided as a guide to create a test to see if I could find out why but could not reproduce that issue.

    it( 'Handles Nested Stores',  async () => {

        class ApplicationStore {
            todoStore: TodoStore
            constructor() {
                this.todoStore = new TodoStore(this)
            }
        }

        class TodoStore {
            todos: Map<string, Todo>
            applicationStore: ApplicationStore
            constructor(applicationStore: ApplicationStore) {
                this.applicationStore = applicationStore
                this.todos = new Map()
                this.todos.set("one", new Todo("Sample", this))
            }
        }

        class Todo {
            store: TodoStore;
            body: string;
            constructor(body: string, store: TodoStore) {
                this.store = store;
                this.body = body;
            }
        }

        const store = observable(new ApplicationStore())

        function App() {
            const todo = store.todoStore.todos.get("one");
            const edit = () => {
                const t = store.todoStore.todos.get("one");
                if (t)
                    t.body = "Sample2";
            }
            return (
                <div>
                    <span>Body: {todo?.body}</span>
                    <button onClick={edit}>Edit</button>
                </div>
            );
        };

        const DefaultApp = observer(App);
        render(<DefaultApp />);
        expect(await screen.findByText(/Body/)).toHaveTextContent("Body: Sample");
        act(()=>screen.getByText('Edit').click());
        expect(await screen.findByText(/Body/)).toHaveTextContent("Body: Sample2");

    });

This particular todo did cause a re-render when edited at least in the way that this test edits it. Would you be able to provide a codesandbox to reproduce that issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants