React - correct way to wait for page load? - javascript

In React JS, what is the correct way to wait for a page to load before firing any code?
Scenario:
A login service authenticates a user then redirects them (with a cookie), to a React App.
The React App then straight away searches for a cookie and validates it against an endpoint.
But the problem I am getting is when user is authenticated at login service, then forwarded to React App, the App is loading so quick before cookie is even loaded.
What is the right way to fire the code? I tried wrapping in a componentDidMount(), didnt work!

I would suggest you to use a state in the Main component of your application (usually App.jsx) which will control loading. When you start the app the state will be true and only after checking all you need to check it will beacome false. If state is loading you will show a spinner or whatever you want and when it is not loading, the website:
class App extends Component {
constructor(props) {
super(props);
this.state = { loading: true }
}
componentDidMount () {
checkToken()
.then(() => this.setState({ loading: false });
}
if (loading) {
return <Spinner /> // or whatever you want to show if the app is loading
}
return (
...rest of you app
)
}
If any doubt just let me know.

you can use Suspense and lazy :)
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

Related

Maximum update depth exceeded from Navigate component react-router-dom v6

I'm using react-router-dom v6 to control the route of my React Js app.
Here are the specifications:
I'm creating the AuthenticationRoute and PrivateRoute components.
The AuthenticationRoute component is used to wrap pages that the user doesn't need to authenticate for examples SignIn, SignUp, ForgotPassword, and ResetPassword pages.
The PrivateRoute component is used to wrap private pages (authentication is needed) for example Home page. Inside the PrivateRoute, there are some layouts. One of them is called the Dashboard layout to wrap the Drawer (Sidebar/Navigation) component and the Home page.
If the user has not logged in via the SignIn page, the app would return the SignIn page.
If the user has logged in, the app would return the Home page.
Here are the current conditions:
Note: The check sign (✅) represents the conditions I want while the cross sign (❌) represents the error or unwanted conditions.
All of the specifications above are met. ✅
The first time user runs the app, the SignIn page is returned because the user has not logged in. ✅
If the user has not logged in and typed "/" route to the address bar (to access the Home page) via the SignIn page, the app will not redirect the user to the Home page instead of returning the SignIn page. ✅
If the user successfully logged in via the SignIn page, the app would return the Home page (with "/" route). ✅
If the user has logged in and typed "/sign-in" route to the address bar (to access the SignIn page) via the Home page, the app return error: ❌
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
at Navigate (https://5xxrw.csb.app/node_modules/react-router/index.js:247:12)
at AuthenticationRoute (https://5xxrw.csb.app/src/components/Routes/AuthenticationRoute.jsx:21:26)
at Routes (https://5xxrw.csb.app/node_modules/react-router/index.js:306:18)
at App
at Router (https://5xxrw.csb.app/node_modules/react-router/index.js:266:18)
at BrowserRouter (https://5xxrw.csb.app/node_modules/react-router-dom/index.js:284:18)
The app should navigate back the user to the Home page ("/" route) instead of returning the error.
Here is the code for the AuthenticationRoute:
function AuthenticationRoute(props) {
const { children } = props;
const userProfile = readUserProfileFromLocalStorage();
return userProfile ? <Navigate replace to="/sign-in" /> : children;
}
and here is the code for the PrivateRoute:
function PrivateRoute(props) {
const { children } = props;
const userProfile = readUserProfileFromLocalStorage();
return userProfile ? (
<Dashboard>{children}</Dashboard>
) : (
<Navigate replace to="/sign-in" />
);
}
Here is the playground: https://codesandbox.io/s/stackoverflow-auth-private-routes-5xxrw
I did a similar thing using react-router-dom v5 but didn't return the error. Everything was fine.
So, what's the solution for this case?
There is an issue on your AuthenticationRoute component. You are redirecting the user to /sign-in when userProfile is defined, which causes an infinite loop since its the same page. It should be navigating to /
function AuthenticationRoute(props) {
const { children } = props;
const userProfile = readUserProfileFromLocalStorage();
return userProfile ? <Navigate replace to="/" /> : children;
}

How do I integrate the cognito hosted UI into a react app?

I am creating a react app - using create-react-app and amplify - and I am trying to set up authentication. I don't seem to be able to handle the federated logins using the hosted UI.
There are some pages which require no authentication to reach and then some which require a user to be logged in. I would like to use the hosted UI since that's prebuilt. I have been following the getting started docs here: https://aws-amplify.github.io/docs/js/authentication
For background I have the following components:
- Amplify - an amplify client which wraps calls in methods like doSignIn doSignOut etc. The idea is to keep all this code in one place. This is a plain javascript class
- Session - provides an authentication context as a React context. This context is set using the amplify client. It has HOC's for using the context
- Pages - some wrapped in the session HOC withAuthentication which only renders the page if the user has logged in
This structure is actually taken from a Firebase tutorial: https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/
Maybe this is just not feasible with Amplify? Though the seem similar enough to me that it should work. The basic idea is that the Session provides a single auth context which can be subscribed to by using the withAuthentication HOC. That way any component that requires a user will be rendered as soon as a user has logged in.
Originally I wrapped the entire App component in the withAuthenticator HOC provided by amplify as described in the docs. However this means that no pages are accessible without being authenticated - home page needs to be accessible without an account.
Next I tried calling to the hosted UI with a sign in button and then handling the response. The problem is when the hosted UI has logged a user in then it redirects back to the app causing it to reload - which is not ideal for a single page app.
Then I tried checking if the user is authenticated every time the app starts - to deal with the redirect - but this becomes messy as I need to move a lot of the amplify client code to the Session context so that it can initialise correctly. The only way I can see to get this is using the Hub module: https://aws-amplify.github.io/docs/js/hub#listening-authentication-events The downside is that after logging in, the app refreshes and there's still a moment when you are logged out which makes the user experience weird.
I would have thought that there would be a way to not cause an application refresh. Maybe that's just not possible with the hosted UI. The confusing thing to me is that the documentation doesn't mention it anywhere. In actual fact there is documentation around handling the callback from the hosted UI which as far as I can see never happens because the entire page refreshes and so the callback can never run.
I've tried to trim this down to just what's needed. I can provide more on request.
Amplify:
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from '../../aws-exports';
import { AuthUserContext } from '../Session';
class AmplifyClient {
constructor() {
Amplify.configure(awsconfig);
this.authUserChangeListeners = [];
}
authUserChangeHandler(listener) {
this.authUserChangeListeners.push(listener);
}
doSignIn() {
Auth.federatedSignIn()
.then(user => {
this.authUserChangeListeners.forEach(listener => listener(user))
})
}
doSignOut() {
Auth.signOut()
.then(() => {
this.authUserChangeListeners.forEach(listener => listener(null))
});
}
}
const withAmplify = Component => props => (
<AmplifyContext.Consumer>
{amplifyClient => <Component {...props} amplifyClient={amplifyClient} />}
</AmplifyContext.Consumer>
);
Session:
const provideAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: null,
};
}
componentDidMount() {
this.props.amplifyClient.authUserChangeHandler((user) => {
this.setState({authUser: user});
});
}
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withAmplify(WithAuthentication);
};
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
render() {
return (
<AuthUserContext.Consumer>
{user =>
!!user ? <Component {...this.props} /> : <h2>You must log in</h2>
}
</AuthUserContext.Consumer>
);
}
}
return withAmplify(WithAuthentication);
};
The auth context is provided once at the top level:
export default provideAuthentication(App);
Then pages that require authentication can consume it:
export default withAuthentication(MyPage);
What I would like to happen is that after the user signs in then I can set the AuthUserContext which in turn updates all the listeners. But due to the redirect causing the whole app to refresh the promise from Auth.federatedSignIn() can't resolve. This causes the user to be displayed with You must log in even though they just did.
Is there a way to block this redirect whilst still using the hosted UI? Maybe launch it in another tab or in a popup which doesn't close my app? Or am I going about this the wrong way? It just doesn't feel very 'Reacty' to cause full page refreshes.
Any help will be greatly appreciated. I can provide more details on request.
Instead of chaining onto the Auth's promise, you can use Amplify's build-in messaging system to listen to events. Here is how I do it in a custom hook and how I handle what gets rendered in Redux.
import { Auth, Hub } from 'aws-amplify';
import { useEffect } from 'react';
function useAuth({ setUser, clearUser, fetchQuestions, stopLoading }) {
useEffect(() => {
Hub.listen('auth', ({ payload: { event, data } }) => {
if (event === 'signIn') {
setUser(data);
fetchQuestions();
stopLoading();
}
if (event === 'signOut') {
clearUser();
stopLoading();
}
});
checkUser({ fetchQuestions, setUser, stopLoading });
}, [clearUser, fetchQuestions, setUser, stopLoading]);
}
async function checkUser({ fetchQuestions, setUser, stopLoading }) {
try {
const user = await Auth.currentAuthenticatedUser();
setUser(user);
fetchQuestions();
} catch (error) {
console.log(error);
} finally {
stopLoading();
}
}

