1

I`m having some problems trying to listen to state changes in this application. Basically I was expecting a useEffect hook to be fired after some state changed, but nothing at all is happening.

This is what I got

index.jsx

// this is a simplification.
// I actually have a react-router-dom's Router wrapping everything 
// and App is a Switch with multiple Route components
ReactDOM.render(
<Provider>
    <App>
</Provider>
, document.getElementById('root'));

useSession.jsx

export const useSession = () => {

  const [session, setSession] = useState(null)

  const login = useCallback(() => {
     // do something
     setSession(newSession)
  })

  return {
    session,
    setSession,
    login
  }
}

Provider.jsx

const { session } = useSession();

useEffect(() => {
  console.log('Print this')
}, [session])

// more code ...

App.jsx

export function Login() {
    const { login } = useSession();
    return <button type="button" onClick={() => { login() }}>Login</button>
}

Well I have this Parent component Provider watching the session state, but when it is updated the useEffect of the provider is never called.

The useEffect is fired only if the setSession is called in the same hook/method. For example, if I import the setSession in the Provider and use it there, the useEffect will be fired; Or if I add a useEffect in the useSession method, it is gonna be fired when login updates the state.

The callback of useEffect is called but only once, when the component is mounted, but not when the state is changed.

How can I achieve this behavior? Having the Provider's useEffect fired whenever session is updated?

Thanks in advance!

Victor Ferreira
  • 6,151
  • 13
  • 64
  • 120
  • 1
    Trying to wrap my head around why the login function is decorated with a `useCallback` hook... without a dependency array. Without I am sure it isn't really memoizing the login callback. I don't think that is the issue, but it is odd. What else are you expecting to trigger changes to `session` in `useSession` *other than the setter*? – Drew Reese Apr 21 '20 at 06:19
  • 2
    Agree with @DrewReese. It might have to be with useCallback. Try using a regular function. If that does't help, a codesandbox will be helpful. – Tom Slutsky Apr 21 '20 at 06:20

1 Answers1

1

I think this is just a bit of misunderstanding of how custom hooks work.Every instance of the component has its own state. Let me just show a simple example illustrating this.

function App () {
  return (
    <div>
      <ComponentA/>
      <ComponentB/>
      <ComponentC/>
      <ComponentD/>
    </div>
  )
}

function useCounter() {
  const [counter, setCounter] = React.useState(0);

  function increment() {
    setCounter(counter+1)
  }

  return {
    increment, counter, setCounter
  }
}

function ComponentA() {
  const { counter, increment }= useCounter()
  return (
    <div>
      <button onClick={()=>increment()}>Button A</button>
      ComponentA Counter: {counter}
    </div>
  )
}

function ComponentB() {
  const { counter, increment }= useCounter()
  return (
    <div>
      <button onClick={()=>increment()}>Button B</button>
      ComponentB Counter: {counter}
    </div>
  )
}

function ComponentC() {
  const { counter }= useCounter();
  return (
    <div>
      ComponentC Counter: {counter}
    </div>
  )
}

function ComponentD() {
  const [toggle, setToggle] = React.UseState(false);
  const { counter }= useCounter();

  React.useEffect(() => {
     setInterval(()=>{
       setToggle(prev => !prev);
     }, 1000)
  })

  return (
    <div>
      ComponentD Counter: {counter}
    </div>
  )
}


From the above code if you can see that incrementing count by clicking Button Awill not affect the count instance of ComponentB.This is because every instance of the component has its own state. You can also see that clicking either buttons won't trigger ComponentC to rerender since they don't share the same instance. Even if i trigger rerender every one second like in Component D thus invoking useCounter the counter in ComponentD remains 0.

Solution However there are multiple ways of making components share/listen to same state changes

  1. You can shift all your state i.e [session state] to the Provider component and make it visible to other components by passing it via props.
  2. You can move state to a global container Redux or simply use Context Api + UseReducer Hook here is an example

But since you are dealing with auth and session management, I suggest you persist the session state in local storage or session storage, and retrieve it whenever you need it. Hope that helped

Chitova263
  • 719
  • 4
  • 13
  • Thanks a lot! This is another interesting reading about this subject https://medium.com/@akrush95/global-cached-state-in-react-using-hooks-context-and-local-storage-166eacf8ab46 Everytime I called `useSession` I was recreating the hooks. I thought that somehow it was preserving the values internally. But it wasn't, so every call is a new state – Victor Ferreira Apr 22 '20 at 17:51