Where and How to request data asynchronously to be passed down as props with React Router (v 1) - reactjs

After reading many questions regarding this topic I am still unsure as to which is the best way to asynchronously fetch data which later will be passed down as props to the child routes with React Router v1.0.0 and up.
My route config looks something like this:
import { render } from 'react-dom';
// more imports ...
...
render(
<Router>
<Route path="/" component={App} />
<IndexRoute component={Dashboard}/>
<Route path="userpanel" component={UserPanel}/>
</Router>,
document.getElementById('container')
)
In my App component I have code which asynchronously fetches data from the backend and will incorporate it into its state, if fetching was successful. I use componentDidMount for this within App.
The state of App will look like this contrived example:
{
user: {
name: 'Mike Smith',
email: 'mike#smith.com'
}
}
I would want to pass the user part of state as props to my IndexRoute and the userpanel route. However I am not sure how I should do this.
A few questions come to mind:
Should I place the async data request somewhere else within my code?
Should I use the React Router api (like onEnter) instead of React lifecycle methods for the data fetching?
How can I pass the state (user) of App to the Dashboard and UserPanel components as props?
I am unsure how to do this with React.cloneElement as seen in other answers.
Thanks for the help in advance.

What you are asking for is persistent data between routes and that's not the job of the router.
You should create a store (in flux terms), or a model/collection (in MVC terms) - the usual approach with react is something flux-like. I recommend redux.
In the redux docs it has an example of fetching a reddit user:
componentDidMount() {
const { dispatch, selectedReddit } = this.props
dispatch(fetchPostsIfNeeded(selectedReddit))
}
Personally I don't think flux/redux is the easiest approach to implement, but it scales well. The essential concept is even if you decide to use something else:
You are correct, as Facebook suggests, async fetching goes best in componentDidMount.
If you want to integrate with other JavaScript frameworks, set timers using setTimeout or setInterval, or send AJAX requests, perform those operations in this method.
Next you need to set this data in a store/model which can be accessed from other components.
The nice thing about redux (with react-redux) is that for each component you can say "Here are the actions this component is interested in" and then that component can simply call the action like UserActions.fetchUserIfNeeded() and the action will figure out whether it already has the user or if it should be fetched, and afterwards it will re-render and the prop will be available.
Answer to Q4: What are you trying to clone and why? If it's a child see this answer.

You can do one thing when your application start at that time you will call the API and fetch the data and register your Route like
my index.js is entry file then
here I have used React-Router 0.13.3 you can change the syntax as per new Router
fetchData(config.url+'/Tasks.json?TenantId='+config.TenantId).then(function(items)
{
var TaskData=JSON.parse(JSON.stringify(items.json.Tasks));
var Data=[];
Object.keys(TaskData).map(function(task){
if(TaskData[task].PageName !=='' && TaskData[task].PageUrl !=='')
{
Data.push({PageName:TaskData[task].PageName,path:TaskData[task].PageName+'/?:RelationId',PageUrl:TaskData[task].PageUrl});
}
});
Data.push({PageName:'ContainerPage',path:'/ContainerPage/?:RelationId',PageUrl:'./pages/ContainerPage'});
var routes=require('./routes')(Data);
$("#root").empty();
Router.run(routes,function(Handler){
React.render(<Handler />,document.getElementById('root'));
});
React.render(<UserRoles />, document.getElementById("userrole"));
}).catch(function(response)
{
showError(response);
});
I have pass the data to routes.js file like var routes=require('./routes')(Data); and my routes.js file look like
export default (data =>
<Route name="App" path="/" handler={App}>
<NotFoundRoute handler={require('./pages/PageNotFound')} />
<Route handler={TaskList} data={data} >
</Route>
{ data.map(task =>
<Route name={task.PageName} path={task.path} handler={require(task.PageUrl)}>
</Route>
) }
</Route>
);

I am not entirely sure I understand the question, but I just recently passed properties to the children of my routes as well. Pardon me if this is not the best way of doing it, but you'll have to clone your children and edit them and then pass down the copies not the children. I'm not sure why react and the react router make you do this, but try this:
let children (or whatever you want to name it) = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {name of property: property value});
});
Afterwards, you should be able to access those properties in this.props in the sub routes. Please ask if you have any questions because this is pretty confusing.

Related

Dynamic paths in react router

