npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stefanholzapfel/lit-state

v5.0.1

Published

A reactive state management for Lit

Downloads

314

Readme

lit-state

A lightweight reactive state management for Lit

npm install @stefanholzapfel/lit-state

Create an interface that represents your state, e.g.:

interface State {
    app?: AppState;
    books?: { 
        data: Book[],
        bookCount?: number; 
    };
}

export interface AppState {
    currentRoute?: string;
    language?: string;
    headerText?: string;
    offline: boolean;
    mobile: boolean;
    user?: User;
}

export interface User {
    username?: string;
    fullName?: string;
}

export interface Book {
    title?: string;
    author?: Author;
}

export interface Author {
    name?: string;
}

You can nest the state as deep as you want.

Somewhere in your app (before using the state), instantiate a LitElementStateService via:

new LitElementStateService<State>(
    // Initial state
    {
        app: {
            offline: false,
            mobile: false
        }
    },
    // Default options for subscriptions
    {
        global: true
    }
);

You can either hold a reference to the newly created service or set the global flag in the state config to "true". With the global flag set, lit-state will hold a reference for you and use it as default service when you provide none.

The second param overwrites the default options for all subscriptions to the state:

{
    getInitialValue: true, // Receive the current value upon subscription or only later changes?
    pushNestedChanges: false, // Receive changes in nested properties of the subscribed state partial?
    getDeepCopy: false, // Get a reference or a deep copy on state change?
    autoUnsubscribe: true // Automatically unsubscribe when the component a subscription lives in is disconnected?
}

Consume state

There are two ways to consume the state:

1. Directly

Use the state via a reference:

litService.state() // Returns the whole State object

const subscription = litService.subscribe(['app', 'offline'], value => {
    value.previous // the value before change
    value.current // the new value
}) 

When subscribing to LitElementStateService provide the path to the "partial" of the state to observe as a string array. You can go arbitrarily deep but the typing only supports 6 levels.

As third parameter you could again override the default subscription parameters (see chapter "Initiate").

Don't forget to unsubscribe when you don't want to listen for state changes anymore, or you get memory leaks: subscription.unsubscribe();

2. In lit-element

lit-state comes with a class that extends LitElement. You can create your lit-element like this:

export class MyComponent extends LitElementStateful<State> {
    constructor() {
        super();
    }
}

Then you can provide a reference to a LitElementStateService in the constructor. If you have created a global LitElementStateService, you don't have to provide any service since lit-state will automatically pick that one (but you still can if you like).

export class MyComponent extends LitElementStateful<State> {
    @state()
    private mobile;
    
    constructor() {
        super();
        this.subscribeState(['app', 'mobile'], value => {
            value.previous // the value before change
            value.current // the new value
            this.mobile = value.current;
        })
        this.connectState(['app', 'mobile'], 'mobile');
    }
}

As you see in the example above you now have two ways to subscribe the state:

this.subscribeState() works the same as when using litService.subscribe directly

this.connectState() will automatically connect the state partial defined to the lit-element's property in the second parameter. The property to connect to must be a @property / @state in your lit-element. requestUpdate() will automatically be called on every change and un-subscription is also handled automatically when the component is disconnected.

Now just use the e.g. @state() mobile in your element as you would normally do.

Again, use the reference kept from instantiation.

To set new values in the state, just provide the new partial you want to replace, e.g.:

litService.set({
    app: {
        mobile: true,
        offline: true
    }
})

The changes will get merged into the existing state, meaning the other properties in app stay untouched.

In some cases you might want to replace a property with nested objects as a whole. For that, there is the _reducerMode flag. This keyword is reserved and shouldn't be used in your state.

An example:

On this state...

{
    app: {
        mobile: true,
        offline: true
    },
    books: { 
        data: [
            {
                name: 'Testbook'
                ...
            },
            {
                name: 'Testbook2'
                ...
            },
        ],
        bookCount: 2; 
    };            
}

...this operation...

