Keselme
Keselme

Reputation: 4279

Component state doesn't change after action dispatch

On startup I use useEffect to fetch some data from local storage. If the fetch was successful I want to send an action and change the state of the component. After dispatching the action, I see that the reducer received an action and returned the new state, but I don't see any change in the component when I try to log the new state. What can be the reason for this behaviour.

Here's the action.ts:

import {TOKEN_VALIDITY} from './actionTypes'

export const setTokenValidity = (isTokenValid: boolean) => ({ 
    type: TOKEN_VALIDITY,
    isTokenValid
})

Here's the reducer.ts:

    const auth = (state = false, action: any) => {
    
        // after dispatch I see this log in the console value is true
        console.log('auth reducer action type is ' + action.type + ' value is ' + action.isTokenValid) 
        
        switch(action.type) {
    
            case TOKEN_VALIDITY:
                return action.isTokenValid
    
            default:
                return state
        }
    }
    
    export default auth

In the component I want to update the state of isTokenValid, but I always get undefined for the value.

This is the component code

const Stack = createStackNavigator();
let userToken = null;

const App = (props:any) => {

  useEffect(() => {

    const bootstrapAsync = async () => {
      try {
        userToken = await retrieveData('Token',null);    
        SplashScreen.hide();

        if(userToken != null) {
          props.setTokenValidity(true)
        }
        
        // this logs undefined for props.isTokenValid -- why???
        console.log("after token isValid: " + props.isTokenValid) 

        console.log('token ' + userToken);
      } catch (e) {
        // Restoring token failed
      }
    };

    bootstrapAsync();
  }, []);
  
  return (
    <NavigationContainer>
    <Stack.Navigator >
    {!props.isTokenValid ? (
      <>
      <Stack.Screen name="Login" component={Login} options={{ headerShown:false }}/>
      <Stack.Screen name="Home" component={Home} options={{ headerShown:false }}/>
      </>
      
      ) : (
      <Stack.Screen name="Home" component={Home} options={{ headerShown:false }}/>
      )}
    </Stack.Navigator >  
  </NavigationContainer>
  );
};

const mapDispatchToProps = (dispatch:any) => ({
  setTokenValidity: (isTokenValid:boolean) => dispatch(setTokenValidity(isTokenValid))
})

const mapStateToProps = (state:any) => ({
  isTokenValid: state.isTokenValid
})

export default connect(mapStateToProps, mapDispatchToProps)(App);

Upvotes: 0

Views: 950

Answers (3)

Alan Doe
Alan Doe

Reputation: 135

The way I solved it in my project, is by creating multiple useEffects.

In order to solve your problem you need to do 2 things:

  1. You need to run bootstrapAsync()
  2. You need to check after bootstrapAsync finishes for the data that was set into userToken

You have already did no.1 successfully, And your current problem is that your component does not updates when it receives new data, aka when userToken updates.

The solution:

Write another useEffect function which will be rendered 2 times: one time on component load(which we will ignore because the fetch isn't done yet) and another time when userToken value updates.

In order to avoid running our new useEffect on component load, we need to create a new state, which we will call allowNavigation.

allowNavigation prop will be set to true only after the fetch is complete.

Then, only when allowNavigation is set to true, we can check userToken and handle it properly.

The following code will help you:

const App = (props:any) => {
  const [allowNavigate, setAllowNavigate] = useState(false);

  useEffect(() => {

    const bootstrapAsync = async () => {
      try {
        userToken = await retrieveData('Token',null);    
        SplashScreen.hide();
        setAllowNavigate(true);
        ...
    };

    bootstrapAsync();
  }, []);

  useEffect(() => {
    if (allowNavigate)
        // this will now log the correct values
        console.log("after token isValid: " + props.isTokenValid) 
        console.log('token ' + userToken);
  }, [allowNavigate]);
...

Upvotes: 0

Tilak Madichetti
Tilak Madichetti

Reputation: 4346

Reducers return a state object so you might wanna try doing this:

      switch(action.type) {
    
            case TOKEN_VALIDITY:
                return { ...state, isTokenValid: action.isTokenValid }
    
            default:
                return state
       }

Upvotes: 0

Rohan Agarwal
Rohan Agarwal

Reputation: 2609

In the first rendering, i.e. when the useEffect is fired and you call the method using props.setTokenValidity to set the token validity, the token gets set. However, the console also gets printed on the same rendering.

When the state gets updated and you get an updated one using the props.isTokenValid, this is the 2nd re-rendering(not the same rendering when useEffect was called) and the useEffect doesn't fire, therefore we don't see the console being printed with the new value.

If you for some reason want to log when isTokenValid is set, use another useEffect

useEffect(() => {
        console.log("after token isValid: " + props.isTokenValid) 

},[props.isTokenValid]);

Upvotes: 1

Related Questions