React-native: rendering view before fetching data from Storage - javascript

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()

Related

Props gets updated 3 times in single refresh

I am new in react.I am trying to use react-redux style from the beginning.
Below is what I tried for a simple product listing page.
In my App.js for checking if the user is still logged in.
class App extends Component {
constructor(props) {
super(props);
this.state = {}
}
componentDidMount() {
if (isUserAuthenticated() === true) {
const token = window.localStorage.getItem('jwt');
if (token) {
agent.setToken(token);
}
this.props.appLoad(token ? token : null, this.props.history);
}
}
render() {
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
isUserAuthenticated() === true
? <Component {...props} />
: <Redirect to='/logout' />
)} />
)
return (
<React.Fragment>
<Router>
<Switch>
{routes.map((route, idx) =>
route.ispublic ?
<Route path={route.path} component={withLayout(route.component)} key={idx} />
:
<PrivateRoute path={route.path} component={withLayout(route.component)} key={idx} />
)}
</Switch>
</Router>
</React.Fragment>
);
}
}
export default withRouter(connect(mapStatetoProps, { appLoad })(App));
In my action.js appLoaded action is as under
export const appLoad = (token, history) => {
return {
type: APP_LOAD,
payload: { token, history }
}
}
reducer.js for it
import { APP_LOAD, APP_LOADED, APP_UNLOADED, VARIFICATION_FAILED } from './actionTypes';
const initialState = {
appName: 'Etsync',
token: null,
user: null,
is_logged_in: false
}
const checkLogin = (state = initialState, action) => {
switch (action.type) {
case APP_LOAD:
state = {
...state,
user: action.payload,
is_logged_in: false
}
break;
case APP_LOADED:
state = {
...state,
user: action.payload.user,
token: action.payload.user.token,
is_logged_in: true
}
break;
case APP_UNLOADED:
state = initialState
break;
case VARIFICATION_FAILED:
state = {
...state,
user: null,
}
break;
default:
state = { ...state };
break;
}
return state;
}
export default checkLogin;
And in Saga.js I have watched every appLoad action and performed the operation as under
import { takeEvery, fork, put, all, call } from 'redux-saga/effects';
import { APP_LOAD } from './actionTypes';
import { appLoaded, tokenVerificationFailed } from './actions';
import { unsetLoggeedInUser } from '../../helpers/authUtils';
import agent from '../agent';
function* checkLogin({ payload: { token, history } }) {
try {
let response = yield call(agent.Auth.current, token);
yield put(appLoaded(response));
} catch (error) {
if (error.message) {
unsetLoggeedInUser();
yield put(tokenVerificationFailed());
history.push('/login');
} else if (error.response.text === 'Unauthorized') {
unsetLoggeedInUser();
yield put(tokenVerificationFailed());
}
}
}
export function* watchUserLogin() {
yield takeEvery(APP_LOAD, checkLogin)
}
function* commonSaga() {
yield all([fork(watchUserLogin)]);
}
export default commonSaga;
After that for productLists page my code is as under
//importing part
class EcommerceProductEdit extends Component {
constructor(props) {
super(props);
this.state = {}
}
componentDidMount() {
**//seeing the props changes**
console.log(this.props);
this.props.activateAuthLayout();
if (this.props.user !== null && this.props.user.shop_id)
this.props.onLoad({
payload: Promise.all([
agent.Products.get(this.props.user),
])
});
}
render() {
return (
// JSX code removed for making code shorter
);
}
}
const mapStatetoProps = state => {
const { user, is_logged_in } = state.Common;
const { products } = state.Products.products.then(products => {
return products;
});
return { user, is_logged_in, products };
}
export default connect(mapStatetoProps, { activateAuthLayout, onLoad })(EcommerceProductEdit);
But in this page in componentDidMount if I log the props, I get it three time in the console. as under
Rest everything is working fine. I am just concerned,the code i am doing is not up to the mark.
Any kinds of insights are highly appreciated.
Thanks
It's because you have three state updates happening in ways that can't batch the render.
You first render with no data. You can see this in the first log. There is no user, and they are not logged in.
Then you get a user. You can see this in the second log. There is a user, but they are not logged in.
Then you log them in. You can see this in the third log. There is a user, and they are logged in.
If these are all being done in separate steps and update the Redux store each step you'll render in between each step. If you however got the user, and logged them in, and then stored them in the redux state in the same time frame you'd only render an additional time. Remember React and Redux are heavily Async libraries that try to use batching to make sure things done in the same time frame only cause one render, but sometimes you have multiple network steps that need to be processed at the same time. So no you're not doing anything wrong, you just have a lot of steps that can't easily be put into the same frame because they rely on some outside resource that has its own async fetch.

how to set react parent component state by a async function and then pass this state to child as props?

