How to handle async request in React-router wrapper - javascript

I want to check if user is authenticated in my React application. Using this guide.
I wrote a wrapper over my <Route /> class that check, if user is authenticated, then we render component, if not, we just redirect him to sign-in page.
const IsAuthenticatedRoute = function ({ component: Component, ...rest }) {
return (
<Route {...rest} render={async (props) => {
return (
await store.isAuthenticatedAsync() === true // here is the point of troubles
? <Component {...props} />
: <Redirect to={{
pathname: '/sign-in',
state: { from: props.location }
}} />
)
}} />)
}
And I use it in my router like this:
ReactDOM.render(
<Provider store={appStore}>
<Router>
<div>
<Switch>
<Route exact path='/' component={App} />
<IsAuthenticatedRoute path='/protected-route' component={Profile} />
</Switch>
</div>
</Router>
</Provider>
,
document.getElementById('root')
)
I want to execute my async request to the server to check if user is authenticated. I've tried to add async keyword to my functions over await call, but it produces an error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.. I almost tried to use promises, but it isn't help too. When I use Promise inside my function and return <Route /> in .then() operator, React says me: IsAuthenticatedRoute(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
So I expect to handle my async function, and then after I get response from server, give access to my user to visit this page. Is it possible only with sending synchronous request to my server or there're another ways to keep my code async and pass user to the protected page?

An async function cannot be rendered as a component, because you'd be rendering a Promise, not a pure function. Pure functions can be rendered, if they return an instance of a component. Promises must be resolved before they can be rendered.
The solution is to start the asynchronous call when the component is mounted and make the component stateful, so that it can mutate when the call is resolved. You will need to render something while waiting for a response. You can render null, but a loading spinner would be more appropriate. This way we have something to render at all times and won't run into errors trying to render a component that isn't defined yet.
Here's my quick hack at what the component could look like:
class RouteRender extends React.Component {
constructor(props) {
super(props)
this.state = { authorized: null }
}
componentDidMount() {
// setState is called once the asynchronous call is resolved.
store.isAuthenticatedAsync().then(
authorized => this.setState({ authorized})
)
}
render() {
if(this.state.authorized === true) {
const { component: Component, componentProps } = this.props
return <Component {...componentProps} />
} else if(this.state.authorized === false) {
return (<Redirect to={{
pathname: '/sign-in',
state: { from: props.location }
}} />)
}
return <LoadingSpinner />
}
}
const IsAuthenticatedRoute = function ({ component: Component, ...rest }) {
return (
// render is now a function rather than a Promise.
<Route {...rest} render={props => <RouterRender componentProps={props} component={Component} />} />
)
}

Related

Fetch data before resolve route

I´m using for routing "react-router" lib. Before render page component, I need fetch data. I want show loader before every routing, because all routes need data from server. All my components is driven by controller, so my solution for this is create this controller in constructor of all components, and on create controller fetch data.
It works, but I´m using typescript and I want access to data without (!) check for data. Better solution for that use wrapper component which wait for data and render currently page. For first routing it works, but componentDidMounnt "below in code" is called only once, so second rounting doesnt work.
<Router>
<Switch>
<MyRoute path="/login" component={LoginPage} exact={true} />
<MyRoute path="/reg" component={RegistrationPage} exact={true} />
</Switch>
</Router>
/*MyRoute*/
async componentDidMount() {
try {
await this.props.routeController.getController(this.props.path).then((controller: PageController) => {
this.setState({
controller: controller
})
this.props.routeController.loading = false;
})
} catch(err) {
// error handling
}
}
render() {
if (!this.props.routeController.loading) {
const { controller } = this.state;
return (
<this.props.component controller={controller} />
)
}
return <div>LOADING</div>;
}
So I need fetch data before routing. After that I need render page component with data in props. Is it possible or Is it good solution for this problem? If not, how can I solve problem with asynchronous routing. Thank you :-)
Make state isLoading : false,
Then in componentWiilMount() / DidMount() set isLoading state true.
After on fetch sucess reset isLoading to false;
componenetWillMount/didMount(){
this.setState({
isLoading: true
})
fetchData().then(res => this.setState(isLoading: false))
.catch(err => this.setState({isLoading: false}));
render(){
return(
{this.state.isLoading ? <Loader /> : <Component View /> }
)
}
You also could use react-router-loading to fetch data before switching the page.
You only need to mark routes with the loading prop and tell the router when to switch the pages using the context in components:
import { Routes, Route } from "react-router-loading";
<Routes> // or <Switch> for React Router 5
<Route path="/page1" element={<Page1 />} loading />
<Route path="/page2" element={<Page2 />} loading />
...
</Routes>
// Page1.jsx
import { useLoadingContext } from "react-router-loading";
const loadingContext = useLoadingContext();
const loading = async () => {
// loading some data
// call method to indicate that loading is done and we are ready to switch
loadingContext.done();
};

How could you pause animation in react-transition-group while loading initial data in components?

I have following App component:
<Route render={( { location } ) => (
<TransitionGroup component="div" className="content">
<CSSTransition key={location.key} classNames="slide" timeout={{
enter: 1000,
exit: 300
}} appear>
<Switch location={location}>
<Route exact path='/' component={Intro}/>
<Route path="/history" component={History}/>
<Route path="/rules" component={Rules}/>
<Route path="/faq" component={Faq}/>
<Route path="/feedback" component={Feedback}/>
<Route path="/partners" component={Partners}/>
</Switch>
</CSSTransition>
</TransitionGroup>
)}/>
And it works fine, but every animation executes immediately. For example, if I go from /rules to /history, I got full animation on both components, but history component require data from the server, so animation applied on empty container.
How could I pause animation in react-transition-group components? I have Redux, so I could change loading variable anywhere in my app. Also, I don't want to preload all data in the store on app start.
I would make your component return null when it's loading and make the loading state determine the CSSTransition key like <CSSTransition key={location.key+loading?'-loading':''}
see example here: https://stackblitz.com/edit/react-anim-route-once
note that to make this work without duplication I had to make the component copy the loading prop and persist it in state, so that one of the copies of the component never displays (which would create a duplication of the component as seen here: https://stackblitz.com/edit/react-anim-route-twice)
<Route render={({ location }) => (
<TransitionGroup component="div" className="content">
<CSSTransition key={location.key+(this.state.loading?'-loading':'-loaded')} classNames="crossFade" timeout={{
enter: 3000,
exit: 3000
}} appear>
<Switch location={location} >
<Route exact path='/' component={Hello} />
<Route exact path='/history' render={() =>
<Delayed setLoading={this.handleSetLoading} loading={this.state.loading} />} />
</Switch>
</CSSTransition>
</TransitionGroup>
)} />
and in the component something like this:
export default class History extends React.Component {
state={loading:this.props.loading}
componentDidMount() {
setTimeout(() => {
this.props.setLoading(false);
}, 2000);
}
render() {
return !this.state.loading ? <div><h1>History! <Link to="/">Home</Link></h1></div> : null;
}
}
So my cases have been a bit different but they might help you think of a solution.
You can delay the initial display easily by adding an if (this.state.isloaded == true) block around your whole router. Start loading when your component mounts, and when the async call completes, setState({isloaded: true}).
You can make your own <Link> component, which launches a request, and only once it’s complete changes the page location. You can do whatever special loading spinners you like in the meantime.
Basically, keep the routing and transition components to one side. I find them to be brittle and painful with cases like this. Let me know if you want any more details or snippets.
I've done peloading through redux and redux-saga. Maybe it's one and only way to achieve following with react-router and react-transition-group, because transition toggle animation anytime when render method is run, even if it return null.
I've implemented following actions:
const LoadingActions = {
START_LOADING: 'START_LOADING',
STOP_LOADING: 'STOP_LOADING',
REDIRECT: 'REDIRECT',
startLoading: () => ({
type: LoadingActions.START_LOADING
}),
stopLoading: () => ({
type: LoadingActions.STOP_LOADING
}),
redirect: ( url, token ) => ({
type: LoadingActions.REDIRECT,
payload: {
url,
token
}
})
};
export default LoadingActions;
In the reducers I've implemented simple loader reducer, that will toggle on and off loading variable:
import { LoadingActions } from "../actions";
const INITIAL_STATE = {
loading: false
};
export default function ( state = INITIAL_STATE, { type } ) {
switch ( type ) {
case LoadingActions.START_LOADING:
return { loading: true };
case LoadingActions.STOP_LOADING:
return { loading: false };
default:
return state;
}
}
The most irritating thing is reducer chain - this.props.loader.loading. Too complex for such simple thing.
import { combineReducers } from "redux";
...
import LoadingReducer from './LoadingReducer';
export default combineReducers( {
...
loader: LoadingReducer
} );
This most work goes in saga:
function* redirect ( action ) {
yield put( LoadingActions.startLoading() );
const { url } = action.payload;
switch ( url ) {
case MENU_URL.EXCHANGE:
yield call( getExchangeData, action );
break;
... other urls...
}
yield put( LoadingActions.stopLoading() );
BrowserHistory.push( url );
}
... loaders ...
function* watchRedirect () {
yield takeLatest( LoadingActions.REDIRECT, redirect );
}
const sagas = all( [
...
fork( watchRedirect )
] );
export default sagas;
I put listener on redirect action, so it will call redirect generator. It will start loading and call data preloading yield call will await for preload to finish and after it will stop loading and redirect. Though it won't wait for positive result, so preloaders should handle errors themselves.
I hoped that I could avoid redux complexity with built-in feature of router or transition library, but it has no such tools to stop transition. So it is one of the best way to achieve transition with preloded data.

Why is my React component mounting after each action?

I have a React component called Home which is calling an action to fetch some groups when the component mounts.
I am calling an action as follows:
componentDidMount() {
const { fetchRecentGroups } = this.props;
fetchRecentGroups();
}
My reducer is picking up each action perfectly fine and is returning a state as follows:
switch(action.type) {
case REQUEST_GROUPS:
return {
...state,
loadState: FETCHING
};
case REQUEST_GROUPS_SUCCESS:
return {
...state,
loadState: SUCCESS,
groups: action.data.groups,
totalResults: action.data.totalResults
};
default:
return state;
}
I am also using the connect HOC on this component as follows:
const mapStateToProps = (state) => {
return {}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchRecentGroups: () => {
dispatch(actions.fetchRecentGroups())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
The Home component is placed inside a Route like this:
<Route
exact={true}
path="/"
component={Home}
/>
My problem is that each time the reducer returns a state, the componentDidMount is called again and again in a loop. I would expect the mount to happen only once at the first load.
If I put componentDidUpdate and componentWillReceiveProps functions in my component, they are never called (only componentDidMount) so I am not able to compare props.
Does anyone know why this may be happening?
EDIT:
I have found my problem to be caused by this piece of code in my route:
const RouteBlock = () => {
if(errorSettings) {
return <Error {...errorSettings} />
}
return (
<div className={styles.RouteBlock}>
<Route exact={true} path="/" component={Home} />
<Route path="/search" render={() => <div>SEARCH</div>} />
</div>
);
};
return <Router><RouteBlock /></Router>
I changed it to:
return <Router>{RouteBlock()}</Router>
Every time you render your component, you are immediately calling function that is setting new state and you are triggering re-rendering of your component. Maybe you should use shouldComponentUpdate life cycle method that will check is your old state the same as new one.
Check out official docs: https://reactjs.org/docs/react-component.html#shouldcomponentupdate

Trouble passing props to componentDidMount in child component (React)

I'm having issues passing a prop to a componentDidMount() call in a child component on my React application.
In my App.js I am passing props via Router like below:
App.js
class App extends Component {
state = {
city: ""
}
componentDidMount() {
this.setState({city: this.props.city});
}
render() {
return (
<div>
<Route path="/" exact render = {() => <Projections city={this.state.city} />} />
<Route path="/:id" component={FullPage} />
</div>
);
}
}
In my Projections.js I have the following:
Projections.js
constructor(props) {
super(props);
this.state = {
location: this.props.city
}
}
componentDidMount () {
console.log(this.state.location);
console.log(this.props.city);
}
console.log(this.state);' returns an empty string.console.log(this.props.city);` returns an empty string as well.
But I need to access the value of the city prop within componentDidMount(). console.log(this.props.city); within render() returns the prop, but not in componentDidMount()
Why is this and how do I return props within componentDidMount()?
In the constructor you should reference props, not this.props:
location: props.city
<Route path="/" exact render = {() => <Projections city={this.state.city} {...this.props} />} />
Try passing rest of props in route
this is because you assigned props in constructor that time it may or may not receive actual value. And it gets called only once in a component lifecycle.
You can use componentWillReceiveProps to get props whenever it receive and update state accordingly.
Inside Projections.js
UNSAFE_componentWillReceiveProps(nextProps){
if(nextProps.city){
this.setState({location:nextProps.city})
}
}
Here is working codesand

React Router v4: Sending requests when navigation changes

I'm coding an authentication with react-router v4 and I'm using the PrivateRoute with render props, like the documentation: Redirects (Auth)
What I'm trying to do is: Whenever the user navigates to a route, I want to dispatch an action to make a request to the backend to verify if he's logged in.
Like this:
// App.js
class App extends Component {
checkAuth = () => {
const { dispatch, } = this.props;
// callback to dispatch
}
render() {
const props = this.props;
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute
exact
path="/dashboard"
component={Dashboard}
checkIsLoggedIn={this.checkAuth}
/>
{/* ... other private routes here */}
</Switch>
</div>
</Router>
);
}
In PrivateRoute.js I'm listening the route to check if it changes, but when a route changes, this function is called too many times, and that's a problem to dispatch an action to make a request.
// PrivateRoute.js
const PrivateRoute = ({ component: Component, auth, checkIsLoggedIn, ...rest }) => (
<Route
{...rest}
render={props => {
props.history.listen((location, action) => {
if (checkIsLoggedIn) {
// Here I check if the route changed, but it render too many times to make a request
checkIsLoggedIn(); // here is the callback props
}
});
if (auth.login.isLoggedIn) {
return <Component {...props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
}
}
/>
);
I need a help to figure it out a good way to call the backend whenever the route changes.
Creating a Higher Order Component (HOC) is a very clean way to do this. This way, you won't need to create a separate PrivateRoute component, and it would take only one line of change to convert any Component from public to protected, or vice versa.
Something like this should work:
import React from 'react';
import { Redirect } from "react-router-dom";
export function withAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLoggedIn: false,
isLoading: true
};
}
componentDidMount() {
// Check for authentication when the component is mounted
this.checkAuthentication();
}
checkAuthentication() {
// Put some logic here to check authentication
// You can make a server call if you wish,
// but it will be faster if you read the logged-in state
// from cookies or something.
// Making a server call before every protected component,
// will be very expensive, and will be a poor user experience.
this.setState({
isUserLoggedIn: true, // Set to true or false depending upon the result of your auth check logic
isLoading: false
});
}
render() {
// Optionally, you can add logic here to show a common loading animation,
// or anything really, while the component checks for auth status.
// You can also return null, if you don't want any special handling here.
if (this.state.isLoading) return (<LoadingAnimation />);
// This part will load your component if user is logged in,
// else it will redirect to the login route
if (this.state.isUserLoggedIn) {
return <WrappedComponent authData={this.state} {...this.props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />;
}
}
}
}
Once you have that component in place, all you need to do is use the HOC in any component that you wish to have protected. For example, in your case, the export line in your Dashboard file would be something like this:
/* Dashboard.js */
class Dashboard extends React.Component { ... }
export default withAuth(Dashboard);
and in your App, you can use a simple Route component:
<Route exact path='/dashboard' component={Dashboard} />
Your App does not need to care about which routes are protected, and which ones aren't. In fact, only the actual components need to know that they are protected.
Hope this helps. Cheers! :)

Categories