I am trying to migrate my website from traditional web app approach to react based app. In this process I am stuck with a problem related to urls. In my website, on server side I use Url Rewrite feature to map urls to their correct controller. But I am not able to figure out how to handle this thing in react router. My current, react router code looks like this
<BrowserRouter>
<Route exact path="/" component={Home}/>
<Route path="/black-forest" component={Product}/>
<Route path="/cakes" component={ProductList}/>
</BrowserRouter>
This code I have written for building a prototype. But ideally there are many different urls which can point to ProductList component. Similarly there are many urls which can point to Product component.
For e.g,
Following urls points to ProductList component
- http://www.example.com/best-cakes-in-australia
- http://www.example.com/cake-delivery-in-india
- http://www.example.com/flowers-delivery-in-canada
At a gross level I have around 10,000 such urls which are created using server side UrlRewrite hence they follow no specific pattern. These url are mostly going to point towards either ProductList or Product component.
How can I create path for all these urls in my react router config? Any pointers would be greatly appreciated.
You may have a separate endpoint to check requested url, which returns page type like 'Product' or 'ProductList' and based on this result you can load more data
For example, let's say you have http://www.example.com/api/urlcheck
in your Router:
<Route exact path="/" component={ Home } />
<Route path="/:customPath" component={ Wrapper } />
in Wrapper
constructor(props) {
super(props)
this.state={
componentName: null
}
}
componentDidMount() {
let pathname = this.props.location.pathname.substr(1)
// alternatively you can get pathname from this.props.location.match.params.customPath
fetch(/* send request to http://www.example.com/api/urlcheck with pathname */)
.then((response)=>{ this.setState({componentName: response.componentName }) })
}
render (){
const { componentName } = this.state
if(componentName === 'Product') return <Product />
else if(componentName === 'ProductList') return <ProductList />
}
in Product or ProductList components in a similar way you can request for specific set of data by id or any other key.
Keep in mind though, if SEO is a big piece, you most likely want to have server side rendering, which is not happening in the example above. componentDidMount renders only in browser and you'll have to replace that with componentWillMount (the only function in react lifecycle that runs on SSR).
SSR is a bit of a pain, especially with requests that are based on responses from other requests. Redux-saga makes life so much easier with this kind of stuff, so I'd recommend to go saga way in your case as well.
You can take a look at react-saga-universal to quickly get up and running with saga and SSR.

Passing data from the Redux store, as Props, to an unknown number of routes

