diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad92582 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/App.js b/App.js index 16c7c60..ef96f38 100644 --- a/App.js +++ b/App.js @@ -4,43 +4,24 @@ * @flow */ -import React, { Component } from 'react'; -import { - Platform, - StyleSheet, - Text, - View -} from 'react-native'; -import { Provider } from 'react-redux' -import store from './store' -import AppNavigation from './src/Navigation' +import React, { Component } from "react"; +import { Platform, StyleSheet, Text, View } from "react-native"; +import { Provider } from "react-redux"; +import { PersistGate } from "redux-persist/es/integration/react"; +// import store from './store' +import AppNavigation from "./src/Navigation"; +import configureStore from "./store"; +const { store, persistor } = configureStore(); export default class App extends Component { render() { return ( - + Loading...} persistor={persistor}> + + ); } } - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#F5FCFF', - }, - welcome: { - fontSize: 20, - textAlign: 'center', - margin: 10, - }, - instructions: { - textAlign: 'center', - color: '#333333', - marginBottom: 5, - }, -}); diff --git a/authFlow.gif b/authFlow.gif new file mode 100644 index 0000000..7c565c7 Binary files /dev/null and b/authFlow.gif differ diff --git a/index.js b/index.js index 961ce2b..a37ef52 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import { AppRegistry } from 'react-native'; -import App from './App'; +import { AppRegistry } from "react-native"; +import App from "./App"; -AppRegistry.registerComponent('reactNavigationDemo', () => App); +AppRegistry.registerComponent("reactNavigationDemo", () => App); diff --git a/package.json b/package.json index a3c4e1f..bf156db 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "dependencies": { "react": "16.0.0-beta.5", "react-native": "0.49.3", - "react-navigation": "^1.0.0-beta.15", + "react-navigation": "1.0.0-beta.15", "react-redux": "^5.0.6", - "redux": "^3.7.2" + "redux": "^3.7.2", + "redux-persist": "^5.2.2" }, "devDependencies": { "babel-jest": "21.2.0", diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c84bfb4 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +# React-Navigation + Redux + + * Accompanying blog : https://medium.com/@shubhnik/a-comprehensive-guide-for-integrating-react-navigation-with-redux-including-authentication-flow-cb7b90611adf + + * Final result with authentication flow(authFlow branch of this repo) + +![demo](./authFlow.gif) \ No newline at end of file diff --git a/src/Actions/actionCreator.js b/src/Actions/actionCreator.js index 41371ee..5897749 100644 --- a/src/Actions/actionCreator.js +++ b/src/Actions/actionCreator.js @@ -1,14 +1,47 @@ -import {incrementCounter, decrementCounter} from './actionTypes' +import { + incrementCounter, + decrementCounter, + Login, + Logout, + Register, + RegisterSuccess, + NavigateToLogoutScreen +} from "./actionTypes"; const incrementAction = () => ({ - type: incrementCounter -}) + type: incrementCounter +}); const decrementAction = () => ({ - type: decrementCounter -}) + type: decrementCounter +}); + +const login = () => ({ + type: Login +}); + +const logout = () => ({ + type: Logout +}); + +const register = () => ({ + type: Register +}); + +const registerSuccess = () => ({ + type: RegisterSuccess +}); + +const navigateToLogoutScreen = () => ({ + type: NavigateToLogoutScreen +}); export { - incrementAction, - decrementAction -} \ No newline at end of file + incrementAction, + decrementAction, + login, + logout, + register, + registerSuccess, + navigateToLogoutScreen +}; diff --git a/src/Actions/actionTypes.js b/src/Actions/actionTypes.js index 02c4b0d..4aa364b 100644 --- a/src/Actions/actionTypes.js +++ b/src/Actions/actionTypes.js @@ -1,7 +1,17 @@ const incrementCounter = "INCREMENT_COUNTER"; const decrementCounter = "DECREMENT_COUNTER"; +const Login = "LOGIN"; +const Logout = "LOGOUT"; +const Register = "REGISTER"; +const RegisterSuccess = "REGISTER_SUCCESS"; +const NavigateToLogoutScreen = "NAVIGATE_TO_LOGOUT_SCREEN"; export { - incrementCounter, - decrementCounter -} \ No newline at end of file + incrementCounter, + decrementCounter, + Login, + Logout, + Register, + RegisterSuccess, + NavigateToLogoutScreen +}; diff --git a/src/Components/Counter.js b/src/Components/Counter.js new file mode 100644 index 0000000..303845c --- /dev/null +++ b/src/Components/Counter.js @@ -0,0 +1,87 @@ +import React, { Component } from "react"; +import { Text, View, TouchableOpacity } from "react-native"; +import { NavigationActions } from "react-navigation"; +import { connect } from "react-redux"; +import { + incrementAction, + decrementAction, + navigateToLogoutScreen +} from "../Actions/actionCreator"; +import { Tabs } from "../Navigation/navigationStack"; + +class CounterView extends Component { + navigate = () => { + this.props.navigateToLogoutScreen(); + }; + + render() { + const { + counterCount, + incrementAction, + decrementAction, + counterString + } = this.props; + return ( + + {counterCount} + {counterString} + + incrementAction()} + style={{ flex: 1, justifyContent: "center", alignItems: "center" }} + > + + INCREMENT + + + decrementAction()} + style={{ flex: 1, justifyContent: "center", alignItems: "center" }} + > + + DECREMENT + + + + + + Navigate to the last tab programmatically: "Logout" + + + + ); + } +} + +const mapStateToProps = state => ({ + counterCount: state.CounterReducer.counter, + counterString: state.CounterReducer.counterString +}); + +const mapDispatchToProps = { + incrementAction, + decrementAction, + navigateToLogoutScreen +}; + +const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterView); + +export default Counter; diff --git a/src/Components/Feed.js b/src/Components/Feed.js new file mode 100644 index 0000000..d63a244 --- /dev/null +++ b/src/Components/Feed.js @@ -0,0 +1,8 @@ +import React, { Component } from "react"; +import { Text, View, TouchableOpacity } from "react-native"; + +export default class Feed extends Component { + render() { + return ; + } +} diff --git a/src/Components/LoginScreen.js b/src/Components/LoginScreen.js new file mode 100644 index 0000000..58a0a81 --- /dev/null +++ b/src/Components/LoginScreen.js @@ -0,0 +1,76 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { NavigationActions } from "react-navigation"; +import { Text, View, TouchableOpacity, StyleSheet } from "react-native"; +import { login, register } from "../Actions/actionCreator"; + +class LoginScreen extends Component { + static navigationOptions = { + title: "Login" + }; + + navigateToRegisterScreen = () => { + this.props.register(); + }; + + render() { + return ( + + + This is a dummy Login Screen, no TextInputs, only Dummy Login button. + + + This is a completely synchronous flow, just for demo. + + + In real life situation, you might be doing async task like calling a + remote server to authenticate.You might need redux-thunk to dispatch + action asynchronously. + + + Login + + + Register + + + ); + } +} + +const styles = StyleSheet.create({ + rootContainer: { + flex: 1, + backgroundColor: "cyan", + justifyContent: "center", + alignItems: "center" + }, + textStyles: { + textAlign: "center", + color: "rgba(0,0,0,0.8)", + fontSize: 16 + }, + touchableStyles: { + marginTop: 15, + backgroundColor: "black", + paddingHorizontal: 50, + paddingVertical: 10, + borderRadius: 5 + } +}); + +const mapDispatchToProps = { + login, + register +}; + +const Login = connect(null, mapDispatchToProps)(LoginScreen); + +export default Login; diff --git a/src/Components/Logout.js b/src/Components/Logout.js new file mode 100644 index 0000000..ab1e12f --- /dev/null +++ b/src/Components/Logout.js @@ -0,0 +1,45 @@ +import React, { Component } from "react"; +import { Text, View, TouchableOpacity, StyleSheet } from "react-native"; +import { connect } from "react-redux"; +import { logout } from "../Actions/actionCreator"; + +class LogoutScreen extends Component { + render() { + return ( + + {/* {this.props.navigation.state.params.name} */} + + Logout + + + ); + } +} + +const mapDispatchToProps = { + logout +}; + +const Logout = connect(null, mapDispatchToProps)(LogoutScreen); + +export default Logout; + +const styles = StyleSheet.create({ + touchableStyles: { + marginTop: 15, + backgroundColor: "black", + paddingHorizontal: 50, + paddingVertical: 10, + borderRadius: 5 + } +}); diff --git a/src/Components/Notification.js b/src/Components/Notification.js new file mode 100644 index 0000000..48d3dfa --- /dev/null +++ b/src/Components/Notification.js @@ -0,0 +1,8 @@ +import React, { Component } from "react"; +import { Text, View, TouchableOpacity } from "react-native"; + +export default class Notification extends Component { + render() { + return ; + } +} diff --git a/src/Components/SignupScreen.js b/src/Components/SignupScreen.js new file mode 100644 index 0000000..73b49eb --- /dev/null +++ b/src/Components/SignupScreen.js @@ -0,0 +1,41 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Text, View, TouchableOpacity, StyleSheet } from "react-native"; +import { login, registerSuccess } from "../Actions/actionCreator"; + +class SignupView extends Component { + render() { + return ( + + this.props.registerSuccess()} + > + Register + + + ); + } +} + +mapDispatchToProps = { + login, + registerSuccess +}; + +const Signup = connect(null, mapDispatchToProps)(SignupView); +export default Signup; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: "indigo", + justifyContent: "center", + alignItems: "center" + }, + button: { + backgroundColor: "pink", + paddingHorizontal: 50, + paddingVertical: 15 + } +}); diff --git a/src/Components/screen1View.js b/src/Components/screen1View.js deleted file mode 100644 index 7a4d593..0000000 --- a/src/Components/screen1View.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Component } from 'react'; -import { - Text, - View, - TouchableOpacity -} from 'react-native'; -import { NavigationActions } from 'react-navigation' - -export default class Screen1View extends Component { - static navigationOptions = { - title:'Screen1' - } - - navigate = () => { - const navigateToScreen2 = NavigationActions.navigate({ - routeName:'screen2', - params:{name:'Shubhnik'} - }) - this.props.navigation.dispatch(navigateToScreen2) - } - - render() { - const {counterCount, incrementAction, decrementAction} = this.props - return( - - {counterCount} - - incrementAction()} - style={{flex:1, justifyContent:'center', alignItems:'center'}} - > - INCREMENT - - decrementAction()} - style={{flex:1, justifyContent:'center', alignItems:'center'}} - > - DECREMENT - - - - Screen2 - - - ) - } -} \ No newline at end of file diff --git a/src/Components/screen2.js b/src/Components/screen2.js deleted file mode 100644 index 3c9821c..0000000 --- a/src/Components/screen2.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, { Component } from 'react'; -import { - Text, - View, - TouchableOpacity -} from 'react-native'; - -export default class Screen2 extends Component { - static navigationOptions={ - title:'Screen2' - } - render() { - return( - - {this.props.navigation.state.params.name} - - ) - } -} \ No newline at end of file diff --git a/src/Containers/screen1.js b/src/Containers/screen1.js deleted file mode 100644 index e6f0563..0000000 --- a/src/Containers/screen1.js +++ /dev/null @@ -1,16 +0,0 @@ -import {connect} from 'react-redux' -import Screen1View from '../Components/screen1View' -import {incrementAction, decrementAction} from '../Actions/actionCreator' - -const mapStateToProps = state => ({ - counterCount: state.CounterReducer.counter -}) - -const mapDispatchToProps = { - incrementAction, - decrementAction -} - -const Screen1 = connect(mapStateToProps, mapDispatchToProps)(Screen1View) - -export default Screen1; \ No newline at end of file diff --git a/src/Navigation/index.js b/src/Navigation/index.js index 2ec7b0a..d938945 100644 --- a/src/Navigation/index.js +++ b/src/Navigation/index.js @@ -1,25 +1,44 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { addNavigationHelpers } from 'react-navigation'; -import NavigationStack from './navigationStack' +import React, { Component } from "react"; +import { BackHandler } from "react-native"; +import { connect } from "react-redux"; +import { addNavigationHelpers, NavigationActions } from "react-navigation"; +import NavigationStack from "./navigationStack"; class AppNavigation extends Component { - render() { - const { navigationState, dispatch } = this.props; - return( - - ) + componentDidMount() { + BackHandler.addEventListener("hardwareBackPress", this.onBackPress); + } + + componentWillUnmount() { + BackHandler.removeEventListener("hardwareBackPress", this.onBackPress); + } + + onBackPress = () => { + const { dispatch, navigationState } = this.props; + if (navigationState.stateForLoggedIn.index <= 1) { + BackHandler.exitApp(); + return; } -} + dispatch(NavigationActions.back()); + return true; + }; -const mapStateToProps = (state) => { - console.log(`****MPSP${JSON.stringify(state)}`) - return ({ - navigationState: state.NavigationReducer - }) + render() { + const { navigationState, dispatch, isLoggedIn } = this.props; + const state = isLoggedIn + ? navigationState.stateForLoggedIn + : navigationState.stateForLoggedOut; + return ( + + ); + } } +const mapStateToProps = state => { + return { + isLoggedIn: state.LoginReducer.isLoggedIn, + navigationState: state.NavigationReducer + }; +}; -export default connect(mapStateToProps)(AppNavigation) +export default connect(mapStateToProps)(AppNavigation); diff --git a/src/Navigation/navigationStack.js b/src/Navigation/navigationStack.js index b024c3a..2f414f7 100644 --- a/src/Navigation/navigationStack.js +++ b/src/Navigation/navigationStack.js @@ -1,14 +1,53 @@ -import { StackNavigator } from 'react-navigation'; -import Screen1 from '../Containers/screen1' -import Screen2 from '../Components/screen2' +import { StackNavigator, TabNavigator } from "react-navigation"; + +import Counter from "../Components/Counter"; +import Logout from "../Components/Logout"; +import Login from "../Components/LoginScreen"; +import Feed from "../Components/Feed"; +import Notification from "../Components/Notification"; +import Signup from "../Components/SignupScreen"; + +export const Tabs = TabNavigator({ + feed: { + screen: Feed, + navigationOptions: { + tabBarLabel: "Feed", + title: "Feed" + } + }, + counter: { + screen: Counter, + navigationOptions: { + tabBarLabel: "Counter", + title: "Counter" + } + }, + logout: { + screen: Logout, + navigationOptions: { + tabBarLabel: "Logout", + title: "Logout" + } + } +}); const navigator = StackNavigator({ - screen1: { - screen: Screen1 - }, - screen2: { - screen: Screen2 + login: { + screen: Login + }, + signup: { + screen: Signup, + navigationOptions: { + title: "Register" + } + }, + mainScreens: { + screen: Tabs, + navigationOptions: { + gesturesEnabled: false, + headerLeft: null } -}) + } +}); -export default navigator; \ No newline at end of file +export default navigator; diff --git a/src/Reducers/counterReducer.js b/src/Reducers/counterReducer.js index 32f186d..37cd351 100644 --- a/src/Reducers/counterReducer.js +++ b/src/Reducers/counterReducer.js @@ -1,18 +1,25 @@ -import {incrementCounter, decrementCounter} from '../Actions/actionTypes' +import { incrementCounter, decrementCounter } from "../Actions/actionTypes"; -const initialState = { counter: 0 } +const initialState = { counter: 0, counterString: "1" }; const counterReducer = (state = initialState, action) => { - switch (action.type) { - case incrementCounter: - return {...state, counter:state.counter + 1} + switch (action.type) { + case incrementCounter: + return { + ...state, + counter: state.counter + 1, + counterString: state.counterString + "1" + }; - case decrementCounter: - return {...state, counter:state.counter - 1} + case decrementCounter: + return { ...state, counter: state.counter - 1 }; - default: - return state - } -} + case "LOGOUT": + return { ...initialState }; -export default counterReducer; \ No newline at end of file + default: + return state; + } +}; + +export default counterReducer; diff --git a/src/Reducers/index.js b/src/Reducers/index.js index ce4a048..8c344bf 100644 --- a/src/Reducers/index.js +++ b/src/Reducers/index.js @@ -1,10 +1,10 @@ -import {combineReducers} from 'redux'; -import CounterReducer from './counterReducer'; -import NavigationReducer from './navigationReducer' +import { combineReducers } from "redux"; +import CounterReducer from "./counterReducer"; +import NavigationReducer from "./navigationReducer"; const AppReducer = combineReducers({ - CounterReducer, - NavigationReducer -}) + CounterReducer, + NavigationReducer +}); -export default AppReducer; \ No newline at end of file +export default AppReducer; diff --git a/src/Reducers/loginReducer.js b/src/Reducers/loginReducer.js new file mode 100644 index 0000000..5b5600e --- /dev/null +++ b/src/Reducers/loginReducer.js @@ -0,0 +1,21 @@ +import { Login, Logout, RegisterSuccess } from "../Actions/actionTypes"; + +const initialState = { isLoggedIn: false }; + +const loginReducer = (state = initialState, action) => { + switch (action.type) { + case Login: + return { ...state, isLoggedIn: true }; + + case Logout: + return { ...state, isLoggedIn: false }; + + case RegisterSuccess: + return { ...state, isLoggedIn: true }; + + default: + return state; + } +}; + +export default loginReducer; diff --git a/src/Reducers/navigationReducer.js b/src/Reducers/navigationReducer.js index 5b3b00c..37cf46c 100644 --- a/src/Reducers/navigationReducer.js +++ b/src/Reducers/navigationReducer.js @@ -1,9 +1,135 @@ -import AppNavigator from '../Navigation/navigationStack' -const initialState = AppNavigator.router.getStateForAction(AppNavigator.router.getActionForPathAndParams('screen1')) -console.log(`*****NavState${JSON.stringify(initialState)}`); +import { NavigationActions } from "react-navigation"; + +import AppNavigator, { Tabs } from "../Navigation/navigationStack"; +import { + Login, + Logout, + Register, + RegisterSuccess, + NavigateToLogoutScreen +} from "../Actions/actionTypes"; + +const ActionForLoggedOut = AppNavigator.router.getActionForPathAndParams( + "login" +); + +const ActionForLoggedIn = AppNavigator.router.getActionForPathAndParams( + "mainScreens" +); + +const stateForLoggedOut = AppNavigator.router.getStateForAction( + ActionForLoggedOut +); + +const stateForLoggedIn = AppNavigator.router.getStateForAction( + ActionForLoggedIn, + stateForLoggedOut +); + +const initialState = { stateForLoggedOut, stateForLoggedIn }; + const navigationReducer = (state = initialState, action) => { - const newState = AppNavigator.router.getStateForAction(action, state) - return newState || state; -} + let nextState; + + switch (action.type) { + case Login: + return { + ...state, + stateForLoggedIn: AppNavigator.router.getStateForAction( + ActionForLoggedIn, + stateForLoggedOut + ) + }; + + case Register: + return { + ...state, + stateForLoggedOut: AppNavigator.router.getStateForAction( + AppNavigator.router.getActionForPathAndParams("signup"), + stateForLoggedOut + ) + }; + + case RegisterSuccess: + return { + ...state, + stateForLoggedIn: AppNavigator.router.getStateForAction( + NavigationActions.reset({ + index: 2, + actions: [ + NavigationActions.navigate({ routeName: "login" }), + NavigationActions.navigate({ routeName: "signup" }), + NavigationActions.navigate({ routeName: "mainScreens" }) + ] + }) + ) + }; + + /* Another option for RegisterSuccess + nextState = { + ...state, + stateForLoggedIn: AppNavigator.router.getStateForAction( + ActionForLoggedIn, + AppNavigator.router.getStateForAction( + AppNavigator.router.getActionForPathAndParams("signup"), + stateForLoggedOut + ) + ) + }; + */ + + case "Navigation/BACK": + return { + ...state, + stateForLoggedOut: AppNavigator.router.getStateForAction( + NavigationActions.back(), + stateForLoggedOut + ) + }; + + case Logout: + return { + ...state, + stateForLoggedOut: AppNavigator.router.getStateForAction( + NavigationActions.reset({ + index: 0, + actions: [NavigationActions.init({ routeName: "login" })] + }) + ) + }; + + /* Other logic for logging out, more cleaner but unlike the above isn't telling the reader + that navigation is reset, that's why I chose the *reset* one for the article. I prefer + this one, what about you? + + case 'LOGOUT': + nextState = { ...state, initialStateForLoggedIn, initialStateForLoggedOut} + break; + */ + + case NavigateToLogoutScreen: + return { + ...state, + stateForLoggedIn: { + ...state.stateForLoggedIn, + routes: state.stateForLoggedIn.routes.map( + route => + route.routeName === "mainScreens" + ? { ...route, index: 2 } + : { ...route } + ) + } + }; + + default: + return { + ...state, + stateForLoggedIn: AppNavigator.router.getStateForAction( + action, + state.stateForLoggedIn + ) + }; + } +}; -export default navigationReducer; \ No newline at end of file +export default navigationReducer; diff --git a/store.js b/store.js index c069038..85743e1 100644 --- a/store.js +++ b/store.js @@ -1,6 +1,52 @@ -import { createStore } from 'redux'; -import AppReducer from './src/Reducers' +import { createStore, combineReducers } from "redux"; +import { + persistCombineReducers, + persistStore, + persistReducer +} from "redux-persist"; +import storage from "redux-persist/es/storage"; -const store = createStore(AppReducer); +import counterReducer from "./src/Reducers/counterReducer"; +import NavigationReducer from "./src/Reducers/navigationReducer"; +import loginReducer from "./src/Reducers/loginReducer"; -export default store; \ No newline at end of file +// config to not persist the *counterString* of the CounterReducer's slice of the global state. +const config = { + key: "root", + storage, + blacklist: ["counterString"] +}; + +const config1 = { + key: "primary", + storage +}; + +// Object of all the reducers for redux-persist +const reducer = { + counterReducer, + NavigationReducer, + loginReducer +}; + +// This will persist all the reducers, but I don't want to persist navigation state, so instead will use persistReducer. +// const rootReducer = persistCombineReducers(config, reducer) + +// We are only persisting the counterReducer and loginRducer +const CounterReducer = persistReducer(config, counterReducer); +const LoginReducer = persistReducer(config1, loginReducer); + +// combineReducer applied on persisted(counterReducer) and NavigationReducer +const rootReducer = combineReducers({ + CounterReducer, + NavigationReducer, + LoginReducer +}); + +function configureStore() { + let store = createStore(rootReducer); + let persistor = persistStore(store); + return { persistor, store }; +} + +export default configureStore;