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} />
}} />
Related
I'm using the following versions:
`"react-router": "^5.2.0",`
`"react-router-domreact-router": "^5.2.0",`
Not sure if my current setup is React-router 5 friendly or not, I was using a version prior to v5 before this.
The problem in this example is with <Route component={withTracker(InterviewContainer)} path="/interviews/companies/:companyId" /> and <Link/>
Here's my scenario:
Home page loads with a list of company links
Click on a company <Link /> which routes me to /interviews/companies/:companyId
Page loads fine, I see images, etc. for that particular company
Click browser's Back button
Click on a different company <Link /> that points to a different companyId
Problem: for #5, when the company page initially loads, it's loading with stale images and data for some reason. So in other words, I'm seeing the previous company's data & images from step #2 briefly until my React hook makes a new call to get data for this new CompanyId and repaints the browser with the right data (data for the companyId represented in the new route)
index.tsx (note the use of BrowserRouter here)
import { BrowserRouter as Router } from 'react-router-dom';
//...more code and then:
render(
<>
<div className="Site">
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
</div>
<Footer />
</>,
);
App.ts
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
...more code and then here are my routes:
<Switch>
<Route component={withTracker(HomePageContainer)} exact path="/" />
<Route
path="/companies/:companyId/details"
render={(props: RouteComponentProps<{ companyId: string }>) => (
<CompanyDetailContainer {...props} fetchCompanyNew={fetchCompanyNew} httpRequest={Request} useFetchCompany={useFetchCompany} />
)}
/>
<Route component={withTracker(InterviewContainer)} path="/interviews/companies/:companyId" />
<Route component={withTracker(About)} path="/about" />
<Route component={withTracker(Container)} path="/" />
<Route component={withTracker(NotFound)} path="*" />
</Switch>
Here is how the company Link is coded:
Note: I am using Redux State
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
InterviewContainer.tsx (the parent that does the company fetching)
class InterviewContainer extends Component<PropsFromRedux & RouteComponentProps<{ companyId: string }>> {
componentDidMount() {
const { fetchCompany } = this.props;
const { companyId } = this.props.match.params;
fetchCompany(companyId);
}
render() {
const { company } = this.props;
return (company && <Interview className="ft-interview" company={company} />) || null;
}
}
const mapState = (state: RootState) => ({
company: state.company.company,
});
const mapDispatch = {
fetchCompany: fetchCompanyFromJSON,
};
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
export default withRouter(connect(mapState, mapDispatch)(InterviewContainer));
LinkItem.tsx (one of the children rendered by InterviewContainer and receives the company from InterviewContainer)
render() {
const { company } = this.props,
uri = company.notInterviewed ? `companies/${company.id}/details` : `/interviews/companies/${company.id}`,
className = `margin-top-10 margin-bottom-10 ${company.notInterviewed ? 'ft-company-not-interviewed' : ''}`;
const link = (
<Link className={className} id={company.id.toString()} to={uri}>
<span id="company-name">{company.name}</span>
</Link>
);
}
I think I may have to reset Redux state on route change. I see people in the past have used LOCATION_CHANGE but that's outdated and that's a constant provided by third party redux libs that are no longer supported. So not sure how to do that with Redux v7+
So I think I just need a way to detect a location change and then somehow update my react store to reset company (set company: state.company.company, to undefined from my redux action)
I know things like this can be cumbersome. Have you tried passing in state with the Link as <Link to={uri} state={...someState} />. Then wherever it is loading it should rerender or reset props according to that. Maybe throw some skeleton loaders or conditional rendering logic.
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 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'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.