Here is the situation:
I am getting an unknown amount of data from a service and putting it in my Redux store
Each data-point will have its own route /:rid
The route is setup like this <Route path=':rid' component={Restaurant}/> using react-router (v3) -- see full render method below
I'd like to be able to pass the correspond data about the specific restaurant, the one whose route was navigated to, to the Restaurant component as props so it can render the component with the correct information for that restaurant
Right now my solution is to pass all the restaurants from the store into the Restaurant component as props this.props.restaurants. Then in componentWillReceiveProps I loop over all the restaurants in this.props.restaurants and check to see if the rid of each restaurant is equal to the this.props.routeParams ie :rid. If it is I then set that state to contain the data I want to show and reference this.state.name as opposed to the data being on `this.props.name'
Is there another way to do this? Right now it's not a performance issue but I can imagine looping over and arbitrarily large data set could lead to so serious load times. Also, it just seems like there should be a way for react-router to pass in this data as props so I can keep this component stateless.
Ideally, something like this would happen:
a request is made to /1234
react-router in my index.js consults/queries the redux store and finds the data for the restaurant with rid 1234 and passes it as props to the component it renders
I imagine it looking something like this <Route path=':rid' component={<Restaurant {...matchedRestaurant} />}/>
Perhaps this questioning can be asked in short like, how do I make a unknown number of routes such that when one is navigated to it is rendered with the data for that corresponding restaurant as props?
Restaurant.js:
componentWillReceiveProps(nextProps) {
this.props.restaurants.forEach((restaurant) => {
if(restaurant.rid == nextProps.routeParams.rid) this.setState({name: restaurant.name})
})
}
index.js:
render(
(
<Provider store={store}>
<Router history={hashHistory}>
<Route path='/' component={App}>
<IndexRoute component={RestaurantList} />
<Route path=':rid' component={Restaurant}/>
</Route>
</Router>
</Provider>
),
document.getElementById('root')
)
https://github.com/caseysiebel/corner-team/blob/master/src/index.js
Instead of having react-router figure this out for you, you should be using selectors (and potentially a package like reselect). Reselect even has a section on how to base your selector on props (in this case like the routerParams.rid): https://github.com/reactjs/reselect#accessing-react-props-in-selectors
For the non-Reselect solution, you could simply change the connect in your Restaurant component like so:
#connect((state, props) => {
return {
restaurants: state.restaurant.restaurants.find((restaurant) => {
return restaurant.rid == props.routeParams.rid
}),
}
})
As #Sean Kwon commented, you should also normalize your data which would make this selector trivial:
#connect((state, props) => {
return {
restaurants: state.restaurant.restaurants[props.routeParams.rid],
}
})
Assuming you have connected your action via mapDispatchToProps, you organize your store and async actions so that this can be possible.
componentDidMount() {
this.props.fetchRestaurant(this.props.params.rid)
}
The store will then update your component with the corresponding restaurant data. This way, you're calling some kind of action to get the corresponding data whilst reducing the need to use the component state, which you should try to avoid in order to keep your state centralized.
Otherwise, for a quick and dirty solution, you can just do this really quickly.
componentWillReceiveProps(nextProps) {
var name = this.props.restaurant.find(res => res.rid === nextProps.routeParams.rid)
this.setState({name: name})
}

How to hydrate server-side parameters with React + Redux

I have a universal React app that is using Redux and React Router. Some of my routes include parameters that, on the client, will trigger an AJAX request to hydrate the data for display. On the server, these requests could be fulfilled synchronously, and rendered on the first request.
The problem I'm running into is this: By the time any lifecycle method (e.g. componentWillMount) is called on a routed component, it's too late to dispatch a Redux action that will be reflected in the first render.
Here is a simplified view of my server-side rendering code:
routes.js
export default getRoutes (store) {
return (
<Route path='/' component={App}>
<Route path='foo' component={FooLayout}>
<Route path='view/:id' component={FooViewContainer} />
</Route>
</Route>
)
}
server.js
let store = configureStore()
let routes = getRoutes()
let history = createMemoryHistory(req.path)
let location = req.originalUrl
match({ history, routes, location }, (err, redirectLocation, renderProps) => {
if (redirectLocation) {
// redirect
} else if (err) {
// 500
} else if (!renderProps) {
// 404
} else {
let bodyMarkup = ReactDOMServer.renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>)
res.status(200).send('<!DOCTYPE html>' +
ReactDOMServer.renderToStaticMarkup(<Html body={bodyMarkup} />))
}
})
When the FooViewContainer component is constructed on the server, its props for the first render will already be fixed. Any action I dispatch to the store will not be reflected in the first call to render(), which means that they won't be reflected in what's delivered on the page request.
The id parameter that React Router passes along isn't, by itself, useful for that first render. I need to synchronously hydrate that value into a proper object. Where should I put this hydration?
One solution would be to put it, inline, inside the render() method, for instances where it's invoked on the server. This seems obviously incorrect to me because 1) it semantically makes no sense, and 2) whatever data it collects wouldn't be properly dispatched to the store.
Another solution which I have seen is to add a static fetchData method to each of the container components in the Router chain. e.g. something like this:
FooViewContainer.js
class FooViewContainer extends React.Component {
static fetchData (query, params, store, history) {
store.dispatch(hydrateFoo(loadFooByIdSync(params.id)))
}
...
}
server.js
let { query, params } = renderProps
renderProps.components.forEach(comp =>
if (comp.WrappedComponent && comp.WrappedComponent.fetchData) {
comp.WrappedComponent.fetchData(query, params, store, history)
}
})
I feel there must be better approach than this. Not only does it seem to be fairly inelegant (is .WrappedComponent a dependable interface?), but it also doesn't work with higher-order components. If any of the routed component classes is wrapped by anything other than connect() this will stop working.
What am I missing here?
I recently wrote an article around this requirement, but it does require the use of redux-sagas. It does pickup from the point of view of redux-thunks and using this static fetchData/need pattern.
https://medium.com/#navgarcha7891/react-server-side-rendering-with-simple-redux-store-hydration-9f77ab66900a
I think this saga approach is far more cleaner and simpler to reason about but that might just be my opinion :)
There doesn't appear to be a more idiomatic way to do this than the fetchData approach I included in my original question. Although it still seems inelegant to me, it has fewer problems than I initially realized:
.WrappedComponent is a stable interface, but the reference isn't needed anyway. The Redux connect function automatically hoists any static methods from the original class into its wrapper.
Any other higher-order component that wraps a Redux-bound container also needs to hoist (or pass through) any static methods.
There may be other considerations I am not seeing, but I've settled on a helper method like this in my server.js file:
function prefetchComponentData (renderProps, store) {
let { params, components, location } = renderProps
components.forEach(componentClass => {
if (componentClass && typeof componentClass.prefetchData === 'function') {
componentClass.prefetchData({ store, params, location })
}
})
}

Get props (or global variables)

I'm attempting to set some global variables on my app which I want to be available to all components. Lets say for example that I want a 'language' and a 'status' property to be passed to each component. This property won't be rendered to the page, instead it will be added to the props for each component, this will be so I can check for that variable when each component loads and output the appropriate styles and languages.
I was hoping it would be something simple like adding props to the router, however no matter what I try, the props come back as 'undefined' on my child components (only the main layoutWrapper component gets the props). Here is how it looks so far:
//app.js
var LayoutWrapper = React.createClass({
render: function () {
return (
<Layout status="available" />
);
}
});
ReactDOM.render(
<Router history={hashHistory}>
<Route path="/" language="en-gb" component={LayoutWrapper}>
<IndexRoute component={Index}></IndexRoute>
</Route>
</Router>,
app);
When handling global level state data, it's recommended you use some kind of state framework like Flux. I'd recommend Redux as it does a great job of reducing boilerplate code to easily pass down app state to any connected component (and subsequently subscribe these components to any changes in the state).
What you are doing fails because there's no consistent way of creating "global" props; you could use the context variable but this is an unstable feature that is not recommended for production use. Otherwise, you have to manually pass down your props from parent to child explicitly.

React router with browserHistory goes to server on every URL change

I am doing something like:
<Router history={browserHistory}>{routes}</Router>
When I do above whenever URL in address bar changes call is going to server but this is not what I want, I want first time page to load from server but after that whenever route change component should load in client side only. Am I missing something here?
In client side I am doing something like :
ReactDOM.render(
<Provider store={app.store}>
<Router history={browserHistory}>{routes}</Router>
</Provider>,
document.getElementById("app")
);
and my routes look like:
const routes = (
<Route path="/" component={DJSAppContainer}>
<Route path="page" component={DJSPage}>
<Route path="/page/:pageName" component={PageContainer} />
</Route>
</Route>
);
Now whenever I do location.href = "/page/xyz" it goes to server and load the content.
You shouldn't change location.href directly. You should send the new path to React using:
ReactRouter.browserHistory.push(newPath);
If you have anchor tags, you should use the <Link> component mentioned in #ahutch's answer.
React router exposes as props the history used in the current views. See their docs for all the props that are injected here.
If you want to redirect from DJSAppContainer or any of the two children views DJSPage or PageContainer you could do it by accessing the history property:
const {history} = this.props;
history.pushState('/path/to/new/state');
If on the other hand you do some fancy stuff and want to redirect from outside the components, try this (taken from react router docs)
// somewhere like a redux/flux action file:
import { browserHistory } from 'react-router'
browserHistory.push('/some/path')
This two approaches assume you are trying to redirect based on some complex logic, not as a consequence of a click event. If you just want to handle click events, try using the <Link/> component of react-router.
It seems that the history property is now deprecated, and seems to be replaced by the router context property as seen in the implementation of the link component. Basically, if you really want your code to be future proof, you should ask for the contextType:
contextTypes: {
router: object
},
And then use it from the context of the component:
this.context.router.push('/some/path')
Assuming you're using node on the back-end, make sure that you have it set up according to the react-router docs.
// send all requests to index.html so browserHistory in React Router works
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'index.html'))
})
Also, when you're linking between components and you want to ensure that you are using react-router for the routing instead of the server, make sure to use Link. Hope that answers your question.
I was having a similar issue, while hashHistory worked without issue browserHistory would always load from the server. I got things working by remembering to call preventDefault(). Here is my function:
handleSubmit: function (e) {
e.preventDefault();
var username = this.usernameRef.value;
this.usernameRef.value = '';
this.context.router.push(`/profile/${username}/`);
}
handleSubmit is called on a form onSubmit.

Resources