Giter Site home page Giter Site logo

ionic-team / stencil-store Goto Github PK

View Code? Open in Web Editor NEW
175.0 11.0 13.0 3.42 MB

Store is a lightweight shared state library by the StencilJS core team. Implements a simple key/value map that efficiently re-renders components when necessary.

Home Page: https://stenciljs.com/

License: Other

TypeScript 96.65% HTML 1.64% JavaScript 1.71%
state global stencil management key-value map dictionary stenciljs

stencil-store's Introduction

@stencil/store

Store is a lightweight shared state library by the StencilJS core team. It implements a simple key/value map that efficiently re-renders components when necessary.

Highlights:

  • Lightweight
  • Zero dependencies
  • Simple API, like a reactive Map
  • Best performance

Installation

npm install @stencil/store --save-dev

Example

store.ts:

import { createStore } from "@stencil/store";

const { state, onChange } = createStore({
  clicks: 0,
  seconds: 0,
  squaredClicks: 0
});

onChange('clicks', value => {
  state.squaredClicks = value ** 2;
});

export default state;

component.tsx:

import { Component, h } from '@stencil/core';
import state from '../store';

@Component({
  tag: 'app-profile',
})
export class AppProfile {

  componentWillLoad() {
    setInterval(() => state.seconds++, 1000);
  }

  render() {
    return (
      <div>
        <p>
          <MyGlobalCounter />
          <p>
            Seconds: {state.seconds}
            <br />
            Squared Clicks: {state.squaredClicks}
          </p>
        </p>
      </div>
    );
  }
}

const MyGlobalCounter = () => {
  return (
    <button onClick={() => state.clicks++}>
      {state.clicks}
    </button>
  );
};

API

createStore<T>(initialState?: T | (() => T), shouldUpdate?)

Create a new store with the given initial state. The type is inferred from initialState, or can be passed as the generic type T. initialState can be a function that returns the actual initial state. This feature is just in case you have deep objects that mutate as otherwise we cannot keep track of those.

const { reset, state } = createStore(() => ({
  pageA: {
    count: 1
  },
  pageB: {
    count: 1
  }
}));

state.pageA.count = 2;
state.pageB.count = 3;

reset();

state.pageA.count; // 1
state.pageB.count; // 1

Please, bear in mind that the object needs to be created inside the function, not just referenced. The following example won't work as you might want it to, as the returned object is always the same one.

const object = {
  pageA: {
    count: 1
  },
  pageB: {
    count: 1
  }
};
const { reset, state } = createStore(() => object);

state.pageA.count = 2;
state.pageB.count = 3;

reset();

state.pageA.count; // 2
state.pageB.count; // 3

By default, store performs a exact comparison (===) between the new value, and the previous one in order to prevent unnecessary rerenders, however, this behaviour can be changed by providing a shouldUpdate function through the second argument. When this function returns false, the value won't be updated. By providing a custom shouldUpdate() function, applications can create their own fine-grained change detection logic, beyond the default ===. This may be useful for certain use-cases to avoid any expensive re-rendering.

const shouldUpdate = (newValue, oldValue, propChanged) => {
  return JSON.stringify(newValue) !== JSON.stringify(oldValue);
}

Returns a store object with the following properties.

store.state

The state object is proxied, i. e. you can directly get and set properties. If you access the state object in the render function of your component, Store will automatically re-render it when the state object is changed.

Note: Proxy objects are not supported by IE11 (not even with a polyfill), so you need to use the store.get and store.set methods of the API if you wish to support IE11.

store.on(event, listener)

Add a listener to the store for a certain action.

store.onChange(propName, listener)

Add a listener that is called when a specific property changes (either from a set or reset).

store.get(propName)

Get a property's value from the store.

store.set(propName, value)

Set a property's value in the store.

store.reset()

Reset the store to its initial state.

store.use(...subscriptions)

Use the given subscriptions in the store. A subscription is an object that defines one or more of the properties get, set or reset.

const { reset, state, use } = createStore({ a: 1, b: 2});

const unlog = use({
  get: (key) => {
    console.log(`Someone's reading prop ${key}`);
  },
  set: (key, newValue, oldValue) => {
    console.log(`Prop ${key} changed from ${oldValue} to ${newValue}`);
  },
  reset: () => {
    console.log('Store got reset');
  },
  dispose: () => {
    console.log('Store got disposed');
  },
})