React onClick event handler not working using Next.js

I'm making a Reddit clone and I'm using Next.js so its server-side rendered. I started without using Next.js and when I learned about it, I immediately switched to it.
I've created a custom _app.js so the header and sidebar exist on every page, and for it to act as the topmost component to hold application state. The later isn't quite working out.
Here's .project/src/pages/_app.js:
import App, { Container } from 'next/app';
// Components
import Header from '../components/Header/Header';
const MainLayout = props => (
<React.Fragment>
<Header
isSidebarOpen={props.isSidebarOpen}
sidebarToggle={props.sidebarToggle}
/>
{props.children}
</React.Fragment>
);
export default class MyApp extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
state = {
isSidebarOpen: true
};
sidebarToggle = () => {
this.setState({ isSidebarOpen: !this.state.isSidebarOpen });
};
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<MainLayout
isSidebarOpen={this.state.isSidebarOpen}
sidebarToggle={this.sidebarToggle}
>
<Component {...pageProps} />
</MainLayout>
</Container>
);
}
}
The issue I'm having is, isSidebarOpen and sidebarToggle are being passed to exactly where I need them – they show up in the console – but the onClick handler doesn't activate and if I change isSidebarOpen, it doesn't take effect until I restart the server. I used this approach before using Next.js and it worked as expected.
How can I achieve what I'm trying to do? I've read the docs, searched Google and Stack Overflow. I even checked issues on their repo without luck. I suspect it to be something to do with a custom _app.js, as the props are passed down correctly.
One possibility might be to define your sidebarToggle function within the component you are using.(Maybe because calling it inside Component the code might be running in _app.js instead of your component this is a big maybe! but worth a try)
Here _app.js is a wrapper around your application and I don't think it is suited to be used at the topmost component to hold state. Better to make a simple react root Component do that!