stateService.set({
    app: {
        _reducerMode: 'merge' // Default: not necessary, just for demo purposes
        mobile: false
    },
    books: {
        _reducerMode: 'replace', // Replaces the property "books" with the given object
        bookCount: 0; 
    };            
})

... gives that resulting state:

{
    app: {
        mobile: false,
        offline: true
    },
    books: {        
        bookCount: 0; 
    };            
}

Note: You shouldn't use _reducerMode or _arrayOperation properties nested in a { _reducerMode: 'replace' } branch, since this branch as a whole will be replaced and this special properties won't be handled.

When you have changes in a deeply nested path you can use the entryPath on the SetStateOptions to navigate to the entryPoint for your change. E.g. if instead of

stateService.set({
    books: {
        bookCount: 0;
    };            
})

you could write:

stateService.set(0, { entryPath: ['books', 'bookCount'] })

It's a matter of taste if / when to use this array notation for navigating to the entry point, but some users might find it more elegant.

Hint: You can use a PredicateFunction or a numeric index for array navigation in your entry path (see section "Array operations")

Same as using directly, just use the method this.setState() on your element instead.

Since it is cumbersome to subscribe / mutate array elements, we have array operators.

type ArrayElementSelector<ArrayName, ElementType> = { array: ArrayName, get: IndexOrPredicateFunction<ElementType> };

The syntax for the predicate function is:

 type PredicateFunction<ArrayType> = (array: ArrayType, index?: number) => boolean;

For the function, the subscription will use the first element where it returns true.

Example with predicate function (takes the fist book with author named "Me" or the element at index 10 from the data aray - whatever comes first):

this.subscribeState(['books', { array: 'data', get: (book, index) => book.author === 'Me' || index === 10 } ], value => {
    value.previous // the value before change
    value.current // the new value
})

Example with numeric index (takes the element at index 10 of the "data" array):

this.subscribeState(['books', { array: 'data', get: 10 }], value => {
    value.previous // the value before change
    value.current // the new value
})

Hint: You can perform multiple array searches in the path (subscribe to elements in nested arrays). If you don't provide a predicate function or numeric index you will subscribe to the whole array. In this case it is the last segment in the path.

{
    _arrayOperation:
        { op: 'update', at: IndexOrPredicateFunction<State[number]> | number, val: StateChange<State[number]> | ((element: State[number]) => StateChange<State[number]>) } |
        { op: 'push', at?: number, val: State[number] } |
        { op: 'pull', at?: IndexOrPredicateFunction<State[number]> | number }
}

Example:

stateService.set({
    books: {
        data: {
            _arrayOperation: { op: 'update', at: book => book.author === 'Mark Twain', val: book => book.title === 'Tom Sawyer' ? { title: "Huckleberry Finn" } : {} }
        }
    };            
})

Hint: You can also nest those operations and use the _reducerMode property in the "val" object.

NOT FINISHED - DON'T USE!!

To persist state and reload it on service instantiation provide an array of cache handlers in the state option's cache property.

Optionally you can also provide a name to enable the cache handler to use different persistent caches for different state services.

Example:

new LitElementStateService<State>({
                exampleState: {
                    offline: false,
                    mobile: false
                }
            },
            {
                global: true,
                cache: {
                    name: 'myState1',
                    handlers: [
                        new LocalStorageCacheHandler<State>()
                    ]
                }
            }));

The LocalStorageCacheHandler is provided with this package (others may follow). You can implement your own following the CacheHandler interface.

If you provide multiple handlers, they will load their initial state in the order you provide them (and therefore may overwrite each other).

To persist a state change provide the name of the cache handler to use in the options parameter:

stateService.set(
    { exampleState: { offline: true } },
    { cacheHandlerName: 'localstorage' }
)

or in LitElementStateful:

this.setState(
    { exampleState: { offline: true } },
    { cacheHandlerName: 'localstorage' }
)

The handler name is the name property of the cache handler.

The cache handler has to be provided when instantiating the service, otherwise you will get an error.