ionic-team/stencil-store

Use @Watch combined with stencil-store

IndyVC opened this issue · 5 comments

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.

My bad, you're able to mimick a watch by using:

componentWillLoad() {
    onChange('coords', () => this.position());
    onChange('labeling', () => this.position());
    onChange('bounds', () => this.resized());
    onChange('view', () => this.resized());
  }

Thanks for suggesting this pattern, but I am concerned about this leaving "phantom" listeners when component's are removed.

I'm using store to hold onto the user's credentials whenever they sign in/out and that I have multiple components that needs to fetch and display data whenever their props change or when the user signs in/out.

First I tried this:

  @Prop() recordId: string
  @State() data: Record<string, any>

  private get credentials(): UserCredentials {
    return store.state.credentials
  }

  @Watch('recordId')
  @Watch('credentials')
  async updateData() {
    this.data = await fetchData(this.recordId, this.credentials)
  }

  render() {
   return <h1>{data.title}</h1><p>{data.description}</p><p>Signed in as {credentials.username}</p>
  }

But I got an error saying that I could only @Watch() props or state, so I replaced the getter w/ state and calling onChange in componentWillLoad as above:

  @State() credentials: UserCredentials

  componentWillLoad() {
    store.onChange('credentials', credentials => { this.credentials = credentials })
  }

That works, but I wondered what would happen when my component is removed from the document. I verified that this was a problem by creating a test page that added and removed my component from the page multiple times and sure enough, even after each instance of the component was removed from the document (via conditional rendering in JSX), the corresponding listener was being executed. So if I had added and removed the component 10 times and then signed in/out, the 10 listeners would fire off, each updating the state, triggering a watch, and resulting in 10 unnecessary requests for data.

I was able to have it only re-fetch data for components that were still in the document by adding @Element() element and updating the listener to:

credentials => {
  if (this.element.isConnected) {
    this.credentials = credentials
  }
}

However, the "phantom" listeners are still being executed, and I'm concerned about how this will scale considering that the component can be added and removed many times in a given user session and that I had planned to use this pattern for other components.

If store had a way remove a listener previously added w/ onChange, then I could use connectedCallback() to call onChange and disconnectedCallback() to to ensure that the component cleans up after itself, but I don't see anything like that.

I wonder if the Stencil team would recommend calling store.onChange() from a component given that the listener will still be executed even after the component is removed. The example only shows calling it from the module that creates the store.

If not, is there a better way to handle scenarios where components need to do more than just re-render (i.e. trigger watches) when a store value changes.

I now see that someone else has noticed this problem where using onChange() from w/in a component is causing references to removed components to not get garbage collected and potentially cause a memory leak: #209 (comment)

#111 also appears to be related and #111 (comment) suggests that you can do something similar to what I propose above using an undocumented fn that is returned from onChange, but given the underlying stencil issue causing the disconnectedCallback to not be called in tests is not fixed until v 3.2.2 and I'm on 2.20.0, I'm weary of trying it.

I was able to resolve my issue by using the undocumented function returned from onChange as @Serabe suggested in #111 (comment) as follows:

  @State() credentials: UserCredentials

  disconnectCredentials: () => {}

  connectedCallback() {
    this.disconnectCredentials = store.onChange('credentials', credentials => { this.credentials = credentials })
  }

  disconnectedCallback () {
    this.disconnectCredentials()
  }