vanjs-org / van

🍦 VanJS: World's smallest reactive UI framework. Incredibly Powerful, Insanely Small - Everyone can build a useful UI app in an hour.
https://vanjs.org
MIT License
3.82k stars 88 forks source link

Context-API support #161

Open Hunter-Gu opened 11 months ago

Hunter-Gu commented 11 months ago

Discussed in https://github.com/vanjs-org/van/discussions/152

Originally posted by **yahia-berashish** October 25, 2023 Hello, I tried to migrate React Context API to VanJS. The key steps: - create a class name and unique id for context provider, and store provide value - find the ancestor with class name by element.closest() when use context - get the unique id from dom element id, then get the provide value from store Current drawback: it will always get the default context when component render, because the framework hasn't bind the reactive state and dom, and it's a little tricky to find the ancestor context provider. Try it on sandbox: https://codesandbox.io/p/sandbox/vanjs-context-provider-poc-qsgdr8?file=%2Fsrc%2Fmain.ts%3A5%2C1 I want to know if there any recommendations about this.
yahia-berashish commented 10 months ago

Hello @Hunter-Gu Can you update us on the progress of the issue?

Hunter-Gu commented 10 months ago

Hello @Hunter-Gu Can you update us on the progress of the issue?

Hello @yahia-berashish I have updated the example by using VanX reactive, it works really good and simple.

I'm still trying to fix the problem: it will always get the initial value at first time painting. The key is we can't get the time when a dom mounted to the document. That make me think about Web Component. I'm working on this way now.

yahia-berashish commented 10 months ago

Can you share a CodeSandbox/Stackblitz link so I can check out the code

Hunter-Gu commented 10 months ago

Sorry for late response.

The codesandbox link: https://codesandbox.io/p/sandbox/vanjs-context-provider-poc-base-on-webcomponent-c4qp5h

I haven't fix it. Because VanJS will batch all updates to next render task, so user will still see the first painting with default value.

yahia-berashish commented 10 months ago

I think the approach you are using is wrong, even if the problem of the first painting was solved, there are still issues of passing props and having to create a separate context for actions, etc. I chatted a bit with ChatGPT about this issue, and after much trial and error I think I found a better way, I will try and open a PR as soon as possible, The code has some issues currently, but I think the approach is promising. This is the Stackblitz project containing the code @Hunter-Gu give it a try, would like to hear your opinion: VanJS Context API test Thank you for your help and time @Hunter-Gu

Hunter-Gu commented 10 months ago

Can this approach support to consume context in conditional rendering?

I try to consume context in conditional rendering, it can't render as expected.

VanJS Context API with conditional rendering You can search ! render context consumer in condition ! to find the issue.

Thank you for your sharing. @yahia-berashish

yahia-berashish commented 10 months ago

That's weird, it seems when a child component is rendered conditionally it uses the state of the last provider. I suppose this is the last issue with the implementation, I will try to fix it, but will appreciate if you can take a look at it too @Hunter-Gu

Hunter-Gu commented 10 months ago

Actually, this is why I want to find a tree structure to represent the component tree.

I find there is no 'real' component in VanJS internally, the VanJS component is a way to provide childDOM, there are no lifecycles, no component instance, etc. So it will be a little hard to implement Context in VanJS.

I will keep trying to fix it on our versions. 😄

yahia-berashish commented 10 months ago

That's true, but the lack of a virtual DOM like the one you described is the main benefit of VanJS, a virtual DOM bloats the package and makes it very hard for the developer using it to know what is actually happening inside of the app. But the features provided by the vDOM can be replaced with lightweight implementations that don't require such a complex base. Hope the context API we are working on will be one of them.

Hunter-Gu commented 10 months ago

Totally agree with you. I like VanJS because it is so tiny and it can run in browser directly with well-designed component-oriented development.

The key point of Context-API is we need to know the ancestor provider of consumer.

Here are two approachs I can think of:

  • maintain a tiny component tree in VanJS internal
    • we can have 'real' component in VanJS in this approach, we can support most of the features of morden framework
    • but this will increase size, I think we should avoid this
  • find a way to get the scope, or declare the scope when define component
    • it will be complicated when create component
    • but it's still worth a try
yahia-berashish commented 10 months ago

How would you go about getting the scope?

Hunter-Gu commented 10 months ago

Now, I can only know DOM tree is not a good way.

  • it will call getProvider when render functional component, but the DOM hasn't rendered, so getProvider will return default value firstly
  • when DOM get committed to the document, I will update with the correct context value and this can work correctly. But VanJS will delay all updates to next render task, so the user can still see the page render with default value at first time.

I don't have a clear idea yet. I will let you know if there are any updates.

Hunter-Gu commented 10 months ago

Hi @yahia-berashish,

I create a new version, sandbox , there are still issues of removing unmounted component node.

The key steps:

  • createComponent: create a component node, pass node and util functions as props
  • we can get parent/children in current component by node
  • commit util function: render in current component node scope, which can keep the correct scope of conditional rendering. we can always render sub components by commit to keep consistent.

If integrate with Web Component, we can easily support functional component lifecycles also.

I think this gives us a glance about how to support context.

Give it a try if you have time, would like to hear your opinion. Thank you!

yahia-berashish commented 10 months ago

Hello @Hunter-Gu The result looks great, and it works fine, but I think it changes the way VanJS is used too much, and I think it would be better to separate the component implementation into a different package. This is the discussion to talk about that further. Would like to hear your opinion.

Hunter-Gu commented 10 months ago

My view is completely the same as yours.

