stencil-state-tunnel icon indicating copy to clipboard operation
stencil-state-tunnel copied to clipboard

Tunnel per instance?

Open kraftwer1 opened this issue 6 years ago • 29 comments

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>

kraftwer1 avatar Jan 29 '19 15:01 kraftwer1

@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?

Secular12 avatar Feb 21 '19 00:02 Secular12

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>

sslotsky avatar Mar 21 '19 18:03 sslotsky

Does anyone have some example code that they can share?

jthoms1 avatar Mar 21 '19 20:03 jthoms1

@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 avatar Mar 21 '19 20:03 sslotsky

@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

sslotsky avatar Mar 22 '19 14:03 sslotsky

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!

jthoms1 avatar Mar 22 '19 20:03 jthoms1

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.

sslotsky avatar Mar 23 '19 10:03 sslotsky

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.

jthoms1 avatar Mar 24 '19 00:03 jthoms1

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.

Secular12 avatar Mar 25 '19 17:03 Secular12

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

jthoms1 avatar Mar 25 '19 18:03 jthoms1

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

mburger81 avatar Apr 08 '19 13:04 mburger81

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

f10l avatar May 02 '19 01:05 f10l

@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.

sslotsky avatar May 23 '19 19:05 sslotsky

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

anthonylebrun avatar Jun 06 '19 13:06 anthonylebrun

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 avatar Jun 20 '19 12:06 TheCelavi

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

TheCelavi avatar Jun 25 '19 14:06 TheCelavi

Is there any progress on this?

hvgotcodes avatar Sep 24 '19 14:09 hvgotcodes

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 avatar Oct 19 '19 17:10 petermikitsh

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

It's published on npm as stencil-context.

petermikitsh avatar Oct 22 '19 21:10 petermikitsh

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.

nilssonja avatar Dec 09 '19 20:12 nilssonja

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 avatar Jan 02 '20 17:01 trazek

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 avatar Jan 04 '20 12:01 trazek

@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

trazek avatar Jan 07 '20 04:01 trazek

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.

boradakash avatar Apr 16 '20 07:04 boradakash

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.

TheCelavi avatar Apr 16 '20 08:04 TheCelavi

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

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

loganvolkers avatar Apr 24 '20 01:04 loganvolkers

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.

cihantas avatar Jun 28 '20 12:06 cihantas

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.

mihar-22 avatar Jul 09 '20 01:07 mihar-22

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

loganvolkers avatar Sep 09 '20 02:09 loganvolkers