Changing app navigation structure from version 4 to 5 in react native

Viewed 920

I was working on an old app using react navigation version 4 the app contains a register and login in page obviously and then the content of the app.

recently I started remaking the content of the app using react navigation version 5 in order to use the shared element animation and the bottom tab navigator and it was fairly simple.

but I struggled with converting the login part to version 5 since the app structure is somewhat complicated and I am somewhat new to react navigation version 5.

i will leave a figure of the app structure bellow a long with samples of the code used.

Navigation used in the app

App.js :

import { setNavigator } from "./app/navigationRef";
const articleListFlow = createStackNavigator({
  Main: MainScreen, // screen with diffrent articles categories
  ResultsShow: ResultShowScreen, // article details screen
});
const loginFlow = createStackNavigator({
  Signup: SignupScreen,
  Signin: SigninScreen,
});
loginFlow.navigationOptions = () => {
  return {
    headerShown: false,
  };
};

articleListFlow.navigationOptions = {
  title: "News Feed",
  tabBarIcon: ({ tintColor }) => (
    <View>
      <Icon style={[{ color: tintColor }]} size={25} name={"ios-cart"} />
    </View>
  ),
  activeColor: "#ffffff",
  inactiveColor: "#ebaabd",
  barStyle: { backgroundColor: "#d13560" },
};
const switchNavigator = createSwitchNavigator({
  ResolveAuth: ResolveAuthScreen,
  MainloginFlow: createSwitchNavigator({
    //WelcomeScreen: WeclomeScreen,
    loginFlow: loginFlow,
  }),

  mainFlow: createMaterialBottomTabNavigator(
    {
      articleListFlow: articleListFlow,
      ArticleSave: ArticleSaveScreen, // we dont need this one
      Account: AccountScreen,
    },
    {
      activeColor: "#ffffff",
      inactiveColor: "#bda1f7",
      barStyle: { backgroundColor: "#6948f4" },
    }
  ),
});
const App = createAppContainer(switchNavigator);
export default () => {
  return (
    <AuthProvider>
      <App
        ref={(navigator) => {
          setNavigator(navigator);
        }}
      />
    </AuthProvider>
  );
};

NavigationRef.js :

import { NavigationActions } from "react-navigation";

let navigator;

export const setNavigator = (nav) => {
  navigator = nav;
};

export const navigate = (routeName, params) => {
  navigator.dispatch(
    NavigationActions.navigate({
      routeName,
      params,
    })
  );
};

// routename is the name of the routes singin singup accountscreen
// params information we want to pass to the screen we want to show

AuthContext.js

import { AsyncStorage } from "react-native";
import createDataContext from "./createDataContext";
import userAPI from "../api/user";

// using navigate to access the navigator and redirect the user
import { navigate } from "../navigationRef";

// AUTHENTICATION REDUCERS
const authReducer = (state, action) => {
  switch (action.type) {
    case "add_error": {
      return {
        ...state,
        errorMessage: action.payload,
      };
    }

    case "clear_error_message": {
      return {
        ...state,
        errorMessage: "",
      };
    }
    case "signin": {
      return {
        errorMessage: "",
        token: action.payload,
      };
    }

    default:
      return state;
  }
};

// CLEARING ERROR MESSAGES WHEN SWITCHING SIGNIN-SIGNUP
const clearErrorMessage = (dispatch) => () => {
  dispatch({ type: "clear_error_message" });
};

// AUTOMATIC SIGNIN ONLY USING TOKENS ON USER DEVICE
const tryLocalSignin = (dispatch) => async () => {
  const token = await AsyncStorage.getItem("token");
  if (token) {
    // if token exists
    dispatch({ type: "signin", payload: token });

    navigate("Main");
  } else {
    // if token doesnt exist
    navigate("WelcomeScreen");
  }
};

// SIGNUP
const signup = (dispatch) => async ({ email, password }) => {
  try {
    const response = await userAPI.post("/signup", { email, password });
    await AsyncStorage.setItem("token", response.data.token);
    dispatch({ type: "signin", payload: response.data.token });

    // making use of the navigate component to access navigation
    // and redirect the user
    navigate("Main");
  } catch (err) {
    dispatch({
      type: "add_error",
      payload: "Something went wrong with sign up",
    });
  }
};

// SIGNIN
const signin = (dispatch) => async ({ email, password }) => {
  try {
    const response = await userAPI.post("/signin", { email, password });
    await AsyncStorage.setItem("token", response.data.token);
    // using signin since the logic is the same
    dispatch({ type: "signin", payload: response.data.token });

    // making use of the navigate component to access navigation
    // and redirect the user
    navigate("Main");
  } catch (err) {
    console.log(err);
    dispatch({
      type: "add_error",
      payload: "Something went wrong with sign in",
    });
  }
};

// SIGNOUT
const signout = (dispatch) => async () => {
  // removing the token makes identification not work again
  await AsyncStorage.removeItem("token");
  dispatch({ type: "signout" });

  navigate("loginFlow");
};

// CREATING CONTEXT AND PROVIDER OBJECTS FOR AUTHENTICATION
export const { Provider, Context } = createDataContext(
  authReducer,
  {
    signin,
    signup,
    signout,
    clearErrorMessage,
    tryLocalSignin,
  },
  {
    token: null,
    errorMessage: "",
  }
);

createDataContext.js

import React, { useReducer } from "react";

export default (reducer, actions, defaultValue) => {
  const Context = React.createContext();

  const Provider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, defaultValue);

    const boundActions = {};

    for (let action in actions) {
      // for every action in the actions, call it with dispatch
      boundActions[action] = actions[action](dispatch);
    }

    return (
      <Context.Provider value={{ state, ...boundActions }}>
        {children}
      </Context.Provider>
    );
  };

  return { Context, Provider };
};

