Lit-element: Need for context properties

Created on 10 May 2018  路  16Comments  路  Source: Polymer/lit-element

I like the turn project took, and I suppose Redux is going to be mainstream in this area. And there is one thing that might improve the way LitElement works with Redux.

This idea is not unique, React also has it.

Now, in order pass a property down the hierarchy, each component needs to define it and pass over to its children

Better approach might be to have context properties that will be available for all affected scope/branch it will be applied.

I am not sure about implementation, but maybe scoped custom element registry and WeakMap might come in handy?

And it will be possible to have this sort of code

my-component-container.js

   import MyComponent from './my-component.js';

   const mapStateToProps = state => ({ someState: state.someState });
   const mapDispatchToProps = dispatch =>
            ({ doSomething: (value) => dispatch({ type: 'DO_SOMETHING', payload: value }) });

   export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

my-component.js

   ...
   export default class MyComponent extends LitElement {
        static get properties() {
            return {
               someState: String,
               doSomething: Function,
            };
        }
        _render({ someState, doSomething }) {
           return html`<input value=${someState} on-change=${doSomething}/>`;
       }
   };

connect.js

   export const connect = (mapStateToProps, mapDispatchToProps) => baseElement => {
       return class extends baseElement {
           connectedCallback() {
                const { store } = this.context;
                if (!store) {
                   throw new Error('no store passed over');
                }
                this.__storeUnsubscribe = store.subscribe(() => this._stateChanged(store.getState()));
                ....
           }

           disconnectedCallback() {
              if (this.__storeUnsubscribe) {
                  this.__storeUnsubscribe();
              }
               ...
           }
       }
   }

index.html

   <store-provider>
      <my-component></my-component>
   </store-provider>
   <script>
      import { createContext } from  '@polymer/lit-element';
      createContext('store-provider', { store: createStore() }));
   </script>

Most helpful comment

I created wc-context library that supports lit-element and skatejs as well vanilla web components

The API is still in flux

All 16 comments

I've been thinking a lot about implementing React-like context to Web Components and finally found an answer: why it should be React-like? Especially when React team itself decided to kill old-style context and create a new one.

In my opinion, React relies on components too much. They even replaced class inheritance with HOCs, trying to make classes looking like a function. It still doesn't work, because component have their own lifecycle, and it breaks the very idea of component = function (I think React is great project changed everything in web development though).

Web Components are different. While React has its own compiler starting with React.render(), Web Components don't have any external system to inject context as React does. But they don't have to do it, because we have a great platform-close tool to achieve any necessary context we need.

Just create a factory that consumes your store and produces connect function, and it will work without any special context:

// createConnect.js
export const createConnector = store => (mapStateToProps, mapDispatchToProps) => baseElement => {
       return class extends baseElement {
           connectedCallback() {
                this.__storeUnsubscribe = store.subscribe(() => this._stateChanged(store.getState()));
                ....
           }

           disconnectedCallback() {
              if (this.__storeUnsubscribe) {
                  this.__storeUnsubscribe();
              }
               ...
           }
       }
   }
// connect.js
export default const connect = createConnect(createStore());

That's it. You don't need extra code to support complex system of context as it was in React.

Also I think it is very similar approach the new React context has. They use a function to create Provider and Consumer components that are exchanging value property between each other. But we don't even need components to do it, everything is built-in.

@Lodin . Thank you for your response. But, one of the problems this issue addresses is testability and SSR. Problem with having static connect or store exported from the module makes it singleton, and harder to mock the store while testing and when doing SSR, especially when your app is a part of a larger app. This is issue also mentioned here.

I agree that having component in dom that does not have any visual footprint but just passes context can be confusing. probably this should not be a part of LitElement, more of a pwa-helpers. I麓ll do a PR with and idea I have and link it here soon.

This talk was posted to the Slack channel, and it might be of interest to this conversation as it came up in the context of Context Properties there: https://youtu.be/6o5zaKHedTE

@Westbrook , Thanks, that is an amazing approach, Never occurred to me.

I have created a small lib called it wc-context, no packaging yet, its coming soon. In meantime want to get some feedback. There is a Redux exmaple in demo folder.

@askbeka I still think the functions are better for it. I've got an idea to make them static and mimic new React.Context API completely.

const createContext = (default) => {
  let ctx = default;

  const provide = (newCtx) => {
    ctx = newCtx;
  }

  const consume = () => {
    if (!ctx) {
      throw new Error('Context is not defined');
    }

    // do whatever you want from consumer
  }

  return {consume, provide};
}

export {
  consume,
  provide,
} = createContext();

What do you think about this approach?

@Lodin , This approach is not much different from just using store in component, I am not saying that it is bad, it may be all you need if you have one team working on whole app and it is not big. But in other cases this apprach does not scale very well.

Imagine that you have an app that is being developed by multiple teams some of them can be offshore and they have their own source control. You can store context as a global object in page and make them use that, but then you have a problem if you need multiple stores in your app, and components should be used with different stores.

Good thing that we have DOM and custom element and events to solve that. I am experimenting with it, for now I am not really happy with API, but I will try to release alpha version soon to get more feedback.
I don't have much time unfortunately:(

@askbeka Oh I see it now. Thanks, talking with you was really useful. Waiting for your context lib!

This outside the scope of LitElement since it is focused on creating custom elements that work with normal DOM API's. We might reconsider adding an API like this in the future if there's enough interest and a concrete proposal.

We recommend looking at PWA Starter Kit for docs, examples, and best practices around using LitElement when making PWAs and other web apps. It uses Redux for state management.

I created wc-context library that supports lit-element and skatejs as well vanilla web components

The API is still in flux

Thanks for @Westbrook mentioning Justin Fagnani's presentation, @askbeka's demo code and @blikblum's library.

@rikakomoe you are welcome. Thanks for reminding. Almost forgot about this issue.
I couldn't find time to update on my final solution.

But, since then I have contributed context implementation for haunted.
Which implements reacts's hooks pattern with webcomponents.

There is also recent ongoing attempt to make react-redux work you can track here

@askbeka The official guide requires a constant existing store before defining the element. The issue is, besides it is not tidy enough though, I am not building a website but a component, if the user creates more than one instance, all of them need their independent state. So I was looking for a declarative method to inject this store into children components, for convenience, without passing down the store over and over. React provides this dependency injection feature as its context while lit-element doesn't yet.

I'm interested about your final solution, if there were any updates/improvements against the solution I've seen so far.

@rikakomoe, you can also try my context approach that implemented in the @corpuscule/context library. It allows avoiding unnecessary HTML elements whose only purpose is to serve as a provider. With this library, you can make your root component a theme provider, a redux provider, a router provider, etc. at the same time without creating expensive HTML elements and increase nesting. It works with any implementation of web components.

The library is based on decorators, so you will need Babel to transpile your components while the decorator proposal is in development. Corpuscule project provides solution for it as well.

BTW, the redux bindings (based on the context library) are also there: @corpuscule/redux. It also works with any web component library as well as vanilla web components.

@Lodin Thanks!

@Lodin More news on this topic,
With new CSS display type display: contents.
You should not be afraid of extra DOM, since it does not affect layout.
It is slready supported by 83% of browsers

Was this page helpful?
0 / 5 - 0 ratings

Related issues

minht11 picture minht11  路  4Comments

quentin29200 picture quentin29200  路  3Comments

Leon-Alexey picture Leon-Alexey  路  5Comments

kurukururuu picture kurukururuu  路  3Comments

ghost picture ghost  路  3Comments