so i want to set the access by firebase function and then pass this access props to tabs component as props ,but tabs component is get the initial state null,firebase auth funtion is resolving after that .
class Admin extends React.Component {
state = {
access: null,
};
componentDidMount() {
this.unListen = firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState(() => ({ access: true }));
}
});
}
componentWillUnmount() {
this.unListen();
}
render(){
return <Tab access={this.state.access}/>
}
}
You can do conditional rendering and not render the tabs until you get access:
return this.state.access
? <Tab access={this.state.access}/>
: <div>Not authorized</div>
It should not be a problem. When you update the state, the component will re-render and all its children will also be re-rendered. If you don't want to render anything while access is null try the following code.
class Admin extends React.Component {
state = {
access: null,
};
componentDidMount() {
this.unListen = firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState(() => ({ access: true }));
}
});
}
componentWillUnmount() {
this.unListen();
}
render(){
const access = {this.state};
return {access && <Tab access={access}/>}
}
}
OR
{access ? <Tab access={access}/> : 'Not Authorized'}
componentDidMount function is called after the render and even though you have your call in componentWillMount, since its an async call, it will only be resolved after the component render cycle has been triggered and hence the data will only have a value after render. In order to correctly handle such a scenario, you must conditionally render your data in the render.
class Admin extends React.Component {
state = {
access: null,
};
componentDidMount() {
this.unListen = firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState(() => ({ access: true }));
}
});
}
componentWillUnmount() {
this.unListen();
}
render(){
const { access } = this.state;
if(access !== null) {
return null;
}
return <Tab access={this.state.access}/>
}
}

How can I navigate once when I found a JWT?

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

Why in the new react-router-dom <Redirect/> does not fire inside the setTimout?

So, I tried to make a little delay between the logout page and redirecting to the main website page. But I fall into the problem, that the react-router-dom <Redirect/> method does not want fire when we put it inside the setTimeout() of setInterval().
So, if we unwrapped it from timer, the <Redirect/> will work normally.
What is the problem is, any suggestions?
My code:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import axios from 'axios';
class LogoutPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(
this.setState({
navigate: true
}),
)
.catch(err => {
console.error(err);
});
if (this.state.navigate) {
setTimeout(() => {
return <Redirect to="/employers" />
}, 2000);
}
};
render() {
if (this.state.navigate) {
setTimeout(() => {
return <Redirect to="/employers" />
}, 2000);
}
return (
<div>You successfully logouted</div>
)
}
}
export default LogoutPage;
You want the render() method to return <Redirect /> in order for the redirect to take place. Currently the setTimeout function returns a <Redirect />, but this does not affect the outcome of the render() itself.
So instead, the render() should simply return <Redirect /> if this.state.navigate is true and you delay the setState({ navigate: true }) method with 2 seconds.
Here's a corrected, declarative way of doing it:
class LogoutPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(() => setTimeout(() => this.setState({ navigate: true }), 2000))
.catch(err => {
console.error(err);
});
};
render() {
if (this.state.navigate) {
return <Redirect to="/employers" />
}
return (
<div>You successfully logouted</div>
);
}
}
For the imperative version, see #Shubham Khatri's answer.
Redirect needs to be rendered for it to work, if you want to redirect from an api call, you can use history.push to programmatically navigate. Also you would rather use setState callback instead of timeout
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(
this.setState({
navigate: true
}, () => {
this.props.history.push("/employers");
}),
)
.catch(err => {
console.error(err);
});
};
Check Programmatically Navigate using react-router for more details
Even if you add Redirect within setTimeout in render it won't work because it will not render it, rather just be a part of the return in setTimeout

How to use a custom component with react-router route transitions?

