Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useStoreListener hook #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

everdimension
Copy link
Contributor

Motivation:
A common pattern is to react to some store change in an effect:

function MyComponent() {
  const { field } = useStore(myStore);

  useEffect(() => {
    if (field === 'satisfies-some-condition') {
      triggerAnotherAction();
    }
  }, [field]);

  return (
    ...other components...
  )
}

While this does the job, unfortunately it makes the whole component re-render when store changes, even if the field doesn't yet satisfy our condition.

Solution here would be to create a hook that is designed to only trigger effects and not interfere with the render phase:

function MyComponent() {
  useStoreListener(myStore, {
    keys: 'field',
    listener: (value, changed) => {
      if (changed === 'satisfies-some-condition') {
        triggerAnotherAction()
      }
    }
  })

  return (
    ...other components...
  )
}

That way, we can use the familiar effect pattern, but avoid unwanted re-renders and calls of other hooks in the component

If you decide this is a good feature, I'll finish the pull request by updating types, tests and docs, thanks!

@ai
Copy link
Member

ai commented Oct 30, 2021

The origin idea of Nano Stores is to move logic from React components.

In general, I see the nanostores’s way to solve this task in:

import { listenKeys } from 'nanostores'

function bindAnotherAction () {
  const unbind = listenKeys(myStore, ['field'], value => {
    if (value.field === 'satisfies-some-condition') {
      triggerAnotherAction()
    }
  })
  return unbind
}

function MyComponent() {
  useEffect(bindAnotherAction)
}

Do you have reasons to do have this logic in React component?

@everdimension
Copy link
Contributor Author

I think this is more of a idealistic vs pragmatic approach

I.e. in a perfect world it might be a good practice to make sure that no local component logic interferes with logic that is handled by the stores (outside react)

In practice, we just wanna be able to do stuff and have the tools that help us.

Extracting bindAnotherAction has a problem of not only being tedious, but mainly the issue is that the listener might close over other variables and state in the component. You lose access to it once you extract it outside the component.

@ai
Copy link
Member

ai commented Oct 31, 2021

OK, let’s add tests and docs.

I agree that sometimes people may want old approach.

@everdimension
Copy link
Contributor Author

Hi again!
Added some tests and types.

I'm not sure whether the keys option works only for the map store, or for the atom store as well if its value is an object.

If everything's fine the next thing to do is to update the docs

@@ -40,6 +40,30 @@ export function useStore<
? Pick<StoreValue<SomeStore>, Key>
: StoreValue<SomeStore>

type Listener<SomeStore extends Store> = (
value: StoreValue<SomeStore>,
changed?: SomeStore extends MapStore ? StoreValue<SomeStore> : never
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this parameter only have a value if the store is map store?

And what is its value if the store value is a primitive?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this parameter only have a value if the store is map store?

Yeap

And what is its value if the store value is a primitive?

In this case you do not have store.setKey and we have no easy way to detect changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index.js Outdated
React.useEffect(() => {
let listener = (value, changed) => listenerRef.current(value, changed)
if (opts.leading) {
listener(store.get(), null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct to pass null here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to call listener(store.get())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks!

@ai
Copy link
Member

ai commented Nov 8, 2021

LGTM. Do you want to add anything else?

@everdimension
Copy link
Contributor Author

Great, thanks!
We should update the docs, too. I can do it as a separate pull-request or as part of this one.

@ai
Copy link
Member

ai commented Nov 8, 2021

Let’s update docs in this PR

@@ -32,3 +32,31 @@ export function useStore(store, opts = {}) {

return store.get()
}

export function useStoreListener(store, opts = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is worth moving the new hook into a separate file: this way it will not affect those who do not need it.

}
}, [store, '' + opts.keys, opts.leading])

return null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the return does nothing, it seems it can be removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants