I have an AppNavigator inside of an App root component. I need to change the initialRouteName in AppNavigator depends on the props of the root component. In a root component when user is signIn i save a credentials in Keychain. And i need to do this way:
userLoggedIn ? HomeScreen : AuthScreen
Can you tell me please how can i sent userLoggedIn props from the root App component to the AppNavigator?
App.js:
import React, { useState, useEffect } from 'react'
import { ApolloProvider } from 'react-apollo'
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { InMemoryCache } from 'apollo-cache-inmemory'
import * as Keychain from 'react-native-keychain'
import AppNavigator from './AppNavigator'
const httpLink = createHttpLink({
uri: 'http://localhost:4000'
})
const authLink = setContext(async (_, { headers }) => {
const tokens = await Keychain.getGenericPassword()
const accessToken = tokens.accessToken
return {
headers: {
...headers,
authorization: accessToken ? `Bearer ${accessToken}` : ''
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
connectToDevTools: true
})
const App = () => {
const [loggedIn, setLoggedIn] = useState(false)
useEffect(async () => {
const tokens = await Keychain.getGenericPassword()
const accessToken = tokens.accessToken
if (accessToken) {
setLoggedIn(true)
}
})
return (
<ApolloProvider client={client}>
<ApolloHooksProvider client={client}>
<AppNavigator userLoggedIn={loggedIn} />
</ApolloHooksProvider>
</ApolloProvider>
)
}
export default App
AppNavigator.js:
import { createStackNavigator, createAppContainer } from 'react-navigation'
import { HomeScreen, AuthScreen } from './screens'
const AppNavigator = createStackNavigator(
{
AUTH_SCREEN: AuthScreen,
HOME: HomeScreen
},
{
initialRouteName: 'AUTH_SCREEN',
headerMode: 'none'
}
)
export default createAppContainer(AppNavigator)
You should use the switch navigator.
https://reactnavigation.org/docs/en/switch-navigator.html#docsNav.
Related
I am new to react native and context Api so any help would be really appreciated. When I start the app I see undefined is not an object _useContext.appUser error. Below is my code.
App.js
import { AsyncStorage } from 'react-native';
import { NavigationContainer } from '#react-navigation/native'
import AuthStackNavigator from './src/navigators/AuthStackNavigator'
import { LightTheme } from './src/themes/light'
import UserTabsNavigator from './src/navigators/UserTabsNavigator'
import AuthProvider from './src/auth/AuthProvider'
import { AuthContext } from './src/auth/AuthProvider';
export default function App() {
const [loggedIn, setLoggedIn] = useState(false);
const { appUser } = useContext(AuthContext);
console.log('context object' + appUser);
useEffect(() => {
AsyncStorage.getItem('user').then(userString => {
if (userString) {
setLoggedIn(true)
}
}).catch(error => {
console.log(error);
})
})
return (
<AuthProvider>
<NavigationContainer theme={LightTheme}>
{loggedIn ? <UserTabsNavigator /> :
<AuthStackNavigator />}
</NavigationContainer>
</AuthProvider>
);
};
AuthProvider.js
import React, { useState, createContext } from 'react';
import { AsyncStorage } from 'react-native';
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const loginUser = () => {
const fakeUser = { username: 'Test' }
AsyncStorage.setItem('user', JSON.stringify(fakeUser));
setUser(fakeUser);
}
const logoutUser = () => {
AsyncStorage.removeItem('user');
setUser(null);
}
return (
<AuthContext.Provider value={{
appUser: user,
login: loginUser,
logout: logoutUser
}}>
{children}
</AuthContext.Provider>
)
}
export default AuthProvider;
I would really appreciate any help here. I have been struggling with this issue for a while now. I am kinda stuck here.
I have combined my react redux.
Here is my App.js
import React from 'react';
import ReduxThunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { compose, createStore, applyMiddleware } from 'redux';
import reducers from './src/reducers';
import AppContainer from './src/navigator'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const App: () => React$Node = () => {
const store = createStore(reducers, {}, composeEnhancers(applyMiddleware(ReduxThunk)));
return (
<Provider store={store}>
<AppContainer />
</Provider>
);
};
export default App;
src/reducers/index.js
import { combineReducers } from 'redux';
import LoginReducer from './LoginReducer';
export default combineReducers({
LoginRedux: LoginReducer
});
If I use my action login(), I can see login action start, but I can't see dispatch start
import React from 'react';
import {
Text,
View,
TouchableOpacity,
} from 'react-native';
import { connect } from 'react-redux';
import { login } from '../actions';
const LoginScreen = ({ navigation }) => {
// console.log('see my test value', testValue)
return (
<View>
<TouchableOpacity
onPress={() => {
login();
}
}>
<View>
<Text>LOGIN</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
const mapStateToProps = (state) => {
const { testValue } = state.LoginRedux;
console.log('mapStateToProps testValue =>', testValue);
return { testValue };
};
export default connect(mapStateToProps, { login })(LoginScreen);
If I console.log(dispatch), it will show dispatch is not defined.
import { LOGIN } from './types';
export const login = () => {
console.log('login action start')
return (dispatch) => {
console.log('dispatch start');
// console.log(dispatch);
dispatch({ type: LOGIN, testValue: 'I am test' });
};
};
src/reducers/LoginReducer.js
import { LOGIN } from '../actions/types';
const INITIAL_STATE = {
testValue: ''
};
export default (state = INITIAL_STATE, action) => {
console.log('reducer =>', action); // I can't see the console.log
switch (action.type) {
case LOGIN:
return {
...state,
testValue: action.testValue
};
default:
return state;
}
};
I have no idea why my action dispatch is not working. Do I set something wrong ?
Any help would be appreciated.
According to Zaki Obeid help, I update like this:
the action code:
export const login = () => {
console.log('login !');
return { type: LOGIN };
};
the function component code:
import { login } from '../../actions';
export const SettingScreen = ({ navigation, login }) => {
// return view code
}
const mapDispatchToProps = dispatch => ({
// you will use this to pass it to the props of your component
login: () => dispatch(login),
});
connect(null, mapDispatchToProps)(SettingScreen);
In LoginScreen component
you will need to add mapDispatchToProps
const mapDispatchToProps = dispatch => ({
// you will use this to pass it to the props of your component
login: () => dispatch(login()),
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
Then
you will need to destructure from the props as:
const LoginScreen = ({ navigation, login }) => {
// your code
}
In actions.js
the way you use dispatch here requires a library redux-thunk and it's used for async calls.
and the normal action should do the job for you:
export const login = () => ({
type: LOGIN,
testValue: 'I am test'
})
I hope this is useful and will solve your problem,
Have a good day.
In a react-redux app, you obtain the dispatch function either from getting a hold of the store object directly (store.dispatch), or via the react-redux connect function, which will provide dispatch as an argument to a function you write and then later hook up to a component
import { connect } from 'react-redux';
const mapStateToProps = ...
const mapDispatchToProps = (dispatch) => {
return {
someHandle: () => dispatch(myActionCreator())
}
}
export const connect(mapStateToProps, mapDispatchToProps)(MyComponent)
You can't just call dispatch out of thin air -- it's not a global function.
It seems you are using the login function directly. you will have to use the props. Just change the name for confusing and use through props.
import { combineReducers } from 'redux';
import LoginReducer from './LoginReducer';
export default combineReducers({
LoginRedux: LoginReducer
});
If I use my action login(), I can see login action start, but I can't see dispatch start
import React from 'react';
import {
Text,
View,
TouchableOpacity,
} from 'react-native';
import { connect } from 'react-redux';
import { login } from '../actions';
const LoginScreen = ({ navigation, userLogin }) => {
// console.log('see my test value', testValue)
return (
<View>
<TouchableOpacity
onPress={() => {
userLogin();
}
}>
<View>
<Text>LOGIN</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
const mapStateToProps = (state) => {
const { testValue } = state.LoginRedux;
console.log('mapStateToProps testValue =>', testValue);
return { testValue };
};
export default connect(mapStateToProps, { userLogin:login })(LoginScreen);
I am developing an App that uses redux-sagas and react-navigation v3.
The problem that I am facing right now is that I want to use the method navigate inside the saga. The action actually is dispatched, I can see it in the logger, but it doesn't change my screen.
Here are my files:
App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Provider } from 'react-redux';
import store from './store';
import Navigator from './routes/index.js'
export default function App() {
return (
<Provider store={store}>
<Navigator />
</Provider>
);
}
routes.js
import React from "react";
import { View, Text } from "react-native";
import {
createBottomTabNavigator,
createSwitchNavigator,
createStackNavigator,
createAppContainer
} from "react-navigation";
import { FontAwesome } from '#expo/vector-icons';
// Screens
import LoginScreen from '../screens/Login.js';
// Routes
import Leads from './leads.js';
import Settings from './settings.js';
const TabsStack = createBottomTabNavigator(
{
Leads: Leads ,
Settings: Settings,
},
{
defaultNavigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused, horizontal, tintColor }) => {
const { routeName } = navigation.state;
const iconSize = 28;
let iconName;
if (routeName === 'Leads') {
iconName = 'home'
} else if(routeName === 'Settings'){
iconName = 'cogs'
}
return <FontAwesome name={iconName} size={iconSize} color={tintColor} />
},
}),
tabBarOptions: {
activeTintColor: '#208DFF',
inactiveTintColor: '#cecece',
showLabel: false
},
}
);
const LoginStack = createStackNavigator({
Login: {
screen: LoginScreen,
navigationOptions: ({navigation}) => ({
header: null
})
}
});
const Router = createSwitchNavigator(
{
Login: LoginStack,
Tabs: TabsStack,
},
{
initialRouteName: "Login"
}
);
export default createAppContainer(Router);
store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from "redux-saga";
import { createLogger } from 'redux-logger';
import sagas from '../sagas/';
import reducers from '../reducers/';
const sagaMiddleware = createSagaMiddleware();
const loggerMiddleware = createLogger({collapsed: true});
const store = createStore(
reducers,
applyMiddleware(sagaMiddleware, loggerMiddleware)
);
sagaMiddleware.run(sagas);
export default store;
and finally my sagas
import { takeLatest, call, put } from "redux-saga/effects";
import { NavigationActions } from "react-navigation";
import * as sessionActions from '../actions/sessions';
import * as navigateActions from '../actions/navigation.js';
import { login } from '../api/index.js';
import { AsyncStorage } from 'react-native';
export function* loginRequest({email, password}){
let response = yield call(login, {email, password})
if(!response.error){
saveToken(response.auth_token);
yield put({type: sessionActions.SESSION_LOGIN_REQUEST_SUCCESS, user: response})
}else{
yield put({type: sessionActions.SESSION_LOGIN_REQUEST_FAILURE, error: response.error})
}
}
export function* loginRequestSuccessful(){
console.log("Teste! 2");
yield put(NavigationActions.navigate({ routeName: 'Leads' }))
}
async function saveToken(token) {
try {
return await AsyncStorage.setItem("auth_token", token);
} catch (err) {
console.error(err);
}
}
function* loginSaga(){
yield takeLatest(sessionActions.SESSION_LOGIN_REQUEST, loginRequest);
yield takeLatest(sessionActions.SESSION_LOGIN_REQUEST_SUCCESS, loginRequestSuccessful);
}
export default loginSaga;
I get this from the logger
action Navigation/NAVIGATE # 20:23:07.668
RemoteConsole.js:80 prev state {sessions: {…}}
RemoteConsole.js:80 action {type: "Navigation/NAVIGATE", routeName: "Leads", ##redux-saga/SAGA_ACTION: true}
RemoteConsole.js:80 next state {sessions: {…}}
I don't know how can I navigate to another screen inside a sagas function.
The only way I got navigation working is using this.props.navigation inside the component, but I need to work inside the sagas.
In my project, I do it as follow:
Step 1: Set ref for the AppNavigationContainer like
AppContainer = createAppContainer(this.Switch);
constructor(props: any) {
super(props);
this.state = {};
}
handleNavigationChange = (
prevState: NavigationState,
newState: NavigationState,
action: NavigationAction,
) => {
};
render(): React.ReactNode {
return (
<Root>
{/* <StatusBar barStyle="dark-content" /> */}
<this.AppContainer
ref={(navigatorRef: any) => {
serviceProvider.NavigatorService().setContainer(navigatorRef);
}}
onNavigationStateChange={this.handleNavigationChange}
/>
</Root>
);
}
Step 2: Add the navigation service which is called by the ref in this.AppContainer
import { NavigationActions, NavigationParams, NavigationRoute, NavigationContainerComponent, NavigationContainer } from 'react-navigation';
export default class NavigatorService {
container?: NavigationContainerComponent & NavigationContainer;
setContainer = (container: NavigationContainerComponent & NavigationContainer): void => {
this.container = container;
}
getContainer = (container: NavigationContainerComponent & NavigationContainer): any => {
return container;
}
navigate = (routeName: string, params?: NavigationParams): void => {
if (this.container) {
this.container.dispatch(
NavigationActions.navigate({
routeName,
params,
}),
);
}
}
goBack = (): void => {
if (this.container) {
this.container.dispatch(
NavigationActions.back(),
);
}
}
getCurrentRoute(): NavigationRoute | null {
if (!this.container || !this.container.state.nav) {
return null;
}
return this.container.state.nav.routes[this.container.state.nav.index] || null;
}
}
Step 3: In Saga, you can call this to navigate to the screen you want
serviceProvider.NavigatorService().navigate(//screenName)
I have been following this tutorial to integrate redux into my react native app.
https://github.com/jlebensold/peckish
On my Home view, I'm not able to call the functions from my action folder.
One difference is that I'm using react-navigation in my app. Wonder if I need to integrate redux with react navigation to be able to use redux for all data?
Below is the full implementation code I have been doing.
On the Home screen, I call the fetchSite function on ComponentDidMount to launch an async call with axios. But I can't even access to this function.
Sorry for this long post but I can't figure out how to make this work so quite difficult to make a shorter code sample to explain the structure of my app.
Let me know if any question.
index.ios.js
import React from 'react'
import { AppRegistry } from 'react-native'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose} from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import reducer from './app/reducers'
import AppContainer from './app/index'
// middleware that logs actions
const loggerMiddleware = createLogger({ predicate: (getState, action) => __DEV__ });
function configureStore(initialState) {
const enhancer = compose(
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware,
),
);
return createStore(reducer, initialState, enhancer);
}
const store = configureStore({});
const App = () => (
<Provider store={store}>
<AppContainer />
</Provider>
);
AppRegistry.registerComponent('Appero', () => App;
reducers/index.js
import { combineReducers } from 'redux';
import * as sitesReducer from './sites'
export default combineReducers(Object.assign(
sitesReducer,
));
reducers/sites.js
import createReducer from '../lib/createReducer'
import * as types from '../actions/types'
export const searchedSites = createReducer({}, {
[types.SET_SEARCHED_SITES](state, action) {
let newState = {};
action.sites.forEach( (site) => {
let id = site.id;
newState[id] = Object.assign({}, site, { id });
});
return newState;
},
});
../lib/createReducer
export default function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
../actions/types
export const SET_SEARCHED_SITES = 'SET_SEARCHED_SITES';
AppContainer in ./app/index
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ActionCreators } from './actions';
console.log(ActionCreators); //Properly gathered the functions from the actions folder
import { Root } from './config/router';
window.store = require('react-native-simple-store');
window.axios = require('axios');
class App extends Component {
render() {
return (
<Root />
)
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(ActionCreators, dispatch);
}
export default connect(mapDispatchToProps)(App);
ActionCreators in './actions';
import * as SiteActions from './sites'
export const ActionCreators = Object.assign({},
SiteActions,
);
Actions in './actions/sites'
import * as types from './types' //See above
export function fetchSites(token) {
return (dispatch, getState) => {
let instance = axios.create({
baseURL: url + 'api/',
timeout: 10000,
headers: {'Accept' : 'application/json', 'Authorization' : 'Bearer ' + token}
});
instance.get('/sites?page=1')
.then(response => {
console.log(response.data.data);
dispatch(setSearchedSites({sites: response.data.data}));
}).catch(error => {
console.log(error);
});
}
}
export function setSearchedSites({ sites }) {
return {
type: types.SET_SEARCHED_SITES,
sites,
}
}
Root file for navigation based on react-navigation
I made it as simple as possible for this example.
import React from 'react';
import {StackNavigator} from 'react-navigation';
import Home from '../screens/Home';
export const Root = StackNavigator({
Home: {
screen: Home,
}
});
And finally my Home screen
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {Text, View} from 'react-native';
class Home extends Component {
componentDidMount()
{
let token = "12345678" //Just for this example
this.props.fetchSites(token).then( (response) => {
console.log(response);
});
}
render() {
return (
<View>
<Text>This is the Home view</text>
</View>
);
}
}
function mapStateToProps(state) {
return {
searchedSites: state.searchedSites
};
}
export default connect(mapStateToProps)(Home);
To use action methods you need to connect in home screen like this
import { fetchSites } from '<your-path>'
// your Home's other code.
const mapDispatchToProps = (dispatch) => {
return{
fetchSites:dispatch(fetchSites())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Home);
after that you can use fetchSites as this.props.fetchSites whenever you want.
RN: 0.42
I tried to use the new navigation (that was released) + redux and I am unable to map the initial state of the redux to props, in a screen where the store is passed.
I followed this: https://reactnavigation.org/docs/guides/redux
I have written my custom reducer.
export const types = {
...
}
export const actionCreators = {
authenticate: () => async (dispatch, getState) => {
...
}
}
const initialState = {
auth: {
...
}
}
export const reducer = (state = initialState, action) => {
const {auth} = state
const {type, payload, error} = action
switch (type) {
...
}
return state
}
In index.ios.js I have combined my own custom reducer
import { addNavigationHelpers } from 'react-navigation';
import * from 'appReducer';
const AppNavigator = StackNavigator({
Home: { screen: MyTabNavigator },
});
const navReducer = (state, action) => {
const newState = AppNavigator.router.getStateForAction(action, state);
return (newState ? newState : state)
};
const appReducer = combineReducers({
nav: navReducer,
app: appReducer
});
#connect(state => ({
nav: state.nav,
}))
class AppWithNavigationState extends React.Component {
render() {
return (
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
})} />
);
}
}
const store = createStore(appReducer);
class App extends React.Component {
render() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
}
Inside Home.js, 'mapStateToProps' does not work.
import React, { Component } from 'react'
import { View, Text, StyleSheet } from 'react-native'
import { connect } from 'react-redux'
import { actionCreators } from './appRedux'
const mapStateToProps = (state) => ({
//This is the problem: Here 'state' has no 'auth' object attached to it
auth: state.auth
})
class Home extends Component {
componentWillMount() {
const {dispatch} = this.props
//Dispatch works
dispatch(actionCreators.authenticate('testId', 'testToken'))
}
render() {
const {auth} = this.props
return (
<View style={styles.container}>
<Text>
Welcome {auth['name']}
</Text>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
}
})
export default connect(mapStateToProps)(Home)
Note, the dispatch function is available to fire but the 'state' does not have the reducer 'initialState' attached to it.
Please let me know the correct way to attach the reducer initialState to various Screens in the new RN navigation.
I got it where you are doing wrong, in your index.ios.js change import * from 'appReducer'; to import {reducer} from 'appReducer'; then your current combined reducer function will be like
const appReducer = combineReducers({
nav: navReducer,
app: reducer
});
then in your home.js your mapStateToProps should be like
const mapStateToProps = (state) => ({
//This is the problem: Here 'state' has no 'auth' object attached to it
authState: state.app //remember store only contains reducers as state that you had passed in combineReducer function
})
now use it in your component like
this.props.authState.auth //remember you had wrapped your auth object within initialState object