My appologies for the long code and thank you in advance for anyone who can help.

2 Answers

There are several things that you need to consider when moving from V4 to V5 it involves some changes and also you can consider using features like the hooks.

The first change will be removing the Switch Navigator and conditionally render the navigator in its place. This will be done in your App.js. As you already have a reducer based implementation you can use the state values to take this decision.

The next change will be the creation of stacks, in V4 you create the navigation by passing the screen, now everything is a component and you pass the screens as children.

The option are also sent as props to either the navigator or the screen itself.

The usage of navigation ref is still possible but you can also use hooks like usenavigation inside components and for your authentication flow you wont be using this as you conditionally render the navigators.

I have made a simplified version based on your code. App.js

const AuthStack = createStackNavigator();
const AppTabs = createMaterialBottomTabNavigator();
const ArticleStack = createStackNavigator();

const Articles = () => {
  return (
    <ArticleStack.Navigator>
      <AppTabs.Screen name="ArticlesList" component={ArticleList} />
      <AppTabs.Screen name="ArticlesDetails" component={ArticleDetail} />
    </ArticleStack.Navigator>
  );
};

export default function App() {
  const [state, dispatch] = React.useReducer(authReducer, {
    isLoading: true,
    token: null,
    errorMessage: '',
  });

  React.useEffect(() => {
    const bootstrapAsync = async () => {
      const userToken = await AsyncStorage.getItem('userToken');
      dispatch({ type: 'RESTORE_TOKEN', token: userToken });
    };

    bootstrapAsync();
  }, []);
  const authContext = React.useMemo(
    () => ({
      signIn: async (data) => {
        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
      signOut: () => dispatch({ type: 'SIGN_OUT' }),
      signUp: async (data) => {
        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
    }),
    []
  );

  return (
    <AuthContext.Provider value={authContext}>
      <NavigationContainer>
        {state.token === null ? (
          <AuthStack.Navigator headerMode="none">
            {state.isLoading ? (
              <AuthStack.Screen name="Welcome" component={WelcomeScreen} />
            ) : (
              <>
                <AuthStack.Screen name="SignIn" component={SignInScreen} />
                <AuthStack.Screen name="SignUp" component={SingUpScreen} />
              </>
            )}
          </AuthStack.Navigator>
        ) : (
          <AppTabs.Navigator
            activeColor="#f0edf6"
            inactiveColor="#3e2465"
            barStyle={{ backgroundColor: '#694fad' }}>
            <AppTabs.Screen
              name="Articles"
              component={Articles}
              options={{
                tabBarLabel: 'Home',
                tabBarIcon: ({ color, size }) => (
                  <MaterialCommunityIcons
                    name="home"
                    color={color}
                    size={size}
                  />
                ),
              }}
            />
            <AppTabs.Screen name="Search" component={SearchScreen} />
            <AppTabs.Screen name="Save" component={SaveScreen} />
            <AppTabs.Screen name="Account" component={AccountScreen} />
          </AppTabs.Navigator>
        )}
      </NavigationContainer>
    </AuthContext.Provider>
  );
}

Auth Context

const AuthContext = React.createContext();
export default AuthContext;

Auth Reducer

export const authReducer = (state, action) => {
  switch (action.type) {
    case 'RESTORE_TOKEN':
      return {
        ...state,
        token: action.token,
        isLoading: false,
      };

    case 'SIGN_IN': {
      return {
        errorMessage: '',
        token: action.payload,
      };
    }

    case 'SIGN_OUT': {
      return {
        errorMessage: '',
        token: null,
      };
    }

    default:
      return state;
  }
};

As you can see the flow will be showing the welcome screen till the token is loaded from async storage and then based on that show the tabs or the login screen. Also the parameters are passed as props. I've moved the actions to app.js but it can be separated as well.

You can see a fully running sample here https://snack.expo.io/@guruparan/navigation-sample-3

Hope this helps, Feel free to ask if there are any questions.

As per your diagram, I have tried to create Navigation

const WelcomeStack = createStackNavigator();
const Tab = createBottomTabNavigator();
const ArticleStack = createStackNavigator();
const MainStack = createStackNavigator();

function Welcome(){
return(
    <WelcomeStack.Navigator>
        <WelcomeStack.screen name='SignIn' component={SignIn}/>
        <WelcomeStack.screen name='SignUp' component={SignUp}/>
    </WelcomeStack.Navigator>
)
}

function  Article(){
return(
    <ArticleStack.Navigator>
        <ArticleStack.Screen name='ArtcileList' name={ArticleList}/>
        <ArticleStack.Screen name='ArticleDetail' name={ArtcileDetail}/>
    </ArticleStack.Navigator>
)
}
function TabNav(){
<Tab.Navigator>
    <Tab.Screen name='Article' component={Article}/>
    <Tab.Screen name='Search' component={Search}/>
    <Tab.Screen name='Save' component={Save}/>
    <Tab.Screen name='Account' component={Account}/>
</Tab.Navigator>
}

function App(){
return(
<NavigationContainer>
<MainStack.Navigator>
   {this.state.isLogin ? 
   <MainStack.Screen name='Tab' component={TabNav}/> 
   :
   <MainStack.Screen name = 'WelcomeStack' component={Welcome}/>
  }
 </MainStack.Navigator>
 </NavigationContainer>
 )
 }

In react navigation 5, their is no switch navigator so you have to go with stack navigation + ternary operator. This is just an idea as per your diagram. You can make it better after some R&D.

Related