state.a; // Someone's reading prop a
state.b = 3; // Prop b changed from 2 to 3
reset(); // Store got reset

unlog();

state.a; // Nothing is logged
state.b = 5; // Nothing is logged
reset(); // Nothing is logged

store.dispose()

Resets the store and all the internal state of the store that should not survive between tests.

Testing

Like any global state library, state should be disposed between each spec test. Use the dispose() API in the beforeEach hook.

import store from '../store';

beforeEach(() => {
  store.dispose();
});

stencil-store's People

Contributors

adamdbradley avatar dependabot[bot] avatar manucorporat avatar mlynch avatar reins-ch avatar renovate[bot] avatar rwaskiewicz avatar sean-perkins avatar serabe avatar simonhaenisch avatar splitinfinities avatar tanner-reits avatar thienhuynh95 avatar vaibhavshn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stencil-store's Issues

Data Leak between instances

Used versions

@stencil/store - v1.3.0
@stencil/core - v2.2.0

Problem description

I have a use case, where i have to use a component with @stencil/store, multiple times on the same page.
The Problem is that they leak state into each other, is this a known bug?

It would be awesome if each instance had an own store, either by passing the component a key or if it would be possible to create a nested store (get/set nested values).

If a demo repository is helpful just let me know

bug: initialState function not working for deep object mutations

Prerequisites

Stencil Store Version

2.0.4

Stencil Version

3.0.0

Current Behavior

According to the documentation, the initialState property can be a function that returns the actual initial state. This is useful for cases where we have deep objects that mutate, as the library is unable to track those changes otherwise. However, I have found that this feature does not seem to be working as expected.
Also, the typing is not correct - the exported state is treated as a function in case I pass function to the createStore

Expected Behavior

When using a function as the initialState, changes to deep objects should be reactive and properly tracked and updated by the library.

Steps to Reproduce

  1. Create store with the createStore function and pass a function which is return with the initialState according to the docs.
  2. Observe that changes to the deep object are not being tracked as expected.

Code Reproduction URL

https://codesandbox.io/p/sandbox/happy-kowalevski-vi29n6

Additional Information

No response

Nested store objects not being reset to it's initial state

When using nested objects as part of your store, calling store.reset() will not reset the nested objects to their respective initial state values.

e.g.

interface SomeStore {
  test1: {
    value: boolean;
  },
  test2: boolean;
}

const store = createStore<SomeStore>({
  test1: {
    value: false,
  },
  test2: false
});

Use @Watch combined with stencil-store

Hi,

For a current project I used to @Watch certain props, now I'm wondering if it's possible to 'watch' the store aswell.
I need to trigger a function once a certain value changes, but I'm not able to do it with stencil-store.

I know there is the 'onChange' you can use to dispatch an event and use the @listen decorator instead. But 'clean-code' wise it would be much better to keep using @watch.

Is there support for it already? It is possible? I can't find any examples.

Thanks in advance.

Why it requires stencil "1.12.1"?

I found really interesting this lightweight project compared to redux, but i cannot implement it due to dependencies issue, because stencil-postcss breaks from 1.10. More info: stencil-community/stencil-postcss#25

Would be great if at least there is a workaround 😅. Why stencil-store requires last version?
Thanks!

feat: redux (@stencil/redux deprecated)

Prerequisites

Describe the Feature Request

Up to now I have been successfully using @stencil/redux.

I was updating some dependencies and I have seen that it has been deprecated. Please could you indicate if @stencil/store intention is to be a replacement for redux?

I'm not really sure if @stencil/store is the replacement so we can forget about reducers, actions and so on, and make complex things easy as it seems @stencil/store. I have some apps where the store is quite complex, containing even class objects with their methods so they are shared to the hole app, like a kind of singletons.

Describe the Use Case

replacement of @stencil/redux

Describe Preferred Solution

No response

Describe Alternatives

I have tried to integrate redux-toolkit without success, I haven't found any specific guide or example for stencil...

Related Code

No response

Additional Information

No response

Add delete method

Its not unusual to use a store as a map of items:

export interface Item {
    id: string;
    name: string;
    created: Date;
}

export type ItemStore = Record<string, Item>;

const itemStore = createStore<ItemStore>({});