A separated library can keep van-core tiny, and give VanJS the ability to support most modern features.

Let's talk about this. Thank you for your help.

yahia-berashish commented 9 months ago

Hello again. Hi @Hunter-Gu I was busy for a while, I would be happy to hear from you on anything new regarding the development of a Context API in VanJS. Hopefully, I will be able to finish working on this little project soon.

Hunter-Gu commented 9 months ago

@yahia-berashish, I apologize for being busy with my personal matters recently and not responding in a timely manner.

If we don't consider implementation details for now, our ideas are completely aligned:

  • separate the component implementation into a different package
  • the component implementation should be lightweight

and these are most important.

So I think we can aim to implement it as soon as possible, and then improve it based on the requirements.

And this implementation should be simple, straightforward, and we can easily build upon it for further improvements.

kwameopareasiedu commented 9 months ago

With VanJS' design, I don't think the Context API approach is needed here. You could just create a reactive state externally and bind to it from anywhere else in the code. When it is updated, all components bound to it will update.

Here's an example of what I mean:

store.ts

import van from "vanjs-core"

export const theme = van.state("light")

topbar.ts

import van from "vanjs-core"
import theme from "./store.ts"

const { div, p, button } = van.tags;

export default function Topbar() {
  return div(
    p("Dark mode"),
    input({
      type: "checkbox",
      checked: theme.val === "dark",
      onchange: e => (theme.val = e.target.checked ? "dark" : "light")
    })
  );
}

app.ts

import van from "vanjs-core"
import theme from "./store.ts"
import Topbar from "./topbar.ts"

const { div, p } = van.tags;

export default function App() {
  return div(
    { className: () => theme.val === "light" ? "theme-light" : "theme-dark" },
    Topbar(),
    // ... rest of app
  );
}
Hunter-Gu commented 9 months ago

@kwameopareasiedu Hi, thank you for your example.

Actually, your example is about state management, not context.

Let's review the definition of the Context API.

Context lets a parent component provide data to the entire tree below it.

Context is a way to do state management, but state management is not Context.

Context can be very useful when we are creating complex component, and one example that comes to mind is form group.

I hope this helps you understand the intention behind this issue.

kwameopareasiedu commented 9 months ago

@Hunter-Gu Having reviewed the previous discussion thread, I think I have more context (pun intended) to this.

With VanJS' design, I think this may be a possible approach. Let's use a sample auth context for this example

auth-provider.ts

import van from "vanjs-core"

const { div } = van.tags;

const authenticated = van.state(false);

const logout = () => {
  // Logout logic
  authenticated.val = false
}

const login = () => {
  // Login logic
  authenticated.val = true
}

export const authContext = {
  authenticated: authenticated.val
  login: login,
  logout: logout
}

export default function AuthProvider({ childBuilder }: { childBuilder: () => HTMLElement }) {
  return div(
   childBuilder()
  );
}

I understand, the issue is in hopes of adding a Context-like API to Van, but with the current design, the state variables are private to the auth-provider file and nothing external can change it arbitrarily. We then export the required state and/or derived values as well as modification functions.

Any file which needs the auth context can simply import this object and work with it. This is the best way I think this can be handled similarly to how React context behaves. The only drawback here is, the context object can also be used by components not under the auth-provider in this case.

yahia-berashish commented 9 months ago

Hello @kwameopareasiedu Your implementation looks to be a good starting point, me and @Hunter-Gu have made some prototypes in the past that almost achieved everything needed for the Context API but encountered issues when trying to handle reactivity in nested children components.

I think it is good to make the implementation requirements clear too:

  • The component consuming the context (the consumer) should be able to access the data directly as well as modify it reactively.
  • Any children of the direct consumer should be able to access the context's data as well without the need to mark them individually.
  • Each context can have multiple providers each one with its own data.
  • Consumers within multiple nested providers should access only the data of the nearest parent provider.

I'm currently working at @b-rad-c VanCone add-on which can help the context development too, as well as trying to add TypeScript support for it, and will appreciate any help.

yahia-berashish commented 8 months ago

Hello. I have thought about this a bit and I think that implementing a React-like context API might be too complicated for a lightweight library like VanJS. I took a look at Svelte's implementation, and I think Svelte-like stores implementation is a better fit. Svelte provides writable, readable, and derived functions, these functions return objects similar to observables in RxJS, where you can subscribe to them, which can be used to update the DOM each time they are changed, this makes them a great choice for global data management.

// count.store.ts
export const countStore = writable(0);
export const doubleStore = derived(countStore, (count) => count * 2);

// counter.ts
const Counter = () => {
  const count = state(0);
  countStore.subscribe((current) => {
    count.val = current
  });

  const double = state(0);
  doubleStore.subscribe((current) => {
    double.val = current
  });

  return button({onclick(){
    countStore.update((current) => current + 1)
  }}, "Count is: ", count, " * 2 = ", double);
};

Or maybe a shortcut syntax:

// count.store.ts
export const countStore = writable(0);

// counter.ts
const Counter = () => {
  const count = extract(countStore);
  return button({onclick(){
    countStore.update((current) => current + 1)
  }}, "Count is: ", count);
};

They don't have the ability to provide different values to different parts of the DOM tree, but this functionality can be replaced with derived stores, granted, this reduces some of the flexibility React's contexts can provide, but it also reduces complexity, and believe me, the Context API can turn into a mess pretty quickly.

Maybe we can try this approach @Hunter-Gu But it's really up to you since I'm a bit busy right now, so it's just a suggestion.