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

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})
}

Related

Move between pages without losing pages content - React JS

I'm trying to navigate between pages without losing pages data. For instance, I have a page that contains many input fields, if a user filled all these fields and tried to move to another page and return back to the first one, all the inputs are gone. I am using react-router-dom but didn't find out a way to prevent that.
What I've done till now :
import { Route, Switch, HashRouter as Router } from "react-router-dom";
<Router>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/hello-world" exact component={HellWorld} />
</Switch>
</Router>
Home Component :
navigateToHelloWorld= () => {
this.props.history.push('/hello-world')
};
Hello World Component :
this.props.history.goBack();
I don't know that that can be supported in such generality. I would just store all state variable values in localStorage, and restore from there when values are present on component render (when using useState then as the default value). Something like this:
const Home = () => {
const [field, setField] = useState(localStorage.field || '');
const handleUpdate = (value) => {
setField(value);
localStorage.field = value;
}
// also add a submit handler incl. `delete localStorage.field`;
return ... // your input fields with handleUpdate as handler.
}
Generally, all you need to do is to store your data in someplace, either a component that doesn't unmount, like the component you are handling your routes in, which is not a good idea actually but it works!
another way is to use some kind of state manager like 'mobx','redux','mst', or something which are all great tools and have great documentation to get you started
another alternative is to store your data in the browser, for your example session storage might be the one to go for since it will keep data until the user closes the tab and you can read it in each component mount.

React Router will not take array as prop for a component

I am trying to pass an array of objects as a prop to a new route. However, whenever I pass the prop and go to that route on my browser, I can see (using console.log) that the array in the prop exists, but it is empty. I'm not sure how to fix this. I've verified that the array is populated before it is passed as a prop.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
routes: []
}
}
addToRoute(record){
let tempRoutes = this.state.routes.slice();
tempRoutes[record.id] = record;
this.setState({
routes: tempRoutes
})
}
render() {
return (
<div>
<Route exact path='/' render={() => <Subapp addToRoute={(record) => this.addToRoute(record)}/>}/>
<Route path={"/record/:id"} render={({match})=><BasicInfo match={match} {...this.state}/>}/>
</div>
);
}
}
I'm unsure where in your app things go wrong, but I do see a few obvious parts in your code that tripped my app up when I was trying to re-create your problem.
<Route exact path='/' render={() => <Subapp addToRoute={(record) => this.addToRoute(record)}/>}/>
Here just pass in this.addToRoute You populate the param within the component.
<Route path={"/record/:id"} render={({match})=><BasicInfo match={match} {...this.state}/>}/>
Restrain yourself from passing on the entire state when possible. Instead, try to explicitly define the props you need. If you don't you might extend state in your component in the future. Then, those new parts of the state are passed along too. Also, explicitly defining props will make your code more readable.
this.state = {
routes: []
}
addToRoute(record){
let tempRoutes = this.state.routes.slice();
tempRoutes[record.id] = record;
this.setState({
routes: tempRoutes
})
}
This won't work. state is an array initially, while in addToRoute() you transform it into an object.
So, you do have some inconsistencies within your code. In trying to recreate your problem I was unable to make the code work because of these errors. After making some adjustments it runs. As you can see I'm passing down the routes object and it is populated in <BasicInfo />. Have a look at this CodeSandBox for a working example.
Let me know if that answered your question.

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.

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.

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

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.

Resources