This seems to work really well with @stencil/store:

const myItem = { id: 'a', name: 'My Item', created: new Date() };

itemStore.set(myItem.id, myItem);
// itemStore.state[myItem.id] = myItem;

itemStore.get('a') // => myItem

until you try to delete an item:

// nothing happens
delete itemStore['a'];

// sets the value to literal undefined
itemStore.set('a', undefined as any)
itemStore.state // { a: undefined } 

I'd like to propose adding a delete method to the store to allow you to do the following:

// deletes item
delete itemStore['a'];

// deletes the item from the store
itemStore.delete('a') // => boolean
itemStore.state // { } 

What are your opinions on this? I can see it being an issue with the current recommended usage:

export interface MyStore {
    clicks: number,
    seconds: number,
    squaredClicks: number
}

const { state, onChange } = createStore<MyStore>({
  clicks: 0,
  seconds: 0,
  squaredClicks: 0
}));

onChange('clicks', value => {
  state.squaredClicks = value ** 2;
});

// this is probably undesirable 
delete state.clicks;
console.log(state.squaredClicks) //=> NaN

But I think its utility outweighs its possible pitfalls.

[Feature Request] computed state depending on multiple states

readme example shows squared click example

onChange('clicks', value => {
  state.squaredClicks = value ** 2;
});

squared clicked computed from one state 'clicks'

to handle multiple dependent states I am doing like following
for speed depending on time and distance

onChange('distance', distance => {
  state.speed = distance / state.time;
});

onChange('time', time => {
  state.speed = state.distance / time;
});

while it works, a better api can be

onChange('distance', 'time', ([distance, time]) => {
   state.speed = distance / time;
})

Improve performance via using a window tiny LFU cache

Prerequisites

Describe the Feature Request

Stores caching of getters is absolutely key for performance (hope you already cache them..)
However, while caching is important for performance, culturally, developers generally opt for a classic LRU cache, which is often signficiantly suboptimal.
The Java library Caffeine is the reference implementation of the state of the art caching algorithm Window tiny LFU, which is generally the best cache strategy.
There seems to exist a straightforward JS port that you could leverage which would make stencil-store the first store out there to have a SOTA cache.

Describe Alternatives

  1. no caching of getters hence subpar performance
  2. caching but with a conventional LRU, AKA suboptimal performance for end users

Related Code

https://github.com/ahume/tiny-lfu-cache

bug: Using store prevents stencil test from being run

Prerequisites

Stencil Store Version

2.0.9

Stencil Version

4.0.2

Current Behavior

I have implemented a basic store in a stencil test project. Whenever I try to set store values en reset them in my test; the test loses the context and cannot find the component anymore. If I run a single test instead of both, it does not have any issue.

my-component.tsx

import { Component, State, h } from '@stencil/core';
import { store, onStoreChange } from '../../store/store';

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true,
})
export class MyComponent {
  @State() test = true;

  setTestState() {
    this.test = !this.test;
  }

  componentWillLoad() {
    onStoreChange('isActive', this.setTestState.bind(this));
  }

  render() {
    return <div>Hello, World! I'm</div>;
  }
}

my-component.spec.ts

import { newSpecPage } from '@stencil/core/testing';
import { MyComponent } from './my-component';
import { store } from '../../store/store';

describe('my-component', () => {
   afterEach(() => {
    store.dispose();
  });

  it('renders', async () => {
    const { root } = await newSpecPage({
      components: [MyComponent],
      html: '<my-component></my-component>',
    });

    store.state.isActive = false;

    expect(root).toEqualHtml(`
      <my-component>
        <mock:shadow-root>
          <div>
            Hello, World! I'm
          </div>
        </mock:shadow-root>
      </my-component>
    `);
  });
  
  it('renders', async () => {
    const { root } = await newSpecPage({
      components: [MyComponent],
      html: '<my-component></my-component>',
    });

    store.state.isActive = false;

    expect(root).toEqualHtml(`
      <my-component>
        <mock:shadow-root>
          <div>
            Hello, World! I'm
          </div>
        </mock:shadow-root>
      </my-component>
    `);
  });
});

store.ts

import { createStore } from '@stencil/store';

export interface StoreState {
  isActive: boolean;
}

const store = createStore<StoreState>({
  isActive: true
});

