I'm using React Router with Redux with server rendering.
I have component that will fetching data from remote api, so I need wait for loading at server side, when making server side rendering
I've used this solution
https://github.com/bananaoomarang/isomorphic-redux
server.jsx
... after matching route, creating storage etc. wait for all needed promises completed ...
fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
.then(renderView)
shared/lib/fetchComponentData.jsx
export default function fetchComponentData(dispatch, components, params) {
const needs = components.reduce( (prev, current) => {
return current ? (current.needs || []).concat(prev) : prev;
}, []);
const promises = needs.map(need => dispatch(need(params)));
return Promise.all(promises);
}
and component that I wrote myself was
export default class ListComponent extends React.Component {
static needs = [
loadSomeData
]
render() {
return (
<div>
</div>
);
}
}
My routes.js was
export default (store) => {
return (<div>
<Route path='/'>
<IndexRoute component={AnotherComponent}/>
<Route path="component" component={Component}/>
</Route>
</div>);
};
So, the flow works good.
1) I go to /component by typing this in browser
2) Server.js calls fetchComponentData
3) fetchComponentData determines that component need load data before rendering, dispatch action with promise
4) wait promise for completed
5) render component and give this as html
But there is a problem. When I go to component from another by calling browserHistory.push('/component') data was not loading at all
I try to decide this by modifiying routes.js in such way
export default (store) => {
const loadData = (nextState, replace, cb) => {
store.dispatch(loadSomeData());
cb();
};
return (<div>
<Route path='/'>
<IndexRoute component={AnotherComponent}/>
<Route path="component" component={Component} onEnter={loadData}/>
</Route>
</div>);
};
But I've got another problem server give me rendered html and client call loadData again
So my question is how to avoid calling onEnter on client for the first time rendering on client?
Thank you for answers!
I propose problem here is affected by invoking routes.js on both clienside and serverside. To say it shorter:
1) there are should be 2 functions: bootstrapServer and bootstrapClient
2) bootstrapServer works well if I realized you correctly: you matches the url with a page what in your request url, dispatch data for its and render;
3) bootstrapClient should contains routes.js with onEnter - it must be invoked only on client-side
So the striking difference is that routes.js must be implemented only on clientside.
Related
Im having a really weird issue with My react authentication context (authProvider) and properly rendering my application page. Currently, my root app looks like this:
const App = () => {
const authCtx = useContext(AuthState);
return(
<AuthStateProvider>
<BrowserRouter>
<Switch>
{!authCtx.isLoggedIn && <Route path="/admin" component={SignInUp} />}
{authCtx.isLoggedIn && <Route path="/admin" component={Admin} />}
<Redirect from="/" to="/admin/myDashboard" />
</Switch>
</BrowserRouter>
</AuthStateProvider>
)};
Then in a seperate file that I use to manage the authentication context, I attempt to pull a JWT from local storage and verify it. If that's successful then the context is updated using state variables (including the "isLoggedIn" variable you see above").
const AuthState = React.createContext({
userName: "",
isLoggedIn: false,
authToken: null,
});
const AuthStateProvider = (props) => {
let token = null;
if(localStorage.getItem("token")) return token = localStorage.getItem("token");
const [ user, setUser ] = useState({
userName: "Anonymous",
isLoggedIn: false,
authToken: token,
});
const autoLogin = useCallback( async () => {
try {
const response = await axios({
method:'post',
url: 'http://127.0.0.1:3001/authEn'
headers: {
"Content-Type": "application/json",
"Authentication": user.authToken
}
});
if(response.status === 200){
//code to update context using setUser state handler
} else {
throw new Error("request failed");
}
} catch (e) {
console.log(e.message);
}
});
useEffect( async () => {
await autoLogin();
}, [autoLogin]);
return (
<AuthState.Provider
value={{
userName: user.userName,
isLoggedIn: user.isLoggedIn,
authToken: user.authToken
}}
>
{props.children}
</AuthState.Provider>
);
}
(I've excluded the code for my setUser handler to try and keep this short.)
So the problem is that as of right now, I'm just trying to see that the application can
A: check for stored token on initial page load / reload
B: Navigate you to either logIn or Admin page accordingly.
The app has no problem taking you to logIn page if there is a faulty/no JWT in localstorage. But when I try testing if the application can properly navigate to the admin page when there is a valid token in local storage (i have a seperate helper function to save a valid token), the page loads, but with NONE of the actual admin dashboard. Instead, all there is on the page is the token itself displayed at the top of the window as if it were just an html page with a single div containing the token as a string. I have no Idea why this happens. When I try rendering the admin component (removing the "isLoggedIn" logic and the authStateProvider) everything is fine. But each time I try adding authentication this way things start getting weird. Am I missing something obvious (usually the case)? Is this just a really stupid approach (also usually the case)? Or is this a low-level react issue (I'm not super familiar with all the intricacies of how react works under the hood.)
I think this is a bad practice to make conditions in the Switch.
Instead, you can create a separate component like ProtectedRoute or wrap your pages with a Higher-Order Component (HOC)
First way with ProtectedRoute
Pass isProtected in props if your wrapped route requires authentification
// ProtectedRoute.js
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedRoute = ({
isProtected,
children,
}) => {
const { isLoggedIn } = useContext(AuthState);
if (isProtected && !isLoggedIn) {
return <Redirect to='/login' />;
}
return children
};
export default ProtectedRoute;
Then in your switch:
<Switch>
{ /* Other routes */ }
<ProtectedRoute isProtected>
<Route path="/admin" component={Admin} />
</ProtectedRoute>
</Switch>
HOC
// withAuth
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const withAuth = (WrappedComponent: any) =>
function (props: any) {
const { isLoggedIn ) = useContext(AuthState);
if (!isLoggedIn) {
<Redirect to='/login' />
}
return <WrappedComponent {...props} />;
}
export default withAuth;
Now you can insert your route in the switch without conditions. If your component requires authentification, wrap it with withAuth.
Example:
const NeedAuth = () => (
<div>Hello I need auth</div>
);
export default withAuth(NeedAuth)
I figured out the issue, and yes it was something super small. In the line of code where I check to see if there is a token stored on localstorage, I use an if block with a return statement. I saw a while back that doing this allows for "if" statements to be written completely on a single line and without brackets {} encapsulating the code. At the time it was really just a style choice but now I see that when the if statement runs (i.e. there is a token in local storage) the return statement within overrides the return statement of the whole functional component. So rather than having a context file that returns a provider that wraps your desired children (my admin page router), It just prints the authtoken. So I returned to traditional styling for the If statement and removed the return statement and it worked fine!
I'm using the package Reacter Router to create and manage the routes of my application.
I am using an OAuth authentication system, which returns the following URL to my application:
http://localhost/login-meli?code=1234567890
I need that each time this route with the query "code" is triggered to execute a certain function, which as an example, can be: console.log('It works!')
I tried the code below and it worked, but I didn't find in the documentation how can I specify a specific query. I also noticed that when I add other lines of code an error is returned.
<Route exact path="/login-meli" render={() => (
console.log('works!!!')
)}/>
You can use the useLocation hook from react-router-dom.
import { Route, useLocation } from "react-router-dom";
Then in your route:
const MyComponent = () => {
const location = useLocation();
const params = new URLSearchParams(location.search);
const code = params.get('code');
if (code === '123456789') {
return (<div>It Works!!!</div>);
} else {
return (<div>404 (you can redirect to an error page</div>);
}
}
Then, for your Router:
<Route exact path="/login-meli"><MyComponent /></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();
};
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 using Redux with redux-simple-router.
Here's what I'm trying to do. A user hits a URL like so:
http://localhost:3000/#/profile/kSzHKGX
Where kSzHKGX is the ID of the profile. This should route to Profile container filled out with the details of the profile with id kSzHKGX.
My routes look like this:
export default (
<Route path="/" component={App}>
...
<Route path="profile" component={Profile} />
...
</Route>
)
So hitting the above link would give me Warning: [react-router] Location "undefined" did not match any routes
My container looks like this:
#connect(
state => state.profile,
dispatch => bindActionCreators(actionCreators, dispatch)
)
export class Profile extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
const { getProfileIfNeeded, dispatch } = this.props
getProfileIfNeeded()
}
render() {
return (
<section>
...
</section>
)
}
}
So normally my container would just be populated from the state as usual in Redux.
Basically I need to have a way of doing some wildcard in the route. Than I need to pass the URL to the action that would pull up the right profile from an API. The question is, is it doable with react-simple-router? Can I do that somehow using UPDATE_PATH? Would it be the proper Redux way? Or should I use something else?
Following Josh David Miller's advice, I made my route look like so:
<Route path="admin/profile/:id" component={Profile} />
Than my container got this method to get the profile from API:
componentWillMount() {
const { getProfile, dispatch } = this.props
getProfile(this.props.params.id)
}
And this to cleanup (without it I would have the previous profile display for split second on component load - before I hit API in componentWillMount)
componentWillUnmount() {
this.props.unmountProfile()
}
Update:
As an alternative to the cleanup, I'm considering using the Container Component Pattern. Basically have the outer component do the data fetching and passing the data to the inner component as a prop.