React-Navigation with Login Screen

后端 未结 10 2761
野性不改
野性不改 2020-11-28 19:30

I am trying to use react-navigation to create a initial LOGIN screen which has no tabbar and header, and once the user has been successfully authenticated will navigate to a

相关标签:
10条回答
  • 2020-11-28 20:03

    Oct 2017 I found this ridiculously confusing, so here is my solution starting from the top down:

    I recommend starting a new project and literally just paste all this in and study it after. I commented the code big-time, so if you are stuck on any specific area, maybe the context can help you get back on track.

    This post shows how to:

    1. completely setup React Native to run react-navigation
    2. Properly integrate with Redux
    3. Handle Android Back Button
    4. Nest Stack Navigators
    5. Navigate from child to parent navigators
    6. Reset the Navigation Stack
    7. Reset the Navigation Stack while navigating from child to parent (nested)

    index.js

    import { AppRegistry } from 'react-native'
    import App from './src/App'
    
    AppRegistry.registerComponent('yourappname', () => App)
    

    src/App.js (this is the most important file because it brings all the shreds together)

    import React, { Component } from 'react'
    // this will be used to make your Android hardware Back Button work
    import { Platform, BackHandler } from 'react-native'
    import { Provider, connect } from 'react-redux'
    import { addNavigationHelpers } from 'react-navigation'
    // this is your root-most navigation stack that can nest
    // as many stacks as you want inside it
    import { NavigationStack } from './navigation/nav_reducer'
    // this is a plain ol' store
    // same as const store = createStore(combinedReducers)
    import store from './store'
    
    // this creates a component, and uses magic to bring the navigation stack
    // into all your components, and connects it to Redux
    // don't mess with this or you won't get
    // this.props.navigation.navigate('somewhere') everywhere you want it
    // pro tip: that's what addNavigationHelpers() does
    // the second half of the critical logic is coming up next in the nav_reducers.js file
    class App extends Component {
        // when the app is mounted, fire up an event listener for Back Events
        // if the event listener returns false, Back will not occur (note that)
        // after some testing, this seems to be the best way to make
        // back always work and also never close the app
        componentWillMount() {
            if (Platform.OS !== 'android') return
            BackHandler.addEventListener('hardwareBackPress', () => {
                const { dispatch } = this.props
                dispatch({ type: 'Navigation/BACK' })
                return true
            })
        }
    
        // when the app is closed, remove the event listener
        componentWillUnmount() {
            if (Platform.OS === 'android') BackHandler.removeEventListener('hardwareBackPress')
        }
    
        render() {
            // slap the navigation helpers on (critical step)
            const { dispatch, nav } = this.props
            const navigation = addNavigationHelpers({
                dispatch,
                state: nav
            })
            return <NavigationStack navigation={navigation} />
        }
    }
    
    // nothing crazy here, just mapping Redux state to props for <App />
    // then we create your root-level component ready to get all decorated up
    const mapStateToProps = ({ nav }) => ({ nav })
    const RootNavigationStack = connect(mapStateToProps)(App)
    
    const Root = () => (
        <Provider store={store}>
            <RootNavigationStack />
        </Provider>
    )
    
    export default Root
    

    src/navigation/nav_reducer.js

    // NavigationActions is super critical
    import { NavigationActions, StackNavigator } from 'react-navigation'
    // these are literally whatever you want, standard components
    // but, they are sitting in the root of the stack
    import Splash from '../components/Auth/Splash'
    import SignUp from '../components/Auth/SignupForm'
    import SignIn from '../components/Auth/LoginForm'
    import ForgottenPassword from '../components/Auth/ForgottenPassword'
    // this is an example of a nested view, you might see after logging in
    import Dashboard from '../components/Dashboard' // index.js file
    
    const WeLoggedIn = StackNavigator({
        LandingPad: {             // if you don't specify an initial route,
            screen: Dashboard     // the first-declared one loads first
        }
    }, {
        headerMode: 'none'
        initialRouteName: LandingPad // if you had 5 components in this stack,
    })                               // this one would load when you do
                                     // this.props.navigation.navigate('WeLoggedIn')
    
    // notice we are exporting this one. this turns into <RootNavigationStack />
    // in your src/App.js file.
    export const NavigationStack = StackNavigator({
        Splash: {
            screen: Splash
        },
        Signup: {
            screen: SignUp
        },
        Login: {
            screen: SignIn
        },
        ForgottenPassword: {
            screen: ForgottenPassword
        },
        WeLoggedIn: {
            screen: WeLoggedIn  // Notice how the screen is a StackNavigator
        }                       // now you understand how it works!
    }, {
        headerMode: 'none'
    })
    
    // this is super critical for everything playing nice with Redux
    // did you read the React-Navigation docs and recall when it said
    // most people don't hook it up correctly? well, yours is now correct.
    // this is translating your state properly into Redux on initialization    
    const INITIAL_STATE = NavigationStack.router.getStateForAction(NavigationActions.init())
    
    // this is pretty much a standard reducer, but it looks fancy
    // all it cares about is "did the navigation stack change?"    
    // if yes => update the stack
    // if no => pass current stack through
    export default (state = INITIAL_STATE, action) => {
        const nextState = NavigationStack.router.getStateForAction(action, state)
    
        return nextState || state
    }
    

    src/store/index.js

    // remember when I said this is just a standard store
    // this one is a little more advanced to show you
    import { createStore, compose, applyMiddleware } from 'redux'
    import thunk from 'redux-thunk'
    import { persistStore, autoRehydrate } from 'redux-persist'
    import { AsyncStorage } from 'react-native'
    // this pulls in your combinedReducers
    // nav_reducer is one of them
    import reducers from '../reducers'
    
    const store = createStore(
        reducers,
        {},
        compose(
            applyMiddleware(thunk),
            autoRehydrate()
        )
    )
    
    persistStore(store, { storage: AsyncStorage, whitelist: [] })
    
    // this exports it for App.js    
    export default store
    

    src/reducers.js

    // here is my reducers file. i don't want any confusion
    import { combineReducers } from 'redux'
    // this is a standard reducer, same as you've been using since kindergarten
    // with action types like LOGIN_SUCCESS, LOGIN_FAIL
    import loginReducer from './components/Auth/login_reducer'
    import navReducer from './navigation/nav_reducer'
    
    export default combineReducers({
        auth: loginReducer,
        nav: navReducer
    })
    

    src/components/Auth/SignUpForm.js

    I will show you a sample here. This isn't mine, I just typed it out for you in this rickety StackOverflow editor. Please give me thumbs up if you appreciate it :)

    import React, { Component } from 'react'
    import { View, Text, TouchableOpacity } from 'react-native
    
    // notice how this.props.navigation just works, no mapStateToProps
    // some wizards made this, not me
    class SignUp extends Component {
        render() {
            return (
                <View>
                    <Text>Signup</Text>
                    <TouchableOpacity onPress={() => this.props.navigation.navigate('Login')}>
                        <Text>Go to Login View</Text>
                    </TouchableOpacity>
                </View>
            )
        }
    }
    
    export default SignUp
    

    src/components/Auth/LoginForm.js

    I'll show you a dumb style one also, with the super dope back button

    import React from 'react'
    import { View, Text, TouchableOpacity } from 'react-native
    
    // notice how we pass navigation in
    const SignIn = ({ navigation }) => {
        return (
            <View>
                <Text>Log in</Text>
                <TouchableOpacity onPress={() => navigation.goBack(null)}>
                    <Text>Go back to Sign up View</Text>
                </TouchableOpacity>
            </View>
        )
    }
    
    export default SignIn
    

    src/components/Auth/Splash.js

    Here is a splash screen you can play around with. I am using it like a higher-order component:

    import React, { Component } from 'react'
    import { StyleSheet, View, Image, Text } from 'react-native'
    // https://github.com/oblador/react-native-animatable
    // this is a library you REALLY should be using
    import * as Animatable from 'react-native-animatable' 
    import { connect } from 'react-redux'
    import { initializeApp } from './login_actions'
    
    class Splash extends Component {
        constructor(props) {
            super(props)
            this.state = {}
        }
    
        componentWillMount() {
            setTimeout(() => this.props.initializeApp(), 2000)
        }
    
        componentWillReceiveProps(nextProps) {
            // if (!nextProps.authenticated) this.props.navigation.navigate('Login')
            if (nextProps.authenticated) this.props.navigation.navigate('WeLoggedIn')
        }
    
        render() {
            const { container, image, text } = styles
            return (
                <View style={container}>
                        <Image
                            style={image}
                            source={require('./logo.png')}
                        />
    
                        <Animatable.Text
                            style={text}
                            duration={1500}
                            animation="rubberBand"
                            easing="linear"
                            iterationCount="infinite"
                        >
                            Loading...
                        </Animatable.Text>
                        <Text>{(this.props.authenticated) ? 'LOGGED IN' : 'NOT LOGGED IN'}</Text>
                </View>
            )
        }
    }
    
    const styles = StyleSheet.create({
        container: {
            flex: 1,
            justifyContent: 'center',
            alignItems: 'center',
            backgroundColor: '#F0F0F0'
        },
        image: {
            height: 110,
            resizeMode: 'contain'
        },
        text: {
            marginTop: 50,
            fontSize: 15,
            color: '#1A1A1A'
        }
    })
    
    // my LOGIN_SUCCESS action creator flips state.auth.isAuthenticated to true    
    // so this splash screen just watches it
    const mapStateToProps = ({ auth }) => {
        return {
            authenticated: auth.isAuthenticated
        }
    }
    
    export default connect(mapStateToProps, { initializeApp })(Splash)
    

    src/components/Auth/login_actions.js

    I'm just going to show you initializeApp() so you get some ideas:

    import {
        INITIALIZE_APP,
        CHECK_REMEMBER_ME,
        TOGGLE_REMEMBER_ME,
        LOGIN_INITIALIZE,
        LOGIN_SUCCESS,
        LOGIN_FAIL,
        LOGOUT
    } from './login_types'
    
    //INITIALIZE APP
    // this isn't done, no try/catch and LOGIN_FAIL isn't hooked up
    // but you get the idea
    // if a valid JWT is detected, they will be navigated to WeLoggedIn
    export const initializeApp = () => {
        return async (dispatch) => {
            dispatch({ type: INITIALIZE_APP })
    
            const user = await AsyncStorage.getItem('token')
                .catch((error) => dispatch({ type: LOGIN_FAIL, payload: error }))
    
            if (!user) return dispatch({ type: LOGIN_FAIL, payload: 'No Token' })
    
            return dispatch({
                type: LOGIN_SUCCESS,
                payload: user
            })
            // navigation.navigate('WeLoggedIn')
            // pass navigation into this function if you want
        }
    }
    

    In other use cases, you may prefer the higher-order component. They work exactly the same as React for web. Stephen Grider's tutorials on Udemy are the best, period.

    src/HOC/require_auth.js

    import React, { Component } from 'react'
    import { connect } from 'react-redux'
    
    export default function (ComposedComponent) {
        class Authentication extends Component {
    
            componentWillMount() {
                if (!this.props.authenticated) this.props.navigation.navigate('Login')
            }
    
            componentWillUpdate(nextProps) {
                if (!nextProps.authenticated) this.props.navigation.navigate('Login')
            }
    
            render() {
                return (
                    <ComposedComponent {...this.props} />
                )
            }
        }
    
        const mapStateToProps = ({ auth }) => {
            return {
                authenticated: auth.isAuthenticated
            }
        }
    
        return connect(mapStateToProps)(Authentication)
    }
    

    You use it just like this:

    import requireAuth from '../HOC/require_auth'
    
    class RestrictedArea extends Component {
        // ... normal view component
    }
    
    //map state to props
    
    export default connect(mapStateToProps, actions)(requireAuth(RestrictedArea))
    

    There, that is everything I wish someone told and showed me.

    TLDR The App.js, and nav_reducer.js files are absolutely the most important to get right. The rest is old familiar. My examples should accelerate you into a savage productivity machine.

    [Edit] Here is my logout action creator. You will find it very useful if you wish to wipe off your navigation stack so the user cannot press Android Hardware Back Button and go back to a screen that requires authentication:

    //LOGOUT
    export const onLogout = (navigation) => {
        return async (dispatch) => {
            try {
                await AsyncStorage.removeItem('token')
    
                navigation.dispatch({
                    type: 'Navigation/RESET',
                    index: 0,
                    actions: [{ type: 'Navigate', routeName: 'Login' }]
                })
    
                return dispatch({ type: LOGOUT })
            } catch (errors) {
                // pass the user through with no error
                // this restores INITIAL_STATE (see login_reducer.js)
                return dispatch({ type: LOGOUT })
            }
        }
    }
    
    // login_reducer.js
        case LOGOUT: {
            return {
                ...INITIAL_STATE,
                isAuthenticated: false,
            }
        }
    

    [bonus edit] How do I navigate from a child Stack Navigator to a parent Stack Navigator?

    If you want to navigate from one of your child Stack Navigators and reset the stack, do this:

    1. Be inside a component adding code, where you have this.props.navigation available
    2. Make a component like <Something />
    3. Pass navigation into it, like this: <Something navigation={this.props.navigation} />
    4. Go into the code for that component
    5. Notice how you have this.props.navigation available inside this child component
    6. Now you're done, just call this.props.navigation.navigate('OtherStackScreen') and you should watch React Native magically go there without problem

    But, I want to RESET the whole stack while navigating to a parent stack.

    1. Call an action creator or something like this (starting off from step 6): this.props.handleSubmit(data, this.props.navigation)
    2. Go into the action creator and observe this code that could be there:

    actionCreators.js

    // we need this to properly go from child to parent navigator while resetting
    // if you do the normal reset method from a child navigator:
    this.props.navigation.dispatch({
        type: 'Navigation/RESET',
        index: 0,
        actions: [{ type: 'Navigate', routeName: 'SomeRootScreen' }]
    })
    
    // you will see an error about big red error message and
    // screen must be in your current stack 
    // don't worry, I got your back. do this
    // (remember, this is in the context of an action creator):
    import { NavigationActions } from 'react-navigation'
    
    // notice how we passed in this.props.navigation from the component,
    // so we can just call it like Dan Abramov mixed with Gandolf
    export const handleSubmit = (token, navigation) => async (dispatch) => {
        try {
            // lets do some operation with the token
            await AsyncStorage.setItem('token@E1', token)
            // let's dispatch some action that doesn't itself cause navigation
            // if you get into trouble, investigate shouldComponentUpdate()
            // and make it return false if it detects this action at this moment
            dispatch({ type: SOMETHING_COMPLETE })
    
            // heres where it gets 100% crazy and exhilarating
            return navigation.dispatch(NavigationActions.reset({
                // this says put it on index 0, aka top of stack
                index: 0,
                // this key: null is 9001% critical, this is what
                // actually wipes the stack
                key: null,
                // this navigates you to some screen that is in the Root Navigation Stack
                actions: [NavigationActions.navigate({ routeName: 'SomeRootScreen' })]
            }))
        } catch (error) {
            dispatch({ type: SOMETHING_COMPLETE })
            // User should login manually if token fails to save
            return navigation.dispatch(NavigationActions.reset({
                index: 0,
                key: null,
                actions: [NavigationActions.navigate({ routeName: 'Login' })]
            }))
        }
    }
    

    I am using this code inside an enterprise-grade React Native app, and it works beautifully.

    react-navigation is like functional programming. It is designed to be handled in small "pure navigation" fragments that compose well together. If you employ the strategy described above, you will find yourself creating re-useable navigation logic that you can just paste around as needed.

    0 讨论(0)
  • 2020-11-28 20:07

    This is my solution based on @parker recommendation:

    1. Create a top level navigator and it should be a stack navigator that renders a login screen.
    2. Another screen within this top level navigator should be your app's Main-Navigator.
    3. When your login state is satisfied, you reset the main stack to just the Main-Navigator.

    This code does the bare minimum to accomplish the above.

    Create a new react-native project, then copy the code below into index.ios.js and/or index.android.js to see it working.

    import React, { Component } from 'react';
    import {
      AppRegistry,
      Text,
      Button
    } from 'react-native';
    import { StackNavigator, NavigationActions } from 'react-navigation';
    
    const resetAction = NavigationActions.reset({
      index: 0,
      actions: [
        NavigationActions.navigate({ routeName: 'Main' })
      ]
    });
    
    class LoginScreen extends Component {
      login() {
        this.props.navigation.dispatch(resetAction);
      }
    
      render() {
        return <Button title='Login' onPress={() => {this.login()}} />;
      }
    }
    
    class FeedScreen extends Component {
      render() {
        return <Text>This is my main app screen after login</Text>;
      }
    }
    
    //Create the navigation
    const MainNav = StackNavigator({
        Feed: { screen: FeedScreen },
    });
    
    const TopLevelNav = StackNavigator({
      Login: { screen: LoginScreen },
      Main: { screen: MainNav },
    }, {
      headerMode: 'none',
    });
    
    
    AppRegistry.registerComponent('ReactNav2', () => TopLevelNav);
    
    0 讨论(0)
  • 2020-11-28 20:10

    I needed this, but none of the other solutions worked for me. So here is my solution for a Login with a drawer (the latter accessible only after proper authentication, and each of the screens inside have there own navigation stack). My code has a DrawerNavigator, but the same could be used for a TabNavigator (createBottomTabNavigator).

    wrapScreen = stackNavigator =>
      createStackNavigator(stackNavigator, {
        defaultNavigationOptions: ({ navigation }) => ({
          headerStyle: { backgroundColor: "white" },
          headerLeft: MenuButton(navigation)
        })
      });
    
    const DrawerStack = createDrawerNavigator(
      {
        // Menu Screens
        firstSection: wrapScreen({ FirstScreen: FirstScreen }),
        secondSection: wrapScreen({
          SecondHomeScreen: SecondHomeScreen,
          SecondOptionScreen: SecondOptionScreen
        }),
        settingSection: wrapScreen({ SettingScreen: SettingScreen }),
        aboutSection: wrapScreen({ AboutScreen: AboutScreen })
      },
      {
        initialRouteName: "firstSection",
        gesturesEnabled: false,
        drawerPosition: "left",
        contentComponent: DrawerContainer
      }
    );
    
    const PrimaryNav = createSwitchNavigator(
      {
        loginStack: LoginScreen,
        appStack: DrawerStack
      },
      { initialRouteName: "loginStack" }
    );
    
    export default createAppContainer(PrimaryNav);
    
    0 讨论(0)
  • 2020-11-28 20:11

    react-navigation now has a SwitchNavigator which helps desired behavior and switching between navigators. Currently there is not much documentation about it but there is a really good example snack created by the library which shows a simple authentication flow implementation. You can check it here.

    SwitchNavigator reference

    SwitchNavigator(RouteConfigs, SwitchNavigatorConfig)

    Example from docs

    const AppStack = StackNavigator({ Home: HomeScreen, Other: OtherScreen });
    const AuthStack = StackNavigator({ SignIn: SignInScreen });
    
    export default SwitchNavigator(
      {
        AuthLoading: AuthLoadingScreen,
        App: AppStack,
        Auth: AuthStack,
      },
      {
        initialRouteName: 'AuthLoading',
      }
    );
    
    0 讨论(0)
提交回复
热议问题