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

Context-API support #161

Open
Hunter-Gu opened this issue Oct 30, 2023 Discussed in #152 · 22 comments
Open

Context-API support #161

Hunter-Gu opened this issue Oct 30, 2023 Discussed in #152 · 22 comments

Comments

@Hunter-Gu
Copy link
Contributor

Discussed in #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
Copy link
Contributor

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

@Hunter-Gu
Copy link
Contributor Author

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
Copy link
Contributor

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

@Hunter-Gu
Copy link
Contributor Author

Hunter-Gu commented Nov 4, 2023

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
Copy link
Contributor

yahia-berashish commented Nov 4, 2023

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
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor Author

Hunter-Gu commented Nov 5, 2023

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
Copy link
Contributor

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
Copy link
Contributor Author

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
Copy link
Contributor

How would you go about getting the scope?

@Hunter-Gu
Copy link
Contributor Author

Hunter-Gu commented Nov 5, 2023

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
Copy link
Contributor Author

Hunter-Gu commented Nov 8, 2023

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
Copy link
Contributor

yahia-berashish commented Nov 8, 2023

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
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor Author

Hunter-Gu commented Dec 25, 2023

@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
Copy link

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
Copy link
Contributor Author

Hunter-Gu commented Dec 30, 2023

@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
Copy link

@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
Copy link
Contributor

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
Copy link
Contributor

yahia-berashish commented Jan 15, 2024

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.

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

3 participants