agile-ts / agile

🌌 Global State and Logic Library for JavaScript/Typescript applications

Home Page:https://agile-ts.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

useProxy is executed twice on sub-tree state update

laurencefass opened this issue Β· comments

πŸ› Bug report

πŸ€– Current Behavior

useProxy is executed twice on sub-tree state update

I have sub-components subscribed to slices of state with both useSelector and useProxy. The useProxy is executing twice.

state

export const APP_STATE = createState({
  slice1: { 
    value: 0
  },
  slice2: { 
    value: 0
  },
});

subsciber components

xport const Slice1Proxy = () => {
  const state = useProxy(APP_STATE);

  useEffect(()=>{
      console.log("Slice1Proxy.mySlice1", state.slice1); //TEST FAIL: executes twice per update
  },[state.slice1]);

  return (<></>)
}

export const Slice1Selector = () => {
  const mySlice1 = useSelector(APP_STATE, (state) => state.slice1);

  useEffect(()=>{
      console.log("Slice1Selector.mySlice1", mySlice1); // TEST PASS: executes once per update
  }, [mySlice1]);
  
  return (
    <></>
  )
}

🎯 Expected behavior

useSelector and useProxy should execute only once per state update

πŸ“„ Reproducible example

https://codesandbox.io/s/agilets-proxy-kj7oo?file=/src/core.js

πŸ’‘ Suggested solution(s)

βž• Additional notes

πŸ’» Your environment

See codesandbox versions

commented

Seems like the 'double' re-render comes from the fact,
that you have subscribed the APP_STATE not only to the child Components,
but also to the parent Component.
-> The child Component is re-rendered once by the useProxy() Hook
and the parent Component is also re-rendered once by the useAgile() Hook

export const Subscriber = () => {
  const myState = useAgile(APP_STATE); // <- re-renders the 'Subscriber 'Component when `APP_STATE` changes

  useEffect(()=>{
    console.log("Subscriber.myState", myState);
  },[myState]);

  return (<>
    <Slice1Selector/>
    <Slice1Proxy />
  </>);
}

The solution was to simply not bind the APP_STATE to the parent Component.
See code sandbox:
https://codesandbox.io/s/agilets-proxy-forked-jdbcd?file=/src/core.js

The wired thing is now why the Component with the useSelector() wasn't re-rendered twice. πŸ€”

I agree that we can can remove the apparent error by removing const myState = useAgile(APP_STATE) from the parent but Im not sure its a solution and Im a bit confused about how and why it works this way.

The parent useEffect() is only called once per update indicating a single render, not two.

useEffect(()=>{
    console.log("Subscriber.myState", myState); // called once per update
  },[myState]);

I cant reason why the proxy update would be - or need to be - called twice.

Generally there may be uses-cases where discrete state updates are required across parents and children. Is there a best practice guide for doing this AND eliminiting duplicate updates?

commented

I agree that we can can remove the apparent error by removing const myState = useAgile(APP_STATE) from the parent but Im not sure its a solution and Im a bit confused about how and why it works this way.

I guess it is with the current AgileTs version the only solution to prevent a double re-render.

The parent useEffect() is only called once per update indicating a single render, not two.

Yes exactly, the parent useEffect() is called once because useAgile() triggers a re-render on the parent Component
when the APP_STATE updates.
Through the parent re-rendered, all children are re-rendered too.
-> The first re-render in Slice1Proxy comes from the parent.

Since we have bound a part of the APP_STATE to the child Component too,
the Child Component is also re-rendered (apart from the parent)
when the selected APP_STATE property updates.
That is because AgileTs and the Child Component don't know anything about their parent Component/s.
-> The second re-render in Slice1Proxy comes from itself.

commented

Generally there may be uses-cases where discrete state updates are required across parents and children. Is there a best practice guide for doing this AND eliminiting duplicate updates?

If that is the case, why not only binding the State to the parent Component?

You can also access the State value without binding it to the particular Component that requires the State value
by using the State's value property.

// Doesn't re-render when MY_STATE changes
const myState = MY_STATE.value;

// Re-renders when MY_STATE changes
const myState = useAgile(MY_STATE);

▢️ I will test how redux behaves in such a use case, and then look further ^^

commented

AgileTs:
https://codesandbox.io/s/httpsgithubcomagile-tsagileissues179agile-bnktp?file=/src/core.js

Redux-Toolkit:
https://codesandbox.io/s/httpsgithubcomagile-tsagileissues179redux-c5szd?file=/src/core.js

In Redux-Toolkit it only re-rendered once, since they re-render the Component via the Context API (which is wrapped globally around all Components)
and not for each Component separately, like AgileTs does.

commented

Will close the issue since it is the expected behavior
that each binding to a Component (no matter if parent or child) results in a re-render (for each Component)
when the bound State mutates.

fixed the unwanted side-effect with a memo() wrapper round the whole component. proxies are awesome. thanks for your help and advice.