QwikDev / qwik

Instant-loading web apps, without effort
https://qwik.dev
MIT License
20.83k stars 1.3k forks source link

State outside of the render tree #645

Closed benjamingr closed 2 years ago

benjamingr commented 2 years ago

Is your feature request related to a problem?

Is there a blessed/good way to create state in Qwik outside of the render tree for logic?

I want to save code containing logic outside of the render tree and reuse it between a qwik web app and non-ui code and to simplify testing it.

Describe the solution you'd like

A non-render-tree API that's an alternative for useStore like observable in MobX.

const store = reactive({ x: 3, y: 5 }); 

This is similar to what getOrCreateProxy does in the code with the unit tests - the way some frameworks like MobX get around it is globality of the map argument (I'm not sure that's such a bad thing in this case since the globality is "transparent"). Alternatively if that's too hard or there is an ideological objection then a factory for it:

const reactive = makeReactive(qInstance /*color my shed */);
const store = reactive({ x: 3, y: 5 });

Similarly, we'd want a way to listen to changes:

autorun(() => console.log(store.x)

This would not replace the render/framework aware hooks.

Describe alternatives you've considered

From reading the code from what I can tell there is no current way to do this - I can theoretically pull in something like MobX and trigger changes in a store.

Additional context

No response

nnelgxorz commented 2 years ago

Would the Context APIs (createContext, useContext, useContextProvider) work for you here? They're not publicly documented but you can see their usage in the ToDo starter.

Link

benjamingr commented 2 years ago

@nnelgxorz probably not since if I understand that example correctly it still uses useStore which you can use only inside the tree.

mhevery commented 2 years ago

It used to be that useStore was called createStore and therefore could be invoked anywhere. The constraint is that the store needs to be serialized when the application is paused and therefore it needs to get a reference to the correct DOM (on the server there can be more than one DOM)

@manucorporat do you remember what was the issue why we had to move them from createStore to useStore (I feel like it had to do with <template/> but at this point, I think we have a workaround for <template/> so I think that constraint is no longer true and we can relax that restriction and can move back to createStore()

Your second question is about creating subscriptions to store. Current way of creating subscription is through useWatch$((track) => track(store, 'prop')) The constraint here is that any API we provide for registering subscriptions needs to be serializable!

I could imagine something like this:

const store = createStore({count: 0});

subscribe(store, 'prop', $()=> {
  console.log("change");
});
// OR
const unsubscribe = watch$((track) => {
  track(store, 'prop');
  console.log("change");
});

@manucorporat would love to hear your thoughts.

mhevery commented 2 years ago

Could we have an example of a test / use case you have in mind? I want to see if there are other ways to solve the problem.

manucorporat commented 2 years ago

it had to do with recursive watching, and the proxyMap, which is a state of the container... Why does it need to be global? can a root component create it and pass it down using useContextProvider?

manucorporat commented 2 years ago

For the reactivity outside the render tree, i have good news, all the subscriptions and reactivity are already decoupled from useStore(), and it's an standalone API, we could hook up, but it's not exposed yet

manucorporat commented 2 years ago

This use case is super interesting, and I am happy to solve it, would be great to get to some specifics, maybe some pseudocode of how you wish it could work + specific use case would definitely help. cc @benjamingr

benjamingr commented 2 years ago

I am not suggesting you remove useStore since I do see the value of the hook being render-aware (though your current reactivity system is very coupled to the render tree).

Why does it need to be global? can a root component create it and pass it down using useContextProvider?

No, that I can do today and it would work just fine.

For the reactivity outside the render tree, i have good news, all the subscriptions and reactivity are already decoupled from useStore(), and it's an standalone API, we could hook up, but it's not exposed yet

Yeah I noticed but I also noticed that the other side of the reactivity isn't - so for example while the whole q-object code allows watching the element (assuming I get a hold of the/a map somehow to know who to notify) there is no autorun like API that lets me watch for changes explicitly outside of the render tree.

This use case is super interesting, and I am happy to solve it, would be great to get to some specifics, maybe some pseudocode of how you wish it could work + specific use case would definitely help. cc @benjamingr

Sure, let's say I'm building something where my store holds mildly (but not very) complicated logic like a stocks application. I want a service that fetches stocks from an API and performs calculations on those like the average yield, total portfolio size etc. Let's call it PortfolioManagementService (not a great name but I'm not actually building this it's a simplification)

I don't want to put such a service inside the render tree for several reasons:

Now, I understand this case presents some issues (like to make sure serialization owrks for example if it's a class) that need addressing but I definitely wouldn't want to duplicate the code or put it inside the render tree.

shairez commented 2 years ago

@manucorporat I agree with @benjamingr that this is important for managing stuff like subscriptions or global data that should be shared across components (or derived from) but without the need to put everything on the context.

Does the example benji gave make sense? can we achieve this with the internal detached reactivity API manu mentioned?

benjamingr commented 2 years ago

@shairez I still owe Misko a gist for why I want DI/services as a concept - I think DI can be done much nicer way than in React context and it'd make services easy/simple.

For example:

// myService.ts

// singleton duration by default, members are reactive by default
@service('UserService')
export class UserService { 
  username: string;
  // ...
  getUserData(username) {
    return fetch('/myApi', { ... }); 
  }
}
// Injects and builds UserService into the component based on parameters
// This can be made more or less magic depending on preferences
export const App = component$((@UserService service: UserService) => {
  // use the service here
  return <p>Hello Qwik</p>;
});

The important thing to notice here is that every service and its dependency chain is like the render tree in that reusability can be built in. Additionally the reason React DI sucks and requires hooks instead of just props is that DI needs to be baked into the framework for that syntax to work.

benjamingr commented 2 years ago

And of course this also solves Reactivity's globality which I think is what Manu didn't like in my example above

mhevery commented 2 years ago

I would love to see what you come up. Feel free to just paste it into this thread.

manucorporat commented 2 years ago

Sorry, thought i already share the data principles from qwik.

https://hackmd.io/@qwik/data-principles#State-is-part-of-the-tree

Please feel free to reopen if you think this proposal can be put in a different way that fits the principles, or a strong argue about some of the principles.

But so far, in Qwik the state must be part of the tree, for technical reason but globals are in general something to avoid even in other templates when doing SSR.

The services seems like an antipattern in qwik, since it couples data with implementation

DustinJSilk commented 2 years ago

Hey @manucorporat i'd like to add an argument for this where it would be very helpful to have stores created outside of the render tree.

A third party state management library built around Qwik may need to dynamically create stores that can feed back into the UI. These wouldn't be global stores but rather:

My example: I'm working on getting GraphQL working with Qwik which is built around a normalised cache of stores. We can't add a new Qwik store from the render tree to the query each time we want to fetch data because the response must be normalised into an unknown number of stores after the data has been returned from the API.

Could there be a way to achieve this whilst still making it safe for most users? Maybe exporting the underlying proxy handler?

Thanks!