React update parent component when child changes the Context - javascript

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.

Related

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

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.

React Nav Bar Not Rerendering With New State

I'm making a nav bar in react that shows different links based on whether or not the user is logged in. (We use firebase for account stuff.)
Here's what I have:
async function isLoggedIn() {
return await UsersManagement.isLoggedIn();
}
export default class Navigation extends React.Component {
constructor(props) {
super(props);
this.state = ({ loggedIn: isLoggedIn() });
}
componentDidMount() {
this.setState({ loggedIn: isLoggedIn()})
}
render() {
return (
{this.state.loggedIn ? (
<LoggedInLinks />
) : (
<NotLoggedInLinks />
)}
}
}
I tried removing state from this component, using the function UserManagement.isLoggedIn() directly. That function works. I can see through print statements that it returns the proper userid or null when not logged in.
So what am I missing? Is there a better way to create a nav bar in react that changes based on logged in status?
Thank you!

Stop React Higher Order Component from mounting before wrapped component?

I have an auth-related HOC which wraps components. In the wrapped components, I want a user prop which is set by the HOC. However, in a child component I call the prop in componentDidMount, and I get the error:
TypeError: Cannot read property 'identity' of null
Which tells me that the user prop (which has the identity attribute) is not being set in time.
Here is the HOC:
export default function withAuth(AuthComponent) {
return class AuthWrapped extends Component {
constructor() {
super();
this.state = {
user: null
};
this.Auth = new AuthService();
}
async componentDidMount() {
try {
const profile = await this.Auth.getProfile(); //Returns a decoded JWT token containing with 'identity':'test'
console.log(profile.identity); //prints 'test' twice
this.setState({
user: profile
});
console.log(this.state.user); //prints the full profile including 'identity':'test' once and null once
}
}
render() {
const user = this.state.user;
console.log(user); //prints null once and the correct profile once
return (
<AuthComponent history={this.props.history} user={this.state.user} />
);
}
};
}
Notice that the console.logs each print twice.
Here's the wrapped component:
class Account extends Component {
componentDidMount() {
axios.get('/user/' + this.props.user.identity).then(res => { //This is the line which triggers the error.
//do something
});
}
render() {
<div><AnotherComponentWrappedBySameHOC/></div>
);
}
}
export default withAuth(Account);
When I remove the other component that is also wrapped by withAuth, the console.logs only run once. Makes sense. However, the logs which still print are the logs which print null. In other words, the wrong logs.
Basically, I think this means that the componentDidMount call in withAuth is taking too long, and completing after the componentDidMount call in Account. Try as I might, though, I have no solutions. Any ideas?
You can just wait untill you have the user and then render the child like
render() {
const user = this.state.user;
console.log(user); //prints null once and the correct profile once
return (<div>
{user && <AuthComponent history={this.props.history} user={this.state.user} />}
</div>);
}
};
This will only render the child component when user has some data

Optimizing React-Redux connected PureComponent

I'm have a very frustrating time trying to optimize my React-Redux application.
I have a header component that is reloading on every change to the redux store. My header component is a PureComponent
I have installed why-did-you-update, and it tells me:
Header.props: Value did not change. Avoidable re-render!
This is my Component:
export class Header extends PureComponent {
logout() {
// this.props.logout();
}
signup = () => {
this.props.history.push(urls.SIGNUP)
}
render() {
console.log("=============== RELOADING HEADER")
return (
<div>
<HeaderContent
logout={this.logout.bind(this)}
signup={this.signup.bind(this)}
user={this.props.user}/>
</div>
)
}
}
export function mapStateToProps(store) {
// EDITTED: I initially thought this was causing the problem
// but i get the same issue when returning a javascript object
//const u = loginUserFactory(store);
const u ={}
return {
user: u,
}
}
export function mapDispatchToProps(dispatch) {
return {
logout: function l() {
dispatch(authActions.logout())
}
}
}
export function mergeProps(propsFromState,propsFromDispatch,ownProps) {
return {
// logout: function logout() {
// propsFromDispatch.logout()
// },
...propsFromState,
...ownProps
}
}
let HeaderContainer = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{pure: true}
)(Header)
export default withRouter(HeaderContainer);
Header.propTypes = {
history: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
logout: PropTypes.func.isRequired,
}
I have verified that the console.log indicating that the render function is called prints every time the redux store is changed.
If I uncomment the function in merge props whyDidYouUpdate complains that the function caused the re-render.
The re-renders are significantly impacting the performance of my app. I considered writing my own shouldComponentUpdate() function, but read that it is a bad idea to do deep equals in that function for performance reasons.
So what do I do?
EDIT:
This is the code in Login User Factory. Initially I thought this was the problem, but when I remove that code I still get the same issue.
const loginUserFactory = state => {
const u = getLoginUser(state);
const isLoggedIn = !_.isEmpty(u);
const location = getLocation(state);
return {
get id() { return u.id },
get groupNames() { return u.group_names },
get avatarSmall() { return u.avatar_small },
get name() { return u.name },
get email() { return u.email },
// my goal was to localize these methods into one file
// to avoid repeated code and
// to make potential refactoring easier
get location() { return location},
get isHost() {return u.type === "host"},
get isBooker() {return u.type === "booker"},
get isLoggedIn() { return isLoggedIn },
}
}
export default loginUserFactory;
I guess that loginUserFactory() creates a new user object every time it gets called which is every time the store gets updated thus always passing a new user object to your component that is not equal to the previous one.
Also your Header doesn't do anything with the user except passing it further down the tree. You should instead connect the HeaderContent component and only map the properties of the user object to it, that it actually needs, e.g. the name.
In general mapStateToProps() should never have any side effects. It should only filter/sort/calculate the props for the connected component given the state and the own props. In the most trivial cases it does nothing more than returning a subset of properties from the store.
You're using bind in your click handlers. Big no-no! Each rerender will create a completely new instance of the function when you bind inside the handlers. Either bind in a constructor or turn your click handler methods into arrow functions.
handleClick = () => {
}
// or
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}
Also, do not implement any manipulations or algorithms in mapStateToProps or mapDispatchToProps. Those also trigger rerenders. Place that logic somewhere else.

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.

Categories

Resources