I am just getting stuck into react-native and need some help navigating to a protected screen when a token is found. Where should I look for a token on app load? And how can I navigate the user once without calling navigate multiple times? The problem I have is I am checking for a token on component mount, which is nested inside a stack. If I navigate to another part of the stack, the function is called again and I am unable to navigate. I can retrieve the token outside of the stack, but then I am having trouble navigating, as I need to pass props.navigate within a screen component. What is the recommended approach to finding a token, and making a navigation?
App.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import store from './store';
import RootContainer from './screens/RootContainer';
export default class App extends React.Component {
render() {
return (
<Provider store={store}>
<RootContainer />
</Provider>
);
}
}
RootContainer.js
...
render() {
const MainNavigator = createBottomTabNavigator({
welcome: { screen: WelcomeScreen },
auth: { screen: AuthScreen },
main: {
screen: createBottomTabNavigator({
map: { screen: MapScreen },
deck: { screen: DeckScreen },
review: {
screen: createStackNavigator({
review: { screen: ReviewScreen },
settings: { screen: SettingsScreen }
})
}
})
}
// Route Configuration for Initial Tab Navigator
}, {
// do not instantly render all screens
lazy: true,
navigationOptions: {
tabBarVisible: false
}
});
return (
<View style={styles.container}>
<MainNavigator />
</View>
);
}
}
WelcomeScreen.js
...
componentDidMount(){
this.props.checkForToken(); // async method
}
// Async Action
export const checkForToken = () => async dispatch => {
console.log("action - does token exist ?");
let t = await AsyncStorage.getItem("jwt");
if (t) {
console.log("action - token exists");
// Dispatch an action, login success
dispatch({ type: LOGIN_SUCCESS, payload: t });
} else {
return null;
}
}
// WelcomeScreen.js continued
componentWillRecieveProps(nextProps){
this.authComplete(nextProps);
}
authComplete(props){
if(props.token){
props.navigation.navigate('map'); // called again and again when I try to navigate from within the Bottom Tab Bar Component
}
}
render(){
if(this.props.appLoading){ // default True
return ( <ActivityIndicator />);
}
return ( <Text>WelcomeScreen</Text> );
}
const mapStateToProps = state => {
return {
token: state.auth.token,
appLoading: state.auth.appLoading // default True
};
};
export default connect(mapStateToProps, actions)(WelcomeScreen);
I would suggest, not to store navigation state in redux.
Just navigate when you found a token or the user logged in.
If you still want to use redux or simply want to react on props changes, then the way is to use some Redirect Component, and render it only when the token changed from nothing to something. You could read about such implementation from react-router here. I think there is no such implementation for React Navigation.
When you are using React Navigation, then I would suggest to look into the docs, because I think it solves the problem you have.
https://reactnavigation.org/docs/en/auth-flow.html
Related
I have a Next.js app, I'm using getInitialProps in my _app.js in order to be able to have persistent header and footer. However, I'm also needing to set data in a Context, and I need to be able to fetch the data based off of a cookie value. I've got the basics working just fine, however my _app sets the cookie on the first load, and then when I refresh the page it pulls in the appropriate data. I'm wondering if there's a way to be able to set the cookie first before fetching the data, ensuring that, if there's a cookie present, it will always pull in that data on the first load? Here is my _app.js, and, while I'm still working on the dynamic cookie value in my cookies.set method, I'm able to fetch the right data from my Prismic repo by hard-coding sacramento-ca for now, as you'll see. All I'm really needing is the logic to ensure that the cookie sets, and then the data fetches.
_app.js
import React from 'react';
import { AppLayout } from 'components/app-layout/AppLayout';
import { Footer } from 'components/footer/Footer';
import { Header } from 'components/header/Header';
import { LocationContext } from 'contexts/Contexts';
import Cookies from 'cookies';
import { Client } from 'lib/prismic';
import NextApp, { AppProps } from 'next/app';
import 'styles/base.scss';
import { AppProvider } from 'providers/app-provider/AppProvider';
interface WithNavProps extends AppProps {
navigation: any;
location: string;
dealer?: any;
cookie: string;
}
const App = ({ Component, pageProps, navigation, dealer }: WithNavProps) => {
const { Provider: LocationProvider } = LocationContext;
const locationData = dealer ? dealer : null;
return (
<LocationProvider value={{ locationData }}>
<AppProvider>
<AppLayout>
<Header navigation={navigation} location={dealer} />
<Component {...pageProps} />
<Footer navigation={navigation} />
</AppLayout>
</AppProvider>
</LocationProvider>
);
};
export default App;
App.getInitialProps = async (appContext: any) => {
const appProps = await NextApp.getInitialProps(appContext);
const cookies = new Cookies(appContext.ctx.req, appContext.ctx.res);
try {
cookies.set('dealerLocation', 'sacramento-ca', {
httpOnly: true,
});
const { data: navigation } = await Client.getByUID('navigation', 'main-navigation', {
lang: 'en-us',
});
const results = await Client.getByUID('dealer', cookies.get('dealerLocation'), {
lang: 'en-us',
});
return {
...appProps,
navigation,
dealer: results,
};
} catch {
const { data: navigation } = await Client.getByUID('navigation', 'main-navigation', {
lang: 'en-us',
});
return {
...appProps,
navigation,
};
}
};
I've got a working auth configuration set up using firebaseui. I have a private landing page that I'd like to redirect the user to, but I'm not sure how to pass the credentialed response into my redux store.
I basically want to call the handleClickLogin method (currently hooked to a dummy button) of my Home component from my signInSuccess callback. In other words I'm trying to dispatch(login()); when I get a successfull signin, which in turn adds the flag to my redux store which I can then use to gate my private landing page. Since firebase.js is not in the component tree, I don't have access to dispatch here, so how do I get the response hooked in to my store?
firebase.js
const uiConfig = ({
// signInSuccessUrl: '/',
signInOptions: [
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
callbacks: {
signInSuccess: (resp) => <<<???>>>,
},
});
firebase.initializeApp(config);
const ui = new firebaseui.auth.AuthUI(firebase.auth());
export const startFirebaseUI = elementId => {
ui.start(elementId, uiConfig);
};
Home.jsx (stripped down)
export class Home extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
};
componentDidMount = () => {
startFirebaseUI('#firebaseui-auth-container');
}
handleClickLogin = () => {
const { dispatch } = this.props;
dispatch(login());
};
render() {
const { user } = this.props;
return (
<Background>
<HomeContainer>
<Button
onClick={this.handleClickLogin}
>
<Text ml={2}>Start</Text>
</Button>
<div id="firebaseui-auth-container" />
</HomeContainer>
</Background>
);
}
}
function mapStateToProps(state) {
return { user: state.user };
}
export default connect(mapStateToProps)(Home);
Somehow typing the question helped me figured it out. Just needed to import the store and the appropriate action, then dispatch it directly.
import { store } from 'store/index';
import { login } from 'actions/index';
callbacks: {
signInSuccess: (resp) => store.dispatch(login(resp)),
}
I have a page that generates a random number in getInitialProps() after 2 seconds. There's a button that allows the user to "refresh" the page via Router.push(). As getInitalProps() takes 2 seconds to complete, I'll like to display a loading indicator.
import React from 'react'
import Router from 'next/router'
export default class extends React.Component {
state = {
loading: false
}
static getInitialProps (context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({random: Math.random()})
}, 2000)
})
}
render() {
return <div>
{
this.state.loading
? <div>Loading</div>
: <div>Your random number is {this.props.random}</div>
}
<button onClick={() => {
this.setState({loading: true})
Router.push({pathname: Router.pathname})
}}>Refresh</button>
</div>
}
}
How can I know when Router.push()/getInitialProps() completes so I can clear my loading indicator?
Edit: Using Router.on('routeChangeComplete') is the most obvious solution. However, there are multiple pages and the user could click on the button multiple times. Is there a safe way to use Router events for this?
Router.push() returns a Promise. So you can do something like...
Router.push("/off-cliff").then(() => {
// fly like an eagle, 'til I'm free
})
use can use Router event listener in pages/_app.js, manage page loading and inject state into component
import React from "react";
import App, { Container } from "next/app";
import Router from "next/router";
export default class MyApp extends App {
state = {
loading: false
};
componentDidMount(props) {
Router.events.on("routeChangeStart", () => {
this.setState({
loading: true
});
});
Router.events.on("routeChangeComplete", () => {
this.setState({
loading: false
});
});
}
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<Container>
{/* {this.state.loading && <div>Loading</div>} */}
<Component {...pageProps} loading={this.state.loading} />
</Container>
);
}
}
and you can access loading as a props in your page component.
import React from "react";
import Router from "next/router";
export default class extends React.Component {
static getInitialProps(context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ random: Math.random() });
}, 2000);
});
}
render() {
return (
<div>
{this.props.loading ? <div>Loading</div> : <div>Your random number is {this.props.random}</div>}
<button
onClick={() => {
this.setState({ loading: true });
Router.push({ pathname: Router.pathname });
}}
>
Refresh
</button>
</div>
);
}
}
you can also show loading text in _app.js (I've commented), that way you don't have to check loading state in every pages
If you wanna use third-party package here a good one nprogress
How can I replace screen with React Navigation for React Native
Now I'm a newbie I can't understand about getStateForAction
now I have params on Screen 1 and Navigate to Screen 2 with passing params username with basic Navigate it's nested screen 1 > 2 on stack
But I need to replace screen 1 with screen 2 after screen 2 it's on active (replace like ActionConst.REPLACE on router flux) and sending params on a new way
Can someone guide me thank you.
screen 1
onPress = () => {
this.props.navigation.navigate('Sc2', {username: this.state.username});
}
Screen 2
componentWillMount() {
const {state} = this.props.navigation;
console.log(state.params.username);
}
---------------
Router
export const LoginNavStack = StackNavigator({
Sc1: {
screen: Sc1
},
Sc2: {
screen: Sc2,
},
});
Incase anyone's still figuring out how to replace screens in React Native and is using react-navigation, here it is:
import { StackActions } from '#react-navigation/native';
navigation.dispatch(
StackActions.replace('Profile', {
user: 'jane',
})
);
For more information refer: https://reactnavigation.org/docs/stack-actions/#replace
Use this instead of navigation.replace
navigation.reset({
index: 0,
routes: [{ name: 'Profile' }],
});
You can use navigation.replace
Use
this.props.navigation.replace('Sc2');
Instead of
this.props.navigation.navigate('Sc2', {username: this.state.username});
You can find more from here https://reactnavigation.org/docs/en/navigation-key.html
"Replace" is only available in stack navigation
replace typed:
import { useNavigation } from '#react-navigation/core'
import { StackNavigationProp } from '#react-navigation/stack'
type YourNavigatorParamList = {
CurrentScreen: undefined
DifferentScreen: { username: string }
}
const navigation = useNavigation<StackNavigationProp<YourNavigatorParamList, 'CurrentScreen'>>()
navigation.replace('DifferentScreen', { username: 'user01' })
https://reactnavigation.org/docs/navigation-prop/#navigator-dependent-functions
https://reactnavigation.org/docs/typescript#annotating-usenavigation
This is working for me.
import { View, Text, Button } from 'react-native'
const Home = (nav:any) => {
const goToLogin = () => {
nav.navigation.replace('Login');
}
return (
<View>
<Text>Home</Text>
<Button title='Go To Login' onPress={goToLogin} />
</View>
)
}
export default Home;
I am trying to render Signin component if user not logged in and if user logged in I am trying to render Home component. On Signin component set Storage 'isLIn' to 'true' On Signout [from home component] set Storage 'isLIn' to 'false' and Every time React-Native App opens checking Storage and Setting State as value of Storage.
Please look at code:
import React, { Component } from 'react';
import { AsyncStorage } from 'react-native';
import { Scene, Router } from 'react-native-router-flux';
import Login from './login_component';
import Home from './home_component';
var KEY = 'isLIn';
export default class main extends Component {
state = {
isLoggedIn: false
};
componentWillMount() {
this._loadInitialState().done();
}
_loadInitialState = async () => {
try {
let value = await AsyncStorage.getItem(KEY);
if (value !== null && value === 'true') {
this.setState({ isLoggedIn: true });
} else {
this.setState({ isLoggedIn: false });
}
} catch (error) {
console.error('Error:AsyncStorage:', error.message);
}
};
render() {
const _isIn = (this.state.isLoggedIn===true) ? true : false;
return (
<Router>
<Scene key="root" hideNavBar hideTabBar>
<Scene key="Signin" component={Login} title="Signin" initial={!_isIn} />
<Scene key="Home" component={Home} title="Home" initial={_isIn}/>
</Scene>
</Router>
);
}
}
I don't know why but view render first before Storage gets value. According to lifecycle of react-native render() execute only after componentWillMount() as React_Doc says.
I am using AsyncStorage to get set and remove Storage and also using React-Native-Router-Flux for routing.
I have tried solutions:
forceUpdate
Solution1
Since what you are doing is asynchronous you can not tell the lifecycle to wait for it. But React provides states and these you can use e.g.
state = {
isLoggedIn: false
isLoading: true
};
And set the state in the async
_loadInitialState = async () => {
try {
let value = await AsyncStorage.getItem(KEY);
if (value !== null && value === 'true') {
this.setState({ isLoggedIn: true, isLoading: false });
} else {
this.setState({ isLoggedIn: false, isLoading: false });
}
} catch (error) {
console.error('Error:AsyncStorage:', error.message);
}
};
And then in your render method you can place a placeholder until your asynctask is finished
render() {
if(this.state.isLoading) return <div> Loading... </div>
else return...
}
Invoking setState in componentWillMount does NOT trigger a re-rendering. componentWillMount runs after state has been set and before the view has been re-rendered. From React Native Docs:
"componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method." - https://facebook.github.io/react/docs/react-component.html#componentwillmount
Instead, you should call _loadInitialState in componentWillReceiveProps()