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

Tunnel per instance? #8

Open
kraftwer1 opened this issue Jan 29, 2019 · 29 comments
Open

Tunnel per instance? #8

kraftwer1 opened this issue Jan 29, 2019 · 29 comments
Assignees

Comments

@kraftwer1
Copy link

kraftwer1 commented Jan 29, 2019

According to the examples, createProviderConsumer() is created statically in a file, e.g. data-tunnel.tsx. This basically means tunnels are created on class level rather than on instance level. While this is totally the way to go for single page apps that only have one instance of a root component, e.g. my-app, it makes it hard when building a component library, where every component instance should have its own tunnel and multiple instances of that component can coexist. In other words:

Current - these two instances of "my-select" now share the same tunnel and mix up the state. This leads to unexpected behavior:

<my-select> <!-- imports "data-tunnel.tsx" -->
  <my-option> <!-- imports "data-tunnel.tsx" -->
</my-select>

<my-select> <!-- also imports "data-tunnel.tsx" -->
  <my-option> <!-- also imports "data-tunnel.tsx" -->
</my-select>

Expected - it would be great if the developer could decide whether to have tunnels on instance level or on class level. Every time an instance of "my-select" is created, also a corresponding tunnel is created. So each parent has its own tunnel and its child components automatically consume that particular tunnel:

<my-select> <!-- creates a new tunnel -->
  <my-option> <!-- consumes that new tunnel -->
</my-select>

<my-select> <!-- creates another new tunnel which does not interfere with the my-select above -->
  <my-option> <!-- consumes that "another" new tunnel -->
</my-select>
@Secular12
Copy link

@jthoms1 Came across this issue as well. Would like to have slotted child components to know the state of a parent component. But, as mentioned above, I cannot have different props for different instances, one will override the other. Making it not useful in component libraries.

Any input on if it is possible to implement this?

@sslotsky
Copy link

Agreed, it would be extremely helpful to allow multiple instances of the same type of tunnel exposing different values. Ideally I should be able to nest them.

<my-tunnel-provider value="foo">
  <my-tunnel-consumer />
  <my-tunnel-provider value="bar">
    <my-tunnel-consumer />
  </my-tunnel-provider>
</my-tunnel-provider>

@jthoms1 jthoms1 self-assigned this Mar 21, 2019
@jthoms1
Copy link
Member

jthoms1 commented Mar 21, 2019

Does anyone have some example code that they can share?

@sslotsky
Copy link

@jthoms1 my example is pretty far from minimal but I can make some time to produce a simple example if nobody else beats me to it

@sslotsky
Copy link

@jthoms1 I put up a repo that demonstrates the issue

https://github.com/sslotsky/stencil-highlander

Both instances of my-tunnel-consumer display the string "Bar" when the first one should display "Foo". Here's the relevant code:

tunnel definition

import { createProviderConsumer } from "@stencil/state-tunnel";

export interface State {
  message: string;
}

export default createProviderConsumer<State>(
  {
    message: ""
  },
  (subscribe, child) => (
    <context-consumer subscribe={subscribe} renderer={child} />
  )
);

provider

import { Component, Prop } from "@stencil/core";

import Tunnel from "../../data/tunnel";

@Component({ tag: "my-tunnel-provider" })
export class MyTunnelProvider {
  @Prop() message: string;

  render() {
    return (
      <Tunnel.Provider state={{ message: this.message }}>
        <slot />
      </Tunnel.Provider>
    );
  }
}

consumer

import { Component } from "@stencil/core";

import Tunnel, { State } from "../../data/tunnel";

@Component({ tag: "my-tunnel-consumer" })
export class MyTunnelConsumer {
  render() {
    return (
      <Tunnel.Consumer>
        {(state: State) => <h1>{state.message}</h1>}
      </Tunnel.Consumer>
    );
  }
}

usage

        <my-tunnel-provider message="Foo">
          <my-tunnel-consumer />
          <my-tunnel-provider message="Bar">
            <my-tunnel-consumer />
          </my-tunnel-provider>
        </my-tunnel-provider>

result

image

@jthoms1
Copy link
Member

jthoms1 commented Mar 22, 2019

Tunnel's created in this way are using the fact that the module is used as a global so anyone can import from it and have access to the exact same data. One way around this might be to create it in a way that you are using a 'key' to differentiate between the different states.

The usage provided is much more dependent on the structure of your application. I understand the issue but could you provide a more 'real world' example of when this would be helpful.

Thank you for your time on this!

@sslotsky
Copy link

First I'll offer a generic answer, which is that this is the way people coming from React will expect this to work, because tunnels are based on React context, and React context works this way. It's likely that there are many real world examples out in the wild.

