Weaverse / global-state-hook

Simple and super lightweight global state sharing and managing for React application using hooks
https://weaverse.io
MIT License
20 stars 4 forks source link

New mechanism for `useSyncStore`? #5

Closed Roguehp98 closed 1 year ago

Roguehp98 commented 1 year ago

At this time, when updating state for global state, it will re-render all components using useSyncStore.

Example

let textStore3 = createSubscription({ value: "Text 3", value2: "Text 4" })
function Text3() {
  let { state, subscription } = useSyncStore(textStore3)
  return (
    <div>
      <input
        value={state.value}
        onChange={(e) => subscription.updateState({ value: e.target.value })}
      />
    </div>
  )
}
function Text4() {
  let { state, subscription } = useSyncStore(textStore3)
  return (
    <div>
      <input
        value={state.value2}
        onChange={(e) => subscription.updateState({ value2: e.target.value })}
      />
    </div>
  )
}

When updated state for value in Text3 component. Text4 component will be re-rendered too. I think It's not good for performance if global state is nested object.

I suggest passed callback is a parameter for useSyncStore, so we can avoid components that have not changed re-render

Like that:

let state = React.useSyncExternalStore(
        subscription.subscribe,
        () => callback ? callback(subscription.state) : subscription.state,
        getServerSnapshot,
    )

But If React updates the mechanism to custom equals function for getSnapShot. It would be best 🫠

paul-phan commented 1 year ago

How do you make use of that callback? I know the return value from getSnapShot should reference the same object if the data is unchanged, then returning something else from that callback would me a mistake. This is an optional API, so I would instead keep it clean, not dropping support for "pick" properties again; we can separate the store into atomic lever instead.

paul-phan commented 1 year ago

The getSnapShot is more valued for this use case: https://github.com/preactjs/signals/blob/9a5cbbdcb856e91156b9d74139047bcb5f9ceb8a/packages/react/src/index.ts#L116.

Roguehp98 commented 1 year ago

How do you make use of that callback? I know the return value from getSnapShot should reference the same object if the data is unchanged, then returning something else from that callback would me a mistake. This is an optional API, so I would instead keep it clean, not dropping support for "pick" properties again; we can separate the store into atomic lever instead.

callback is an optional function that gives value we want to subscribe to it.

Example: If we just want Text3 component subscribes to value in global state. We will pass callback option for useSyncStore

function Text3() {
    let { state, subscription } = useSyncStore(textStore3, (snapshot) => snapshot.value)

    return (
        <div>
            <input
                value={state}
                onChange={(e) => subscription.updateState({ value: e.target.value })}
            />
        </div>
    )
}

So even value2 was changed, Text3 component doesn't re-render.

AFAIK, based on return value from getSnapShot, React will shallow compare(use Object.is) to check a difference between prev and new state. So if global state is an object like an example, it will re-render every time.

We need to keep useSyncStore clean, by separating the store into atomic lever instead. But in many cases, it is really hard to make it smaller than that, comparing 2 objects (basic case with a few level) is a thing that we can't avoid it.

paul-phan commented 1 year ago

You're right; let's create a pull request for it. BTW, I recommend you look at some new approaches like preact-signals with support of batching multiple store updates into one. An opposite mental model vs. global state hook now.