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();
};
Related
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} />} />
)
}
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.
I was sure to correctly create my collection, publish the data, subscribe to the right publication, and check that the data was actually appearing in the Mongo Shell. I even console.log()'d the data that was being published to ensure that the publication was working. Yet, the following line of code fails to return anything:
const maybeMeet = Meets.find({meetId: maybeId}).fetch();
This could be found below and in Line 39 of /client/imports/routes/routes.js in the linked repo.
At one point, I even tried to create a new Meteor method 'meets.query' that would just publish all the data I need, (insecurely) averting the need for a publications and subscriptions (it's now commented out on Line 59 of /client/imports/api/meets.js). That too did not work. In general, it seems as if the client can't receive any data from the server, but going from the client to the server seems to work (I could insert things into my Meets collection).
Here is the source of the problem (part of routes.js):
export const routes = (
<div id='app'>
<Header />
<Router history={browserHistory}>
<Switch>
<Route exact path="/" render={() => {
return <Landing />
}} />
<Route path="/before" render={() => {
return <Before />
}} />
<Route path="/meet" render={() => {
Meteor.subscribe('allMeets');
const maybeId = queryString.parse(location.search).m;
console.log(typeof maybeId);
console.log(maybeId);
const maybeMeet = Meets.find({meetId: maybeId}).fetch(); //***RETURNS NOTHING!***
return maybeMeet.length ? <Created meet={maybeMeet[0]} /> : <NotFound />;
}} />
<Route path="*" render={() => {
return <NotFound />
}} />
</Switch>
</Router>
</div>
);
Here is where I publish the data (part of `meets.js'):
if (Meteor.isServer) {
Meteor.publish('allMeets', function() {
return Meets.find();
});
}
Please see the repo for the entirety of the code if you need to see more: https://github.com/kpeluso/meetr
I apologize to for the messy code - it's a new project.
The problem here is that a subscribe operation is asynchronous, as it has to fetch data from the server.
The solution is to wrap the component rendered by the router in a WithTracker so that it will re-run when the data is available and start rendering to the DOM
More information on how to do that is on the docs:
https://guide.meteor.com/react.html#using-withTracker
WithTracker, check meteor docs https://guide.meteor.com/react.html#using-withTracker
For example in this code, for the APP component, withTracker keeps reactivity in sync with the subscription, and the things list fetching the collection.
export default withTracker(() => {
Meteor.subscribe('allThings')
return {
things: Things.find({}).fetch()
}
})(App);
Checkout my meteor react boilerplate.
https://github.com/pkcwong/meteor-react-starter
I use the package meteor/react-meteor-data. The HOF withTracker is the right solution.
In my Created component, I had a Tracker.autorun() with an error in it, and that led to everything else crashing. The call to Meteor.subscribe(allMeets); in my router is also, as mentioned, async and was not being handled as such.
I found the withTracker to be cumbersome, but I gleamed a lot of inspiration from its docs. As a result, the edited code in my Created component now includes this:
componentDidMount() {
this.meetTracker = Tracker.autorun(() => {
const subHandle = Meteor.subscribe('allMeets');
const loading = !subHandle.ready();
const maybeMeet = Meets.find({meetId: this.props.meetId}).fetch();
if (!maybeMeet.length && loading) {
this.setState({active: <Loading />});
} else if (!loading) {
if (maybeMeet.length) {
this.setState({active: <During meet={maybeMeet[0]} />});
} else {
createHistory().push('/PageNotFound');
window.location.reload();
}
}
});
}
... and the code in my router now includes this:
<Route path="/meet" render={() => {
const maybeId = queryString.parse(location.search).m;
return <Created meetId={maybeId} />
}} />
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! :)
I'm developing a universal react application using redux. I use react-router v3.
I want to show a progress bar "BEFORE" going to next route (next route is fetching data from API).
for example imagine I am in "Home Page" and I want go to "Submit Page". when I click on the Submit Link (react-router Link) first show a progress bar in "Home Page" and wait for Submit page data fetching and then go to "Submit Page".
My React Routes:
<Route component={App}>
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} component={SubmitPage}/>
<Route path={HomingRoutes.SearchPage} component={SearchPage}/>
<Route path={`${HomingRoutes.DealsPage}`} component={DealsPage}/>
<Route path={`${HomingRoutes.DealPage}/:id(/:title)`} component={DealPage}/>
<Route path={`${HomingRoutes.Detail}/:id(/:title)`} component={DetailPage}/>
<Route path="*" component={NoMatch}/>
</Route>
in Home Page :
<Link to "/Submit" >Submit</Link>
My Submit page Container code is :
class SubmitContainer extends React.Component {
static readyOnActions(dispatch) {
return Promise.all([
dispatch(SubmitActions.fetchSubmitInitialData()),
]);
}
componentDidMount() {
this.props.fetchSubmitInitialData();
}
}
"fetchSubmitInitialData" is an action creator that fetch data from API.
One solution would be to pass a placeholder component as props to your SubmitPage that will render only when data is fetching.
So you can use something like:
class SubmitContainer extends React.Component {
state = {
loading: true
progress: 0,
}
componentDidMount() {
// fetch some data and update the state
// consider updating the progress more often
this.props.fetchSubmitInitialData()
.then(() => {
this.setState({ loading: false, progress: 100 })
})
}
render() {
const Placeholder = this.props.placeholder
// Show the placeholder when loading
if (this.state.loading) {
return <Placeholder loading progress={this.state.progress} />
}
// Otherwise render your component with the data
return <SubmitPage data={/*..*/}>
}
}
And finally pass you could use the component HomePage as placeholder like this:
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} render={(props) => (
<SubmitContainer {...props} placeholder={HomePage} />
)}/>
Here I use the render props with React router v4. But I'm sure there is an equivalent for the version 3
Now HomePage will render during data fetching and can use the props loading and progress to show a spinner or something
You can add onEnter hook into your router and add onEnter.js inside your SubmitContainer folder and move fetchSubmitInitialData to the onEnter.js then import your store here and dispatch it. the implementation might looks like this:
Your React-Route
import { onEnterSubmitPage } from './your onEnter path/onEnter'
<Route component={App}>
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} component={SubmitPage} onEnter={onEnterSubmitPage}/>
<Route path={HomingRoutes.SearchPage} component={SearchPage}/>
<Route path={`${HomingRoutes.DealsPage}`} component={DealsPage}/>
<Route path={`${HomingRoutes.DealPage}/:id(/:title)`} component={DealPage}/>
<Route path={`${HomingRoutes.Detail}/:id(/:title)`} component={DetailPage}/>
<Route path="*" component={NoMatch}/>
</Route>
create onEnter.js file in SubmitPage container:
/**
* Import dependencies and action creators
*/
import { store } from '../../index'
import { fetchSubmitInitialData } from './actions'
/**
* Define onEnter function
*/
export function onEnterSubmitPage() {
store.dispatch(fetchSubmitInitialData())
}
then we can integrate state for progress bar into redux too.
actions.js
/** Import all dependencies here **/
import axios from 'axios'
import { FETCH_SUBMIT_INITIAL_DATA, IS_FETCHING_INITIAL_DATA } from './constants'
export function fetchSubmitInitialData() {
/** this dispatch is from middleware **/
return (dispatch) => {
/** this will set progress bar to true **/
dispatch(fetchSubmitInitialData(true))
/** Your fetching action here, this will depend on your configuration **/
axios.get(`url`, {{ headers: `bearer //your token`}})
.then( (response) => {
dispatch(fetchSubmitInitialData(false))
})
}
}
export function isFetchInitialData(status) {
return {
type: IS_FETCHING_INITIAL_DATA,
status
}
}
so there is no need to fetch the data inside SubmitPage container.
One solution would be to pass a placeholder component as props to your SubmitPage that will render only when data is fetching.
So you can use something like:
class SubmitContainer extends React.Component {
render() {
/** this come from your reducer **/
const { isFetching, submitInitialData } = this.props
// Show the placeholder when loading
if (isFetching) {
return <Loader />
}
// Otherwise render your component
return <SubmitPage data={/*..*/}>
}
}
// Map state to props
const mapStatetoProps = ({ app }) => {
isFetching: //,
submitInitialData: //
}
export default connect(mapStatetoProps, null)(SubmitContainer)