Updating mounted or mounting component warning

I have the following warning in my tests:
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.
Please check the code for the ProtectedRoute component.
So I checked my ProtectedRoute component. This component is built upon the Route component from react-router and checks if user is logged in before rendering route. Here is the component code:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router-dom';
import { Authentication } from 'local-authentication-service';
const renderMergedProps = (component, ...otherProps) => {
const finalProps = Object.assign({}, ...otherProps);
return (
React.createElement(component, finalProps)
);
};
class ProtectedRoute extends Component {
constructor() {
super();
this.state = { loading: true };
}
async componentDidMount() {
try {
const user = await Authentication.getUser();
this.setState({ user, loading: false });
} catch (e) {
this.setState({ loading: false });
}
}
render() {
const { loading, user } = this.state;
const { component, ...otherProps } = this.props;
if (loading) return <div>Loading...</div>;
return user ? <Route render={routeProps => renderMergedProps(component, routeProps, { user }, otherProps)} /> : <Redirect to="/auth/login" />;
}
}
ProtectedRoute.propTypes = {
component: PropTypes.oneOfType([
PropTypes.element,
PropTypes.func,
]).isRequired,
};
export default ProtectedRoute;
As far as I see, the only state change is done in componentDidMount() so it should not be throwing this error.
Where should I check to solve this warning ?
Thank you !
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.?
Before componentDidMount life cycle,render is called.
In render,
return user ?
<Route render={routeProps => renderMergedProps(component, routeProps, { user }, otherProps)} />
: <Redirect to="/auth/login" />;
initially user is evaluated as a false resulted in Redirect route is getting called. So, current ProtectedRoute component is getting unmounted.
But at the same time,you are settting setState inside componentDidMount which not going to execute as your component getting unmounted.
Because of this, you are getting above warning.
By the time your async auth call completes, you've already redirected your user to /auth/login and unmounted ProtectedRoute. When the auth call finally completes, it tries to update ProtectedRoute's state. That's when React gives you this nice message saying you are trying to update a component which is no longer there. Read for details :)
It's a bit tricky but here's what's happening:
On the initial mounting of the component, componentDidMount is invoked and fires off your Authentication.getUser call. Since it's async, it does run immediately and instead gets added to the the javascript event loop. At this point, the code continues on. Since the auth call hasn't actually completed, you have no user of of yet. In your render method you specify to redirect the viewer to /auth/login if there is no user. Since this is the case, you are redirected and your component ends up unmounting.
Do you see where I am going?
The event loop finally decides that no other synchronous code needs to be ran and gives that sitting authentication call a chance to fire. It does its thing and tries to update the state based on where there is/is not a user. But component is no longer there. :(
Then you get this nice message from the great react gods. But you're a coder, and you learn from this and this starts to happen less and less.
How can you avoid this?
You need your component state to keep track of whether or not your auth call has completed. Only after the call is completed should you try to check if there is/isn't a user. Only then, if a user is not present should you redirect.
Maybe an isFetchingUser state property would suffice? Then, once isFetchingUser is false and user does not exist you redirect, otherwise if isFetchingUser is true, you can show either a loading message, a spinner or just a white screen since your async call should hopefully be very fast.

React JS component renders multiple times in Meteor

I using Meteor 1.3 for this application together with react js and Tracker React.
I have a page to view all available users in the application. This page require user to login to view the data. If user not logged in, it will shows the login form and once logged-in the component will render the user's data.
Main component for the logic.
export default class MainLayout extends TrackerReact(React.Component) {
isLogin() {
return Meteor.userId() ? true : false
}
render() {
if(!this.isLogin()){
return (<Login />)
}else{
return (
<div className="container">
<AllUserdata />
</div>
)
}
}
}
And in the AllUserdata component:
export default class Users extends TrackerReact(React.Component) {
constructor() {
super();
this.state ={
subscription: {
Allusers : Meteor.subscribe("AllUsers")
}
}
}
componentWillUnmount(){
this.state.subscription.Allusers.stop();
}
allusers() {
return Meteor.users.find().fetch();
}
render() {
console.log('User objects ' + this.allusers());
return (
<div className="row">
{
this.allusers().map( (user, index)=> {
return <UserSinlge key={user._id} user={user} index={index + 1}/>
})
}
</div>
)
}
};
The problem is when logged in, it only shows the current user's data. All other user objects are not rendered. If I check on the console, console.log('User objects ' + this.allusers()); show objects being rendered 3 times: the first render only shows the current user's data, the second one renders data for all users (the desired result), and the third one again renders only the current user's data.
If I refresh the page, the user data will be rendered properly.
Any idea why?
React calls the render() method of components many times when it's running. If you're experiencing unexpected calls, it's usually the case that something is triggering changes to your component and initiating a re-render. It seems like something might be overwriting the call to Meteor.users.find().fetch(), which is probably happening because you're calling that function on each render. Try inspecting the value outside of the render method or, better yet, rely on tests to ensure that your component is doing what it should be :)
From https://facebook.github.io/react/docs/component-specs.html#render
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it's invoked, and it does not read from or write to the DOM or otherwise interact with the browser (e.g., by using setTimeout). If you need to interact with the browser, perform your work in componentDidMount() or the other lifecycle methods instead. Keeping render() pure makes server rendering more practical and makes components easier to think about.
See also:
https://facebook.github.io/react/docs/advanced-performance.html
https://facebook.github.io/react/docs/top-level-api.html#reactdom
https://ifelse.io/2016/04/04/testing-react-components-with-enzyme-and-mocha/

Categories

Resources