As for my specific use case, my team is building a component library that talks to our API. I have a docs page that shows many of these components in use, and I wanted some of them to show data from production and I wanted others to show data from staging. Example:

<connection env="prod">
  <marketplace />
  <product label="some-product-label" />
  <connection env="stage">
    <plan-selector product-id="2349823fadf234afee" />
  </connection>
</connection>

Our API is microservices, so by putting env="stage" I am really injecting a set of service URLs into components that need to talk to the API.

Of course they don't strictly have to be nested, but placing the connection components as siblings yields the same effect.

@jthoms1
Copy link
Member

jthoms1 commented Mar 24, 2019

I think this is valid. The more I think about it. There are other reasons why someone might want this. I think I have some ideas on solving this and keeping it backward compatible. I will on a PR.

@Secular12
Copy link

Secular12 commented Mar 25, 2019

Just to add on this, so you can see my use case:

<wc-accordion some-prop="some-value">
  <wc-accordion-item>My accordion item 1</wc-accordion-item>
  <wc-accordion-item>My accordion item 2</wc-accordion-item>
  <wc-accordion-item>My accordion item 3</wc-accordion-item>
</wc-accordion>

In my case, I am watching this issue so that I could pass a single bit of information on the parent component (wc-accordion) that all of the children components (wc-accordion-item) could read from (through a slot) and the child components could use the prop on the parent component as a default value, but if the children components have a specified value it would override that parent value, if that makes any sense.

Basically, and in general, it would allow components that are built with each other in mind, can sync up a lot better especially through slots.

@jthoms1
Copy link
Member

jthoms1 commented Mar 25, 2019

I think we have the need in Ionic as well. This will be priority for the next release.

  • datetime + picker + picker column
  • segment + segment button
  • item + range / checkbox / toggle / etc
  • reorder group + reorder
  • radio group + radio

@mburger81
Copy link

mburger81 commented Apr 8, 2019

We have the same issue,

in our case we are doing a web component for Masorny JavaScript lib, so at the end we have this

<lan-masorny>
    <lan-masorny-item>A</lan-masorny-item>
    <lan-masorny-item>B</lan-masorny-item>
</lan-masorny>

At the end in the LanMasornyItem component we have to use some properties and function exposed in LanMasorny component!

So in this case you can have only ONE masorny component loaded per page, which would be Ok for our use case but probably not for everyone.
BUT the problem is, if we use the component in a page named Dashboard with IONIC, where the pages are cached(!!!) then we have the problem that after returning to first loaded page we have the instance for second dahsboard page. It's difficult to explain, but at the end the end Stencil State Tunnel is NOT working with @ionic/angular route reuse strategy!
The only workaround for this problem is to be SURE that is DESTROYED with angulars *ngIf on ionViewWillLeave event

thx

p.s. are there news about this problem?!?! thx a lot

@f10l
Copy link

f10l commented May 2, 2019

Is there any plan or progress with this feature? It would really help in quite some use cases for component collections.

@sslotsky
Copy link

@jthoms1 Let me know if there's any way I could help get the ball rolling on this. Would really love to see this added in time for Stencil One to come out of beta! If it's not too incredibly hard and you can suggest where to start looking, I'd be open to taking a shot at it.

@anthonylebrun
Copy link

@jthoms1 @sslotsky also happy to give it a shot and help out however I can!

@TheCelavi
Copy link

TheCelavi commented Jun 20, 2019

Hi, I have a strange behavior that contradicts problems stated here.

Namely, I have a top component with render function:

    public render() {

        return (
            <Tunnel.Provider state={this}>
                <slot/>
            </Tunnel.Provider>
        );
    }

As you can see, I am sending as a state an instance of component.

Consumer (a component inside top component, an inner component) consumes this state like this:

Tunnel.injectProps(CarouselPager, ['page', 'setPage']);

I have two those components on the page (carousel is top component, provider, pager is nested component that consumes state):

<carousel>
    <pager></pager>
</carousel>

<carousel>
    <pager></pager>
</carousel>

For some reason which is unknown to me - this sh*** works as expected, every pager has access only to parent component instance as state. Why? Beats me...

@TheCelavi
Copy link

TheCelavi commented Jun 25, 2019

I have spent some time thinking about this issue and in general about problem of shared stated among composed/composition of components within some context, as well on application level.

In that matter, I have wrote a small library as proof of concept, where components can share a state store. Solution is simplified version of NGXS, and uses RXJS as implementation of observable pattern.

My biggest issue was that order of invocation of component lifecycle method is not guarantied, I have experienced that order can vary, sometimes, child components are rendered first, sometimes, parent components - without any code modification. So I had to introduce a global registry of providers to solve this problem. However, because of that, there is a neat feature which allows to the user to dynamically add subcomponents/consumers in runtime, as well as parent components/providers.

