setState() on root container doesn't keep routes of react-navigation (V3) - javascript

I have a root Component (App) which renders a nested navigation as shown in the code.
Inside of app, I have the user object (stored in state) which is used by all child objects. It contains information about the groups you're in.
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
user: null
}
}
render() {
if (!this.state.user) {
// from the login component, I make an API call to my backend.
// If the user logs in, I do app.setState({user: loggedInUser}) from inside the LoginComponent
return <LoginComponent app={this} />
}
return createAppContainer(createSwitchNavigator({
MainApp: {
screen: createBottomTabNavigator({
StartPage: {
screen: ({ navigation }) => {
return <StartPage navigation={navigation} app={this} />
}
},
Groups: {
screen: createStackNavigator({
ListGroups: {
// When clicking on a Group from the GroupsPage, I execute:
// this.props.navigation.navigate('GroupDetail', { group, app })
// (from inside the GrousPage screen)
screen: ({ navigation }) => <GroupsPage navigation={navigation} app={this} />
},
GroupDetail: {
// Inside the GroupDetail, you can leave or join a group.
// If you do that, I'm making an API call and get the new user object.
// Then, I do this.props.navigation.getParam('app').setState({user: newUser})
// "app" was passed from ListGroups
screen: GroupDetail
}
})
}
})
}
}))
}
}
Now, when I'd like to set the updated user (from GroupDetail via app.setState({user: newUser})), the navigation doesn't keep its route and goes back to StartPage.
However, what I wanted instead is to re-render GroupDetail with the new user.
How would I be able to keep the navigation state? Or do you think I have a design flaw and any idea on fixing it? Thanks!
(Yes, I'd like to keep the user object only inside of App and pass it down. Main reason is to only have few API calls)

you should not create a new navigator on authentication but instead add a route to your navigator, which will be your default route and this route will take care of your login.
It should be passed a callback to set the user like this:
setUser = user => this.setState({user});
return <AuthPage setUser={this.setUser} /> // You also don't have to pass navigation. It will be passed automatically.
Inside your AuthPage you can set the user with
this.props.setUser(newUser);
and navigate to your desired route with the navigation prop or just go back to the previous route:
this.props.navigation.navigate('App');
Or this.props.navigation.goBack();
To keep user logged in you should check if the user is logged in in your Auth route and react accordingly:
componentDidMount() {
const user = getUser(); // This depends on how you persist the user
this.props.navigation.navigate(user ? 'App' : 'Auth');
}
Hope this helps.

According to docs, you have to persist the user navigation state.
read more here.

Related

How to implement routing from and to root (index.js) in react with redux

I'm programming a react server webpage, trying to redirect from index.js (i.e: localhost:3000) to Login page: (localhost:3000/login), and from login to index (in case of failed login). What do I need to write in index.js and login.js?
This is for a react based app, using also redux framework. I've tried a few ways including setting up a BrowserRouter etc. All won't really do the redirecting.
My current code is this:
in index.js:
class Index extends Component {
static getInitialProps({store, isServer, pathname, query}) {
}
render() {
console.log(this.props);
//const hist = createMemoryHistory();
if (!this.props.isLoggedIn){
return(<Switch><Route exact path = "/login"/></Switch>)
}
else{...}
in login.js:
render() {
console.log(this.props);
if (fire.auth().currentUser != null) {
var db = fire.firestore();
db.collection("users").doc(fire.auth().currentUser.uid).get().then((doc) => {
this.props.dispatch({ type: 'LOGIN', user: doc.data() });
})
}
const { isLoggedIn } = this.props
console.log(isLoggedIn)
if (isLoggedIn) return <Redirect to='/' />
I except the root to redirect to login if no session is on, and login to redirect to root once there is a successful login.
I am currently getting "You should not use <Switch> outside a <Router>" at index (I have tried to wrap with BrowserRouter, ServerRouter, Router. the first says it needs DOM history. adding history does not change error. two others do not error but are blank display on browser.)
and "ReferenceError: Redirect is not defined" at login.
Any help will be appreciated.
you can use a HOC (Higher-Order Components)
something like this
export default ChildComponent => {
class ComposedComponent extends Component {
componentWillMount() {
this.shouldNavigateAway();
}
componentWillUpdate() {
this.shouldNavigateAway();
}
shouldNavigateAway() {
if (!this.props.authenticated) {
this.props.history.push('/')
}
}
render() {
return <ChildComponent {...this.props} />
}
}
}
As of now you're trying to return a route declaration wrapped in a Switch component. If you want to redirect the user to the /login page if hes not logged in, you need the route to be declared higher up in the component hierarchy, and then you would be able to return the <Redirect /> component. Either way, I would suggest you check out the react router documentation to see how they do authentication.
https://reacttraining.com/react-router/web/example/auth-workflow

How to navigate to directly to the initial route when using TabNavigator?

I am using TabNavigator from react-navigation. It's working fine but I need to do a little trick.
When I navigate between StackNavigator routes, after changing tabs I need my route go directly in the initial route. So I need the route state to be reset.
const HomeStack = StackNavigator({
Main: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
const AboutStack = StackNavigator({
Main: { screen: AboutScreen },
});
TabNavigator(
{
Home: { screen: HomeStack },
About: { screen: AboutStack },
}
Let's say I am in the Main route from the Home tab and then I have navigated to Profile before switching to the About tab. When I go back to the Home tab I want my app directly to navigate to the Main route and clear history. Just like a reset.
Any suggestion ?
You maybe could use the willFocus listener in the Profile route of your HomeStack.
Listener:
class Profile extends Component {
componentDidMount() {
this.didFocusListener = this.props.navigation.addListener(
'didFocus',
() => { console.log('did focus') },
);
}
componentWillUnmount() {
this.didFocusListener.remove();
}
render() {
return ( /* your render */ );
}
}
And then in the case you are on Main and you are navigating to your Profile route. You should set a params in the navigation to say that the previous route is Main.
Route parameters: navigation.navigate('Profile', { previous_screen: 'Main' });
So now in your willFocus listener:
if the previous_screen param is set, it means that you don"t have to do anything.
If not, means that you come from the other tab and that it will navigate to the wrong route. So you can either reset you're navigation route or just navigate to the 'Profile' route.
NOTE:
I didn't try this solution and maybe the transition animation is not going to be smooth. So tell me if it does the job well or not.

Checking if user signed in before react native

In my react native app I save the user information securely on the key chain, so that after they have logged in once, I save the information and then the next time the user comes, the information is already there and so the user won't need to log in.
The issue is that I do the check in componentDidMount, and then if the user has never logged in before or logged out in their last visit I redirect them to the loginScreen like so:
componentDidMount() {
//Need to check if they've signed in before by checking the USER_INFO.
SecureStore.getItemAsync("USER_INFO").then(response => {
//Have they signed in before?
if (response !== undefined) {
//yes.
//continue with app stuff.
}
else {
//Not logged in before need to go to login.
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Login', params: this.props.navigation.state.params }),
]
});
this.props.navigation.dispatch(resetAction);
}
});
}
The problem is that I get a warning that 'Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.'. Which makes sense because I am redirecting before the screen has rendered, but then the question is, where should I perform these checks?
Thanks
I see that you are using react-navigation. I have done the same thing that you are trying to accomplish but in a different way.
To simplify I have three screens in a navigator
// Navigator.js
export const RootNavigator = StackNavigator({
Splash: { screen: SplashView },
Login: { screen: LoginView },
RootDrawer: { screen: RootDrawer }
}, {
headerMode: 'none',
initialRouteName: 'Splash'
});
And then in my SplashView (which is my starting point) I authenticate the user in its constructor. And while it is authenticating the user, the SplashView is simply rendering a Text that says "Splash Screen" but could obviously be anything.
constructor(props) {
super(props);
this.authenticateSession();
}
authenticateSession() {
const { navigation } = this.props;
dispatch(storeGetAccount())
.then(() => {
navigation.dispatch(navigateToAccountView(true));
})
.catch(() => {
navigation.dispatch(navigateToLoginView());
});
}
The functions navigateToAccountView() and navigateToLoginView() are just so I can use them at other places but the navigateToLoginView() looks like this
export function navigateToLoginView() {
return NavigationActions.reset({
index: 0,
key: null,
actions: [
NavigationActions.navigate({ routeName: 'Login' })
]
});
}
Usually and as far as I know, the best way to handle this kind of checks is by wrapping your component by some HOC(High Order Component) Doing your logic there, and depending if the user passes the checks you can throw a redirection to login page or load the user data and keep forward rendering your component.
This is a good practice so you can create a withAuth() HOC that will wrap the components or the parts of your app that can only be accessed by authenticated users. And you will have a component that is highly reusable.
So you will export your "protected component" like this:
export default withAuth(myComponent)
performing the logic in the withAuth HOC instead of in you component.

React update parent component when child changes the Context

I have four React components:
An overall parent (App or similar)
A Header child, called by App
A Profile page
A LogoutLink, called by Profile
I am implementing a User authentication/login system using Auth0. When the user logs in (via a Login button in the Header), App changes the Context of the User object to include all the data retrieved from Auth0. This user data is then accessible to any part of the system which requires it.
When logged in, the UI automatically updates (using Context changes) so that the Header is now showing "Hey there {name}" rather than "Login" as before. This is also a link leading to the Profile page/component (using React Router's <Link to="/profile></Link> element).
On the Profile page there is a LogoutLink. When clicked, this logs the user out, and returns to the home page. It should also update the UI automatically to change the message in the Header back from "Hey there {name}" to "Login". This is done by Context changes again. However, this feature doesn't actually work - the user is successfully logged out, but to see the change described just above, the user needs to refresh the whole page. this.context.user is not being updated and sent back to Profile and Header. I know this is because of Redux and it's one-way data flow (i.e data can only go downwards, not up), but I need to find a way around it.
Here is the basic code I have:
LogoutLink.js
export default class LogoutLink extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {
user: null,
};
}
static propTypes = {
value: React.PropTypes.string,
}
static contextTypes = {
user: React.PropTypes.object,
} // get context so this.context is available to get initial user data (before logout)
static childContextTypes = {
user: React.PropTypes.object,
}
getChildContext() {
return {user: this.state.user}
} // these two are for updating context
componentWillMount() {
this.setState({ user: this.context.user });
} // set the internal LogoutLink state
onClick() {
this.setState({ user: null }); // set the internal user state to null following logout
}
renderLogoutLink() {
const {value} = this.props;
const {user} = this.state;
if (user != null) {
return <Link to="/profile" onClick={this.onClick}>{value}</Link>
} else {
return <span>You're already logged out!</span>
}
}
render() {
return <span>{this.renderLogoutLink()}</span>
}
}
Header.js:
export default class Header extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
this.showLock = this.showLock.bind(this); // lock is the Auth0 module responsible for Login, also passed down by context
}
static contextTypes = {
user: React.PropTypes.object,
lock: React.PropTypes.object,
}
showLock() {
const {lock} = this.context;
lock.show();
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
if (this.context.user == null && nextContext.user != null) {
return true;
} else if (this.context.user != null && nextContext.user == null) {
return true;
}
return false;
} // after the LogoutLink is clicked, this still returns false
renderLoginButton() {
const {user} = this.context;
if (user) {
const name = user.nickname;
return <Link to="/profile">Hey there {name}!</Link>
} else {
return <button onClick={this.showLock}>Login</button>
}
}
render() {
return (
<header>
{this.renderLoginButton()}
</header>
);
}
}
I am following the official React docs about Context and updating it, found here: https://facebook.github.io/react/docs/context.html
As I said I know why this is not working: data can only be sent one way. But I need to find a way to make this work, and to be honest I think I've been staring at this too long and am now out of options and my brain is a bit frazzled.
Found a solution to this issue. In my App.js code, which is setting the initial context for the User item, I have added a method onto the User to set the User to null, which then trickles down through the app. In the Logout link it calls this method. Here is the code:
In App.js:
profile.logout = () => {
this.setState({ profile: null });
}
the getChildContext() method then sets the user context from this state change:
getChildContext() {
return {
user: this.state.profile
};
}
In LogoutLink.js:
onClick() {
const {user} = this.context;
user.logout();
}
The React "context" feature is useful, but also complicated and quirky, which is why there's still a number of cautions around using it. In particular, I believe that components that return false from shouldComponentUpdate will cause lower components to not update if the context contents change.
My advice would be to use context as little as possible, and only for data that does not change. Put changing data, such as the current user, into your Redux xtate.

React: Invoke redirect to page, and pass parameter

I am building my first React app.
In my code, I redirect to another page (a component) using
browserHistory.push(pathToMyComponent)
I also have tried out the react-router Link-element. The Link element enables me to pass data to the target component without having it showing up in the URL, like this:
<Link to={`/myComponent/${someData}`}>
But now I don't want to create a link. Instead I want to perform some operations when pushing a button, and then redirect with some calculated data.
How do I do this?
Edit:
Here is my code. In loginpage, the user can perform a facebook login. What I want is to redirect the user to the lobby after login succeeded. I want to pass the userid to the lobby.
<Router history={browserHistory} onUpdate={() => window.scrollTo(0, 0)}>
<Route path="/" component={LoginPage}/>
<Route path="/lobby" component={Lobby}/>
</Router>
This is what I wrote after reading your answer. When this is executed, the log prints Uncaught TypeError: Cannot read property 'context' of null
this.context.router.push({ //browserHistory.push should also work here
pathname: '/lobby',
state: {userid: authData.uid}
});
Edit2: You mean like this? It gives syntax error.
class LoginPage extends React.Component {
contextTypes = {
router: React.PropTypes.object
};
constructor(props){
super(props);
...
...
I would create a normal button with an onClick event handler which would fire a function. In this function you can calculate the data that you want and then finally do a push to your router.
Example:
render() {
return (
...
<button onClick={this._handleButtonClick}>Click me</button>
...
);
}
_handleButtonClick = () => {
//calculate your data here
//then redirect:
this.context.router.push({ //browserHistory.push should also work here
pathname: pathToMyComponent,
state: {yourCalculatedData: data}
});
}
You'll then be able to get that data from your location's state.
Check out the docs for this here.
EDIT:
To use this.context.router add this inside your components' class:
static contextTypes = {
router: React.PropTypes.object
}
I found an easy trick here:
<Link
to={{
pathname: "/profile",
data: data // your data array of objects
}}
>
//in /profile
render() {
const { data } = useLocation().data;
return (
// render logic here
)
}

Categories

Resources