The article Confirming Navigation explains how to use a browser confirmation box in your transition hook. Fine. But I want to use my own Dialog box. If I were to use the methods from the history module I think this is possible. Is it possible to do this with the setRouteLeaveHook in react-router?
The core problem is that setRouteLeaveHook expects the hook function to return its result synchronously. This means you don't have the time to display a custom dialog component, wait for the user to click an option, and then return the result. So we need a way to specify an asynchronous hook. Here's a utility function I wrote:
// Asynchronous version of `setRouteLeaveHook`.
// Instead of synchronously returning a result, the hook is expected to
// return a promise.
function setAsyncRouteLeaveHook(router, route, hook) {
let withinHook = false
let finalResult = undefined
let finalResultSet = false
router.setRouteLeaveHook(route, nextLocation => {
withinHook = true
if (!finalResultSet) {
hook(nextLocation).then(result => {
finalResult = result
finalResultSet = true
if (!withinHook && nextLocation) {
// Re-schedule the navigation
router.push(nextLocation)
}
})
}
let result = finalResultSet ? finalResult : false
withinHook = false
finalResult = undefined
finalResultSet = false
return result
})
}
Here is an example of how to use it, using vex to show a dialog box:
componentWillMount() {
setAsyncRouteLeaveHook(this.context.router, this.props.route, this.routerWillLeave)
}
​
routerWillLeave(nextLocation) {
return new Promise((resolve, reject) => {
if (!this.state.textValue) {
// No unsaved changes -- leave
resolve(true)
} else {
// Unsaved changes -- ask for confirmation
vex.dialog.confirm({
message: 'There are unsaved changes. Leave anyway?' + nextLocation,
callback: result => resolve(result)
})
}
})
}
I made it work by setting a boolean on state whether you have confirmed to navigate away (using react-router 2.8.x). As it says in the link you posted:
https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md
return false to prevent a transition w/o prompting the user
However, they forget to mention that the hook should be unregistered as well, see here and here.
We can use this to implement our own solution as follows:
class YourComponent extends Component {
constructor() {
super();
const {route} = this.props;
const {router} = this.context;
this.onCancel = this.onCancel.bind(this);
this.onConfirm = this.onConfirm.bind(this);
this.unregisterLeaveHook = router.setRouteLeaveHook(
route,
this.routerWillLeave.bind(this)
);
}
componentWillUnmount() {
this.unregisterLeaveHook();
}
routerWillLeave() {
const {hasConfirmed} = this.state;
if (!hasConfirmed) {
this.setState({showConfirmModal: true});
// Cancel route change
return false;
}
// User has confirmed. Navigate away
return true;
}
onCancel() {
this.setState({showConfirmModal: false});
}
onConfirm() {
this.setState({hasConfirmed: true, showConfirmModal: true}, function () {
this.context.router.goBack();
}.bind(this));
}
render() {
const {showConfirmModal} = this.state;
return (
<ConfirmModal
isOpen={showConfirmModal}
onCancel={this.onCancel}
onConfirm={this.onConfirm} />
);
}
}
YourComponent.contextTypes = {
router: routerShape
};
Posting my solution for intercept back button or even a route change. This works with React-router 2.8 or higher. Or even with withRouter
import React, {PropTypes as T} from 'react';
...
componentWillMount() {
this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeaveCallback.bind(this));
}
routerWillLeaveCallback(nextLocation) {
let showModal = this.state.unsavedChanges;
if (showModal) {
this.setState({
openUnsavedDialog: true,
unsavedResolveCallback: Promise.resolve
});
return false;
}
return true;
}
}
YourComponent.contextTypes = {
router: T.object.isRequired
};
Above is great except when user goes back in history. Something like the following should fix the problem:
if (!withinHook && nextLocation) {
if (nextLocation.action=='POP') {
router.goBack()
} else {
router.push(nextLocation)
}
}
Here's my solution to the same. I made a custom dialog component that you can use to wrap any component in your app. You can wrap your header and this way have it present on all pages. It assumes you're using Redux Form, but you can simply replace areThereUnsavedChanges with some other form change checking code. It also uses React Bootstrap modal, which again you can replace with your own custom dialog.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter, browserHistory } from 'react-router'
import { translate } from 'react-i18next'
import { Button, Modal, Row, Col } from 'react-bootstrap'
// have to use this global var, because setState does things at unpredictable times and dialog gets presented twice
let navConfirmed = false
#withRouter
#connect(
state => ({ form: state.form })
)
export default class UnsavedFormModal extends Component {
constructor(props) {
super(props)
this.areThereUnsavedChanges = this.areThereUnsavedChanges.bind(this)
this.state = ({ unsavedFormDialog: false })
}
areThereUnsavedChanges() {
return this.props.form && Object.values(this.props.form).length > 0 &&
Object.values(this.props.form)
.findIndex(frm => (Object.values(frm)
.findIndex(field => field && field.initial && field.initial !== field.value) !== -1)) !== -1
}
render() {
const moveForward = () => {
this.setState({ unsavedFormDialog: false })
navConfirmed = true
browserHistory.push(this.state.nextLocation.pathname)
}
const onHide = () => this.setState({ unsavedFormDialog: false })
if (this.areThereUnsavedChanges() && this.props.router && this.props.routes && this.props.routes.length > 0) {
this.props.router.setRouteLeaveHook(this.props.routes[this.props.routes.length - 1], (nextLocation) => {
if (navConfirmed || !this.areThereUnsavedChanges()) {
navConfirmed = false
return true
} else {
this.setState({ unsavedFormDialog: true, nextLocation: nextLocation })
return false
}
})
}
return (
<div>
{this.props.children}
<Modal show={this.state.unsavedFormDialog} onHide={onHide} bsSize="sm" aria-labelledby="contained-modal-title-md">
<Modal.Header>
<Modal.Title id="contained-modal-title-md">WARNING: unsaved changes</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to leave the page without saving changes to the form?
<Row>
<Col xs={6}><Button block onClick={onHide}>Cancel</Button></Col>
<Col xs={6}><Button block onClick={moveForward}>OK</Button></Col>
</Row>
</Modal.Body>
</Modal>
</div>
)
}
}

Categories