├── .babelrc ├── .buckconfig ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── .watchmanconfig ├── README.md ├── __tests__ ├── index.android.js └── index.ios.js ├── android ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── app │ ├── .classpath │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── BUCK │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── reactnativereduxexample │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties └── settings.gradle ├── app.json ├── index.android.js ├── index.ios.js ├── ios ├── ReactNativeReduxExample-tvOS │ └── Info.plist ├── ReactNativeReduxExample-tvOSTests │ └── Info.plist ├── ReactNativeReduxExample.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── ReactNativeReduxExample-tvOS.xcscheme │ │ └── ReactNativeReduxExample.xcscheme ├── ReactNativeReduxExample │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── main.m └── ReactNativeReduxExampleTests │ ├── Info.plist │ └── ReactNativeReduxExampleTests.m ├── package-lock.json ├── package.json ├── src ├── actions │ ├── actiontypes.js │ └── index.js ├── app.js ├── components │ └── screens │ │ ├── homeTab.js │ │ ├── login.js │ │ ├── screens.js │ │ └── searchTab.js ├── img │ └── checkmark.png └── reducers │ ├── index.js │ └── rootReducer.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | emoji=true 26 | 27 | module.system=haste 28 | 29 | munge_underscores=true 30 | 31 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 32 | 33 | suppress_type=$FlowIssue 34 | suppress_type=$FlowFixMe 35 | suppress_type=$FixMe 36 | 37 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-9]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 38 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-9]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 40 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 41 | 42 | unsafe.enable_getters_and_setters=true 43 | 44 | [version] 45 | ^0.49.1 46 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 50 | 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/screenshots 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ##### Installation to run 3 | 4 | ``` 5 | $ npm install 6 | ``` 7 | 8 | ##### Then to run for iOS 9 | 10 | ``` 11 | $ react-native run-ios 12 | 13 | ``` 14 | 15 | ##### For Android 16 | 17 | ``` 18 | $ react-native run-android 19 | ``` 20 | 21 | 22 | # Explanation of React-Native-Navigation Wix with Redux 23 | 24 | 25 | React Native version: 0.47.1 26 | 27 | ##### Folder Structure: 28 | 29 | ``` 30 | 31 | /src 32 | --/actions 33 | actiontypes.js 34 | index.js 35 | --/components 36 | --/screens 37 | homeTab.js 38 | login.js 39 | screens.js 40 | searchTab.js 41 | --/img 42 | checkmark.png 43 | --/reducers 44 | index.js 45 | rootReducer.js 46 | app.js 47 | 48 | ``` 49 | 50 | ##### app.js 51 | The app component will behave as our overall application, and within app the navigators will exist in there 52 | ``` 53 | app 54 | SingleScreenApp 55 | Login 56 | TabScreenApp 57 | home 58 | search 59 | ``` 60 | The function **startApp()** will change the app's current navigator based on our variable *root* the values that *root* can have are *login* (SingleScreenApp) for when the user is logging in for the first time and *after-login* (TabScreenApp) for when the user has logged in. 61 | 62 | In order for the app to know whether or not these live changes happened we must use Redux's Store. Store has three important methods, **getState()**, it gets the current state of the redux store, the second Store method is called **dispatch()**, it lets you dispatch actions to let you change the state of your application using reducers. With the help of the third redux store method, called **subscribe()**, it lets you register a callback that redux will call anytime an action has been dispatched 63 | 64 | By subscribing our store to the method *onStoreUpdate()*, whenever an action is dispatched, **onStoreUpdate()** will be ran to check if the root has been altered. If it has, it will run **startApp()** with the new root value: *login*, or *after-login*. 65 | ``` 66 | export default class App extends Component { 67 | constructor(props) { 68 | super(props); 69 | store.subscribe(this.onStoreUpdate.bind(this)); 70 | store.dispatch(appActions.appInitialized()); 71 | } 72 | 73 | onStoreUpdate() { 74 | let {root} = store.getState().app; 75 | // handle a root change 76 | if (this.currentRoot != root) { 77 | this.currentRoot = root; 78 | this.startApp(root); 79 | } 80 | } 81 | 82 | startApp(root) { 83 | switch (root) { 84 | Navigation.startSingleScreenApp({ 85 | case 'login': 86 | screen: { 87 | screen: 'ReactNativeReduxExample.Login', 88 | title: 'Welcome', 89 | navigatorStyle: {}, 90 | navigatorButtons: {} 91 | }, 92 | }); 93 | return; 94 | 95 | case 'after-login': 96 | Navigation.startTabBasedApp({ 97 | tabs: [ 98 | { 99 | label: 'Home', 100 | screen: 'ReactNativeReduxExample.HomeTab', 101 | icon: require('./img/checkmark.png'), 102 | selectedIcon: require('./img/checkmark.png'), 103 | title: 'Hey', 104 | overrideBackPress: false, //this can be set to true for android 105 | navigatorStyle: {} 106 | }, 107 | 108 | { 109 | label: 'Search', 110 | screen: 'ReactNativeReduxExample.SearchTab', 111 | icon: require('./img/checkmark.png'), 112 | selectedIcon: require('./img/checkmark.png'), 113 | title: 'Hey', 114 | navigatorStyle: {} 115 | 116 | 117 | } 118 | 119 | ], 120 | }); 121 | return; 122 | default: //no root found 123 | } 124 | } 125 | ``` 126 | 127 | # Passing Store to components 128 | 129 | To have your components access to the states that you are keeping track of through redux, you pass the store and provider when registering your navigation components. 130 | 131 | ##### src/components/screens.js 132 | ``` 133 | 134 | import { Navigation } from 'react-native-navigation'; 135 | import Login from './login'; 136 | import HomeTab from './homeTab'; 137 | import SearchTab from './searchTab'; 138 | 139 | export default (store, Provider) => { 140 | Navigation.registerComponent('ReactNativeReduxExample.Login', () => Login, store, Provider); 141 | Navigation.registerComponent('ReactNativeReduxExample.HomeTab', () => HomeTab, store, Provider); 142 | Navigation.registerComponent('ReactNativeReduxExample.SearchTab', () => SearchTab, store, Provider); 143 | } 144 | 145 | ``` 146 | 147 | # How the root state is managed via reducers 148 | 149 | In Redux, actions are simply objects that describe the type of changes being done in the app, and reducers are functions that perform those changes directly to the state. 150 | 151 | The **combineReducers()** creates a mapping of which reducer will handle which state field in our Store. As an example: 152 | 153 | the key, *todoslist* is a field in our state object, and the value next to that key represents the reducer that will handle that field in our state object. The same idea is applied to the *visibilityFilter* key in this example. By convention, you should name the the reducer the same as the state that it is handling. 154 | ``` 155 | const todoApp= combineReducers({ 156 | todoslist: todoslist, 157 | visibilityFilter: visibilityFilter 158 | }); 159 | ``` 160 | 161 | Since the key and value are the same, we can use ES6 shorthand notation to get the same results. 162 | ``` 163 | const todoApp= combineReducers({ 164 | todoslist, 165 | visibilityFilter 166 | }); 167 | ``` 168 | 169 | Keeping this idea in mind, we head back to our own code. In our application we have the root reducer handling the root state. In redux, state must not be mutable. 170 | 171 | The file *../actions/actiontypes* will just hold constants to indicate what type of action is being done. 172 | 173 | ##### rootReducer.js 174 | ``` 175 | import * as types from '../actions/actiontypes'; 176 | import Immutable from 'seamless-immutable'; 177 | 178 | const initialState = Immutable({ 179 | root: undefined // 'login' / 'after-login' 180 | }); 181 | 182 | //root reducer 183 | export default function root(state = initialState, action = {}) { 184 | switch (action.type) { 185 | case types.ROOT_CHANGED: 186 | return state.merge({ 187 | root: action.root 188 | }); 189 | default: 190 | return state; 191 | } 192 | } 193 | ``` 194 | 195 | We then bundle our reducers into *Reducers/index.js*: 196 | ``` 197 | import root from './appReducer'; 198 | export { 199 | root 200 | } 201 | ``` 202 | 203 | So that we can use this object that is being exported to be placed into our combineReducers method 204 | 205 | ``` 206 | import * as reducers from "./reducers/index"; 207 | const reducer = combineReducers(reducers); 208 | ``` 209 | 210 | We now know know which reducer handles which state field in our application. 211 | 212 | # Dispatching our first action and connecting components 213 | We define an action creator, which is just a function that returns an action object. An action object should always have the *type* key field, to indicate what action is being done. 214 | 215 | We then created **appInitialized()** method to set the root value as login for when the user first opens the application, **appInitialized()** calls the **changeAppRoot()**, which will return a a new action and dispatch that action to our reducers. The same logic is applied to **login()** 216 | 217 | Thanks to the *redux thunk* we can make asynchronous requests based on our actions, vanilla redux is not capable of doing this. We would want this capability because for example, if a user clicked a button to do some API request, that event should grab data from a server, and then when that request is resolved, we create our action. 218 | 219 | *for a clearer explanation of redux thunk refer to References section below* 220 | ``` 221 | return async function(dispatch, getState) { 222 | // since all business logic should be inside redux actions 223 | // this is a good place to put your app initialization code 224 | //run action creator to return action 225 | ``` 226 | 227 | ##### src/actions/index 228 | ``` 229 | import * as types from './actiontypes'; 230 | 231 | //action creator 232 | export function changeAppRoot(root) { 233 | return { 234 | type: types.ROOT_CHANGED, 235 | root: root 236 | }; 237 | } 238 | 239 | export function appInitialized() { 240 | return async function(dispatch, getState) { 241 | // since all business logic should be inside redux actions 242 | // this is a good place to put your app initialization code 243 | dispatch(changeAppRoot('login')); 244 | }; 245 | } 246 | 247 | export function login() { 248 | return async function(dispatch, getState) { 249 | // login logic would go here, and when it's done, we switch app roots 250 | dispatch(changeAppRoot('after-login')); 251 | }; 252 | } 253 | ``` 254 | ##### src/components/screens/login.js 255 | Now that we have our actions defined, we created a method called onLoginPress, which will dispatch the action **login()**. 256 | 257 | We then must make this component be literally connected to redux's store so that we can call dispatch. We do this by running: **export default connect()(Login)** 258 | 259 | ``` 260 | import {connect} from 'react-redux'; 261 | import * as appActions from '../../actions/index'; 262 | .... 263 | ...... 264 | 21 | 22 | 23 | ); 24 | } 25 | 26 | /* 27 | onLoginPress: 28 | Changes the root value of the app to be 'after-login', changing it to tab view 29 | */ 30 | onLoginPress() { 31 | 32 | this.props.dispatch(appActions.login()); 33 | 34 | } 35 | } 36 | 37 | 38 | export default connect()(Login); -------------------------------------------------------------------------------- /src/components/screens/screens.js: -------------------------------------------------------------------------------- 1 | import { Navigation } from 'react-native-navigation'; 2 | import Login from './login'; 3 | import HomeTab from './homeTab'; 4 | import SearchTab from './searchTab'; 5 | 6 | 7 | export default (store, Provider) => { 8 | Navigation.registerComponent('ReactNativeReduxExample.Login', () => Login, store, Provider); 9 | Navigation.registerComponent('ReactNativeReduxExample.HomeTab', () => HomeTab, store, Provider); 10 | Navigation.registerComponent('ReactNativeReduxExample.SearchTab', () => SearchTab, store, Provider); 11 | 12 | } -------------------------------------------------------------------------------- /src/components/screens/searchTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | View 7 | } from 'react-native'; 8 | import {Navigation} from 'react-native-navigation'; 9 | 10 | export default class Searchtab extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | SEARCH!!! 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | const styles = StyleSheet.create({ 23 | container: { 24 | flex: 1, 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | backgroundColor: '#F5FCFF', 28 | }, 29 | welcome: { 30 | fontSize: 20, 31 | textAlign: 'center', 32 | margin: 10, 33 | }, 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/img/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keri4141/React-Native-Navigation-Redux-Example/2f6d03389822dcd57f4005433db996fc156b0425/src/img/checkmark.png -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {root} from './rootReducer'; 2 | 3 | /* 4 | This file exports the reducers as an object which 5 | will be passed onto combineReducers method at src/app.js 6 | */ 7 | export { 8 | root 9 | } -------------------------------------------------------------------------------- /src/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actiontypes'; 2 | import Immutable from 'seamless-immutable'; 3 | 4 | const initialState = Immutable({ 5 | root: undefined // 'login' / 'after-login' 6 | 7 | }); 8 | 9 | //root reducer 10 | export function root(state = initialState, action = {}) { 11 | 12 | switch (action.type) { 13 | 14 | case types.ROOT_CHANGED: 15 | return state.merge({ 16 | root: action.root 17 | }); 18 | 19 | default: 20 | return state; 21 | } 22 | } 23 | --------------------------------------------------------------------------------