const onStoreChange = (key: keyof StoreState, cb: (value: string | boolean) => void): void => {
  store.onChange(key, (value: any) => {
    cb(value);
  });
};

export { store, onStoreChange };

Current error
TypeError: Cannot read properties of undefined (reading '$instanceValues$')

Expected Behavior

Run both tests without any issues.

Steps to Reproduce

With the code provided in the Current behavior box the failure could be reproduced.

Code Reproduction URL

https://github.com/pimmesz/stencil-test

Additional Information

No response

Error with stencil store

Stencil version:

I'm submitting a:

[x] bug report
[x] feature request
[ ] support request => Please do not submit support requests here, use one of these channels: https://stencil-worldwide.herokuapp.com/ or https://forum.ionicframework.com/

Current behavior:
I have installed @stencil/store.
It works just fine except that when I try to run storybook (i am using storybook with stencil) I get the following error :
ERROR in ./node_modules/@stencil/store/dist/index.mjs 57:59-70 Can't import the named export 'forceUpdate' from non EcmaScript module (only default export is available) @ ./dist/collection/components/store.js

Expected behavior:
Storybook would just run normally

Steps to reproduce:

Related code:

components/store.ts:

import { createStore } from "@stencil/store";

const { state } = createStore({
  large: document.documentElement.classList.contains("gxg-large"),
});

export default state;

storybook/webpack.config.js

const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const WriteFilePlugin = require("write-file-webpack-plugin");

module.exports = async ({ config }) => {
  config.entry.push(path.join(__dirname, "../dist/gemini.js"));
  config.entry.push(path.join(__dirname, "../dist/gemini/gemini.css"));
  fs.readdirSync(path.join(__dirname, "../dist/collection/components")).map(
    function(file) {
      jsFilePath = path.join(
        __dirname,
        `../dist/collection/components/${file}/${file}.js`
      );
      try {
        if (fs.existsSync(jsFilePath)) {
          config.entry.push(jsFilePath);
        }
      } catch (err) {
        console.error(err);
      }

      cssFilePath = path.join(
        __dirname,
        `../dist/collection/components/${file}/${file}.css`
      );
      try {
        if (fs.existsSync(cssFilePath)) {
          config.entry.push(cssFilePath);
        }
      } catch (err) {
        console.error(err);
      }
    }
  );

  config.plugins.push(
    new CopyPlugin([
      {
        from: "**/*",
        to: "./",
        context: "dist"
      }
    ])
  );

  config.plugins.push(new WriteFilePlugin());

  config.module.rules.push({
    test: /\.stories\.jsx?$/,
    loaders: [require.resolve("@storybook/source-loader")],
    enforce: "pre"
  });

  return config;
};
// insert any relevant code here

Other information:
Sergio Arbeo, one of the contributors of stencil core, sugested me to configure webpack so it loads correctly the dependencies.
I don't know webpack... But the error seems to be in stencil store... Please any help will be much appreciated.
error-store

onChange stopped working with multiple stores

I have components that used a single store, and many components used the same one, each adding their on onChange as needed. As the store got larger, I wanted to split it into 2-3 stores that are limited in scope.

My components looked to be working fine, and they could all read and write the stores, but eventually I noticed that the onChange was not firing for any components other than the "parent" wrapper I have that wraps all the other components, as it's the one that loads some configs that go into the store.

It could be that it's just that it's the first component to be defined and run and calls the stores.

Running stencil/core 2.9 and store 1.5

Maintenance of the repository

Our team is using StencilJS for about a year and we are overall happy with the framework. Recently we have a use case for @stencil/store. However there is a concern that the repository is no longer maintained. We can see open PRs that tackle existing issues but no one is reviewing and merging these PRs. Community asks about updates and no one is answering.

So the question is: Is this repository still maintained?

@stencil/store is present in official documentation so in case the repository is no longer maintained it would be better to remove it from the official docs and inform the community here.

bug: test issue

Prerequisites

Stencil Store Version

3.0.0

Stencil Version

3.0.0

Current Behavior

testing jira

Expected Behavior

testing jira

Steps to Reproduce

testing jira

Code Reproduction URL

here.you.go/

Additional Information

No response

Inferred types for `on` callback values

Example:

export const { state, on } = createStore({ foo: 'abc', bar: 123 });