There is a small demo as well, provided with this library: https://github.com/RunOpenCode/stencil-state-store

EDIT: here is a demo video: https://youtu.be/D07vAxlEUS0

@hvgotcodes
Copy link

Is there any progress on this?

@petermikitsh
Copy link

I tried nesting tunnels, as I wanted to override values in tunnel, lower down the component tree. This ends up causes an infinite loop of re-rendering. (Maybe this should be a separate issue, I'm not sure).

Demo repo: https://github.com/petermikitsh/stencil-nested-tunnel

  render() {
    const context = {foo: 'Test'};
    return (
      <div>
        <Tunnel.Provider state={context}>
          <Tunnel.Consumer>
            {(context: TunnelContext) => {
              const newContext = {...context, foo: 'foo'};
              return (
                <Tunnel.Provider state={newContext}>
                  <Tunnel.Consumer>
                    {(context: TunnelContext) => {
                      return <div>{context.foo}</div>
                    }}
                  </Tunnel.Consumer>
                </Tunnel.Provider>
              );
            }}
          </Tunnel.Consumer>
        </Tunnel.Provider>
        <Tunnel.Provider state={context}>
          <Tunnel.Consumer>
            {(context: TunnelContext) => {
              const newContext = {...context, foo: 'bar'};
              return (
                <Tunnel.Provider state={newContext}>
                  <Tunnel.Consumer>
                    {(context: TunnelContext) => {
                      return <div>{context.foo}</div>
                    }}
                  </Tunnel.Consumer>
                </Tunnel.Provider>
              );
            }}
          </Tunnel.Consumer>
        </Tunnel.Provider>
      </div>
    );
  }

Browser:

Screen Shot 2019-10-19 at 9 57 28 AM

@petermikitsh
Copy link

I took a stab at implementing nested context overriding here: https://github.com/petermikitsh/stencil-context

It's published on npm as stencil-context.

@nilssonja
Copy link

Are there any updates on this issue? I feel like this is a necessity for anyone hoping to build a collection of components with parent-child relationships where children are passed in by consumers as <slot />'s.

@trazek
Copy link

trazek commented Jan 2, 2020

When building component library's the ability to have multiple parent components interact with the children via tunnel would be very useful. A "real world use case" could be as simple as creating a abc-list component which interacts with the @State() and/or @Prop of the children; abc-list-item:

<abc-list id="list1">
   <abc-list-item />
   <abc-list-item />
   <abc-list-item />
   <abc-list-item />
...
</abc-list>

<abc-list id="list2">
   <abc-list-item />
   <abc-list-item />
   <abc-list-item />
   <abc-list-item />
...
</abc-list>

In the above pseudo code example, having the first line only interact with the immediate children is obviously necessary and beneficial. Is this many fixed in the latest version of Stencil?

@trazek
Copy link

trazek commented Jan 4, 2020

I took a stab at implementing nested context overriding here: https://github.com/petermikitsh/stencil-context

It's published on npm as stencil-context.

Very cool. Will take a look. Will you be updating this along with stencil updates?

@trazek
Copy link

trazek commented Jan 7, 2020

@jthoms1 Any update on this? My team had to go away from using the tunnel because of this. We'd love to help make this happen but are under a time crunch right now

@boradakash
Copy link

Hey @sslotsky
Did you find any solution for you use case? I'm also having similar setup like you've(i.e diff environments) but couldn't find anything except using @State() decorator. which also difficult in my use case because I've nested components as well.
Let me know if you've found any solution to this.
Thanks.

@TheCelavi
Copy link

Our implementation of components composition and shared state is now stable: https://github.com/RunOpenCode/stencil-state-store, we dropped state tunnel concept. Documentation is updated, after some trial period of usage, we figure out what public API should be so we released version 1.0.

Here is the real-world example of its usage: https://www.miross.rs/en - all carousels are composed web components sharing state.

@loganvolkers
Copy link

@petermikitsh excellent library! https://blog.mikit.sh/post/Stencil-Context/

I like the use of events for propagating the request to subscribe to context.

@cihantas
Copy link

Landed in the same boat a few days ago. I forked and updated the project to use instances instead of a static, globally shared tunnel. Works, but introduces breaking changes. Will share it soon.

@mihar-22
Copy link

mihar-22 commented Jul 9, 2020

Hey guys I've created a solution to passing props down component trees that's instance scoped. Feel free to check it out -> https://github.com/mihar-22/stencil-wormhole.

@loganvolkers
Copy link

I'm working on a standard event contract to unify the efforts of the authors of libraries here and in the polymer community.

Here is a summary of the libraries and how they work: https://github.com/saasquatch/dom-context/tree/v1

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