on('set', (key, newValue, oldValue) => {
  if (key === 'foo') {
    // `newValue` and `oldValue` both have type `any`
  }
}

Wondering whether it would be possible to type it in a way that when I narrow down the key, it also narrows down the types of newValue and oldValue.

I might be able to look into this at some point... just leaving it here as a note that this would be neat.

Edit: Alternatively, it would be nice to expose oldValue in the onChange method (where it's easy to add type info for it). Then I wouldn't have a need for on anyway.

Add license file

Most other ionic repos have License.MD files. I'd assume that this repo has the umbrella license by team ionic, but it really should also be place here as it is in other repos.

bug: test bug

Prerequisites

Stencil Store Version

a

Stencil Version

a

Current Behavior

a

Expected Behavior

a

Steps to Reproduce

a

Code Reproduction URL

a

Additional Information

No response

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update dependency @types/node to v20.12.7

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/actions/download-archive/action.yml
  • actions/download-artifact v4.1.5@8caf195ad4b1dee92908e23f56eeb0696f1dd42d
.github/workflows/actions/get-core-dependencies/action.yml
  • actions/setup-node v4.0.2@60edb5dd545a775178f52524783378180af0d1f8
.github/workflows/actions/publish-npm/action.yml
  • actions/checkout v4.1.1@b4ffde65f46336ab88eb53be808477a3936bae11
.github/workflows/actions/upload-archive/action.yml
  • actions/upload-artifact v4.3.2@1746f4ab65b179e0ea60a494b83293b640dd5bba
.github/workflows/build.yml
  • actions/checkout v4.1.1@b4ffde65f46336ab88eb53be808477a3936bae11
.github/workflows/main.yml
  • actions/checkout v4.1.1@b4ffde65f46336ab88eb53be808477a3936bae11
  • actions/setup-node v4.0.2@60edb5dd545a775178f52524783378180af0d1f8
.github/workflows/release-dev.yml
  • actions/checkout v4.1.1@b4ffde65f46336ab88eb53be808477a3936bae11
  • actions/checkout v4.1.1@b4ffde65f46336ab88eb53be808477a3936bae11
npm
package.json
  • @ionic/prettier-config ^4.0.0
  • @stencil/core ^4.0.0
  • @types/jest ^29.0.0
  • @types/node ^20.2.0
  • jest ^29.0.0
  • np ^10.0.0
  • prettier ^3.0.0
  • rollup ^4.0.0
  • ts-jest ^29.0.0
  • typescript ~5.0.0
  • @stencil/core >=2.0.0 || >=3.0.0 || >= 4.0.0-beta.0 || >= 4.0.0
  • node >=12.0.0
  • npm >=6.0.0
  • node 20.12.2
  • npm 10.5.2
test-app/package.json
  • @stencil/core ^4.0.0
  • @types/jest ^29.0.0
  • @types/node ^20.11.7
  • jest ^29.0.0
  • jest-cli ^29.0.0
  • puppeteer ^22.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

does not reset the store to its initial state if empty array

The function reset does not seem to reset the store to its initial state if it was empty array.

Given a store such as

interface MyStore {
  something: MyObject[];
}

const {state, reset} = createStore<MyStore>({
  something: []
});

export default {state, reset};

calling myStore.reset() has no effect on something.

bug: Type error when running tests for component with onChange listener

Prerequisites

Stencil Store Version

2.0.0

Stencil Version

2.13.0

Current Behavior

When a component has an onChange listener which has a function that manipulates a https://github.com/State property:

import { onChange } from '../a-store';

export class AppProfile { 
  @State() something = 0;

  componentWillLoad() {
    onChange('clicks', (value) => {
      this.something++;
    });
  }
// ...
}

when I try to unit test the component I get a type error like:

● app-profile › clicks the button again

TypeError: Cannot read properties of undefined (reading '$instanceValues$')

  12 |   componentWillLoad() {
  13 |     onChange('clicks', (value) => {
> 14 |       this.something++;
     |       ^
  15 |     });
  16 |   }
  17 |   

  at getValue (node_modules/@stencil/core/internal/testing/index.js:538:282)
  at AppProfile.get [as something] (node_modules/@stencil/core/internal/testing/index.js:565:13)
  at src/components/app-profile/app-profile.tsx:14:7
  at node_modules/@stencil/store/dist/index.js:144:43
  at node_modules/@stencil/store/dist/index.js:88:40
      at Array.forEach (<anonymous>)
  at reset (node_modules/@stencil/store/dist/index.js:88:24)
  at Object.dispose (node_modules/@stencil/store/dist/index.js:94:9)
  at Object.<anonymous> (src/components/app-profile/app-profile.spec.ts:7:5)

The error only occurs if I'm referencing a @State property, if I reference a component method that just logs something, for example, there is no error.

However, this error doesn't happen with the first test in the suite, only subsequent tests. I'm calling store.dispose() before each test.

Expected Behavior

Tests should run without throwing an error and failing.

Steps to Reproduce

Download repo, run npm install then npm run test

OR

  1. Create a component which registers an onChange listener that references a @State variable

  2. Write a unit test suite for the component with at least 2 tests

  3. Run the unit tests

Code Reproduction URL

https://github.com/samshareski/store-test-bug

Additional Information

No response

Expose `oldValue` in `onChange`

Stupid example:

interface Foo {
  value: string[];
}

export const { state, on, onChange } = createStore({ foo: { value: ['foo'] } });

Currently I have to do

on('set', async (key, newValue: Foo, oldValue: Foo) => {
  if (key !== 'foo') {
    return;
  }

  // crazy complex logic that compares the values and does something
  if (Array.from(new Set(newValue)).join() !== Array.from(new Set(oldValue)).join()) {
    await updateServer(newValue);
  }
}

This involves filtering the key and manually specifying the types for newValue and oldValue.

I'd prefer to be able to do

onChange('foo', async (foo, oldFoo) => {
  if (Array.from(new Set(foo)).join() !== Array.from(new Set(oldFoo)).join()) {
    await updateServer(foo);
  }
}

(with type inference)

Uncaught TypeError: t.keys is not a function or its return value is not iterable

I’m getting the above error in production, when minified (type: dist-custom-elements-bundle, externalRuntime: false, dir: 'www', minify: true). It comes from this bit of the store:

/**
 * Check if a possible element isConnected.
 * The property might not be there, so we check for it.
 *
 * We want it to return true if isConnected is not a property,
 * otherwise we would remove these elements and would not update.
 *
 * Better leak in Edge than to be useless.
 */
const isConnected = (maybeElement) => !('isConnected' in maybeElement) || maybeElement.isConnected;
const cleanupElements = debounce((map) => {
    for (let key of map.keys()) {
        map.set(key, map.get(key).filter(isConnected));
    }
}, 2000);
const stencilSubscription = ({ on }) => {
    const elmsToUpdate = new Map();
    if (typeof getRenderingRef === 'function') {
        // If we are not in a stencil project, we do nothing.
        // This function is not really exported by @stencil/core.
        on('dispose', () => {
            elmsToUpdate.clear();
        });
        on('get', (propName) => {
            const elm = getRenderingRef();
            if (elm) {
                appendToMap(elmsToUpdate, propName, elm);
            }
        });
        on('set', (propName) => {
            const elements = elmsToUpdate.get(propName);
            if (elements) {
                elmsToUpdate.set(propName, elements.filter(forceUpdate));
            }
            cleanupElements(elmsToUpdate);
        });
        on('reset', () => {
            elmsToUpdate.forEach((elms) => elms.forEach(forceUpdate));
            cleanupElements(elmsToUpdate);
        });
    }
};

It’s that for loop in cleanupElements that’s breaking. It doesn’t seem to be affecting anything - functionality is all there, but my logs have a billion of these messages, which is not great, and it’s visible in the console. When I debug, the map variable is indeed a Map, with zero entries, so it seems like it shouldn’t balk at that, but it does. Dunno exactly what this does, but it does come from the Stencil store code.

It seems like it's coming ferom the instantiation of my Validator class, which is a homebrew .ts file, not part of a .tsx Stencil component, if that's relevant.

Memory leak

I think this could lead to memory leaks where components are not able to be garbage collected.

It looks like elements are being removed from the update list when a state property is changed based on the forceUpdate result. This works if all properties are being changed frequently, but will not clear the list if the property never changes.

For example, if there is a global state property that never changes and a large number of elements use this state value when rendering, then the list will continue to grow over time and the elements will never be removed.

It seems like it would need some kind of global event when elements are disconnected or some event from the individual elements in order to dispose properly.

bug: Memory leak

Prerequisites

Stencil Store Version

2.0.3

Stencil Version

2.22.2

Current Behavior

Nodes aren't garbage collected when using stencil/store.

Expected Behavior

Nodes should get garbage collected.

Steps to Reproduce

Clone https://github.com/raymondboswel/stencil-redux-example
In terminal a: cd mock-server; node server.ts
In terminal b: npm run start

Open performance monitor, and memory tab. Trigger garbage collector. Take note of nodes count.

Spam click 'Toggle RxJS Todos' button. Trigger garbage collector. Take note of nodes count.

Spam click 'Toggle Stencil/store Todos' button. Trigger garbage collector & notice that nodes count does not decrease.

Code Reproduction URL

https://github.com/raymondboswel/stencil-redux-example

Additional Information

No response

Type error when running tests for component with onChange listener.

When a component has an onChange listener which has a function that manipulates a @State property:

import { onChange } from '../a-store';

export class AppProfile { 
  @State() something = 0;

  componentWillLoad() {
    onChange('clicks', (value) => {
      this.something++;
    });
  }
...
}

when I try to unit test the component I get a type error like:

 ● app-profile › clicks the button again

    TypeError: Cannot read properties of undefined (reading '$instanceValues$')

      12 |   componentWillLoad() {
      13 |     onChange('clicks', (value) => {
    > 14 |       this.something++;
         |       ^
      15 |     });
      16 |   }
      17 |   

      at getValue (node_modules/@stencil/core/internal/testing/index.js:538:282)
      at AppProfile.get [as something] (node_modules/@stencil/core/internal/testing/index.js:565:13)
      at src/components/app-profile/app-profile.tsx:14:7
      at node_modules/@stencil/store/dist/index.js:144:43
      at node_modules/@stencil/store/dist/index.js:88:40
          at Array.forEach (<anonymous>)
      at reset (node_modules/@stencil/store/dist/index.js:88:24)
      at Object.dispose (node_modules/@stencil/store/dist/index.js:94:9)
      at Object.<anonymous> (src/components/app-profile/app-profile.spec.ts:7:5)

The error only occurs if I'm referencing a @State property, if I reference a component method that just logs something, for example, there is no error.

However, this error doesn't happen with the first test in the suite, only subsequent tests. I'm calling store.dispose() before each test.

I reproduced this error on a fresh npm init stencil project with npm install @stencil/store added.

Testing: Cannot read property '$hostElement$' of undefined

  • When testing components which use the store, I can only have a single test for them
  • If I have more than one test, I am getting the error "TypeError: Cannot read property '$hostElement$' of undefined".
  • Problem 1: When trying beforeEach(() => store.reset()) as described in the documentation.
  • Problem 2: Without using/importing the store in my spec file at all, but testing a component with a method which interacts with the store.

For both problems you can comment out one of the tests to see the remaining one pass without any errors. As long as if there is only a single it() test, everything works as expected.

Reproduce

  • Repo with 2 branches. Branch "master" has the code for Problem 1. Branch "method" for problem 2.

Problem 1:

  • npm init stencil and choose component starter
  • Install devDependencies @types/jest, jest, jest-cli, @types/puppeteer, puppeteer, @stencil/store
  • Add store.ts
import { createStore } from "@stencil/store";

export const store = createStore({
  clicks: 0
});
  • Use in my-component.html
export class MyComponent {
  render() {
    return <h1>{store.state.clicks}</h1>;
  }
}
  • Change my.component.spec.ts
describe('my-component', () => {
  beforeEach(() => {
    store.reset();
  });

  it('renders', async () => {
    const {root} = await newSpecPage({
      components: [MyComponent],
      html: '<my-component></my-component>'
    });
    expect(root).toEqualHtml(`
      <my-component>
        <mock:shadow-root>
          <h1>0</h1>
        </mock:shadow-root>
      </my-component>
    `);
  });
  it('renders', async () => {
    const {root} = await newSpecPage({
      components: [MyComponent],
      html: '<my-component></my-component>'
    });
    expect(root).toEqualHtml(`
      <my-component>
        <mock:shadow-root>
          <h1>0</h1>
        </mock:shadow-root>
      </my-component>
    `);
  });
});

TypeError: Cannot read property '$hostElement$' of undefined

   5 | describe('my-component', () => {
   6 |   beforeEach(() => {
>  7 |     store.reset();
     |           ^
   8 |   });

Add CHANGELOG.md

Documentation request. It would be great if this project started keeping a CHANGELOG file, documenting notable changes as development goes on. A CHANGELOG.md would conform nicely with @ionic-team/stencil as well.

I think having a changlog in place for future releases will help Stencil Store users. I don't think there is any obligation to go back and document past changes. :-)

feat: allow removing `onChange` listeners

Prerequisites

Describe the Feature Request

A change listener can be registered to a store via the store.onChange method, but there is no way to remove that listener. It would be useful to have a clear API to remove a change listener. The best way to currently get this functionality is to supply a listener then to nullify that listener when we no longer want that listener to be used. For example:

interface ToDo {
  id: string
  description: string
}

interface Worker {
  doActions(todos: ToDo[]): void
}

interface WorkerStore {
  workers: Record<Worker['id'], Worker>
}

interface ToDoStore {
  todos: ToDo[]
}

const workerStore createStore<WorkerStore>({ workers: {} })
const todoStore = createStore<ToDoStore>({ todos: [] })

function doWork(todos: ToDo[]) {
  workerStore.state.workers['someId'].doActions(todos)
}

class WorkerStore {
  doWork: ((todos: ToDo[]) => void) | null = null

  initialize(todos: ToDo[]) {
    this.doWork = doWork
    const update = (todos: ToDo[]) => this.doWork?.(todos)
    todoStore.onChange('todos', update)
  }

 disconnectListeners() {
    this.doWork = null
  }
}

It seems like we should be able remove a listener rather than changing the method used in the listener.

Describe the Use Case

There are at least two cases in which I would like to turn off a change listener.

One case involves two or more stores that are related. In the example above, the WorkerStore depends on the ToDoStore. It's currently difficult to make a change to the
independent store (e.g. todoStore.state.todos) without making changes or triggering side effects in the dependent store (e.g. workerStore). There may be cases where I want to turn off the listener. For instance, if I call todoStore.reset(), I may not want the workerStore to respond to those changes.

Another case occurs when I register a change handler in a class instance that is garbage collected later on. This seems to cause a memory leak because the listener I registered is no longer available. As an example, I have an Ember application that imports a stencil store. I then define a change handler in a component class or service class. When that component/service class instance is destroyed in the Ember runtime loop, the change listener is still registered to the store but there is no actual reference to the listener function. If there was a method to remove a listener, I could call that method in a component's willDestroy hook. The memory leak here is particularly problematic when running test suites.

Describe Preferred Solution

An API similar to the addEventListener/removeEventListener API could be desirable. Perhaps it would be something like the following:

const workerStore = createStore<WorkerStore>({ workers: {} })
const todoStore = createStore<ToDoStore>({ todos: [] })

function doWork(todos: ToDo[]) {
  workerStore.state.workers['someId'].doActions(todos)
}

// Add onChange listener
todoStore.addListener('todos', doWork) // rename onChange

// Remove listener
todoStore.removeListener('todos', doWork)

Describe Alternatives

The alternative is the workaround I detailed in the issue description.

Related Code

No response

Additional Information

No response

Example usage of Subscriptions?

I noticed the store.use() method in the docs but I don't see any examples of how it's intended to be used. Could someone provide an example and context for when and how to use this method? Thanks!

feat: test feat

Prerequisites

Describe the Feature Request

a

Describe the Use Case

a

Describe Preferred Solution

a

Describe Alternatives

a

Related Code

a

Additional Information

a

Is this project actively maintained?

My PR #41 has not had any reviews or comments since I opened it 2 weeks ago.
Also some other PRs are still waiting for a longer time.

So now I am asking myself if I should use this package if it is not actively maintained (and still young).

How to store a value permanently?

If I store a variable inside the store.ts why, when I refresh the page, the data it's reset to the default value that is in the store? It shouldn't be remain the same value unless I change it in the code?
I use state.variable = value to store inside my code. Maybe I should use something different?

This is my store
`import { createStore } from "@stencil/store";

const { state } = createStore({
accountname: "",
email: "",
usertype: "",
clientId: 0,
});

export default state;
`

and this is how I assign the variable in my code
state.clientId = 1;

Thank you

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.