How can I update a ReactJS component based on URL / path when using React-Router?
The code below works, but is this the correct way to do this? Seems like a lot of code to make a simple update. I was hoping there would be a stateful API call in the router to automatically take care of this scenario.
var MyHomeView = React.createClass({
componentDidMount: function() {
this.props.updateHeader();
},
render: function() {
return (
<div>
<h2>Home</h2>
</div>
);
}
});
var MyAboutView = React.createClass({
componentDidMount: function() {
this.props.updateHeader();
},
render: function() {
return (
<div className="my-page-text">
<h2>About</h2>
</div>
);
}
});
var MyHeader = React.createClass({
mixins: [ CurrentPath ],
getInitialState: function() {
return {
myPath: "about",
classes: "ion-ios7-information"
};
},
updateHeader: function() {
// Classnames refer to www.ionicons.com
if (this.getCurrentPath() === "/") {
this.setState( {myPath: "about" } );
this.setState( {classes: "ion-ios7-information" } );
} else {
this.setState( {myPath: "/" } );
this.setState( {classes: "ion-ios7-rewind" } );
}
},
render: function() {
return (
<Link to={this.state.myPath}>
<i className={this.state.classes} />
</Link>
);
}
});
var App = React.createClass({
updateHeader: function() {
this.refs.header.updateHeader();
},
render: function() {
return (
<div>
<MyHeader ref="header" />
<this.props.activeRouteHandler updateHeader={this.updateHeader} />
</div>
);
}
});
React.renderComponent((
<Routes>
<Route path="/" handler={App}>
<DefaultRoute handler={MyHomeView} />
<Route name="about" handler={MyAboutView} />
</Route>
</Routes>
), document.body);
In react-router 2.0.0 you can use the hashHistory or browserHistory:
browserHistory.listen(function(ev) {
console.log('listen', ev.pathname);
});
<Router history={browserHistory}>{routes}</Router>
This has been updated if you are working with the react-router > v11.0.
You can read the details here
TLDR:
// v0.11.x
var Something = React.createClass({
mixins: [ Router.State ],
render: function () {
var path = this.getPath();
}
});
For the full State API: https://github.com/rackt/react-router/blob/master/doc/04%20Mixins/State.md
This question has been open awhile, and it seems like there should be a more straightforward solution, but I'll take a stab and share what we do in our application.
We are using the Flux architecture, which has the notion of Stores that inform components when their state is updated. In react-router, they have a PathStore that fits into this model nicely, since when the URL changes it can then notify components that care and need to be updated.
The StoreListener we use in our application is publicly available here: https://github.com/odysseyscience/react-flux-suppprt. I should've published to NPM, but haven't yet. I'll do that soon if you think it would be helpful.
Here is how we use our StoreListener to listen for changes on the PathStore from react-router, and how you would use it in your MyHeader:
var StoreListener = require('path/to/StoreListener');
var PathStore = require ('react-router/modules/stores/PathStore');
var MyHeader = React.createClass({
mixins: [
StoreListener(PathStore)
],
getStateFromStores: function() {
return {
path: PathStore.getCurrentPath()
};
},
render: function() {
var path = this.state.path;
var myPath;
var classes;
if (this.state.path === "/") {
myPath = 'about';
classes = 'ion-ios7-information';
} else {
myPath = '/';
classes = 'ion-ios7-rewind';
}
return (
<Link to={myPath}>
<i className={classes} />
</Link>
);
}
});
This starts with a mixin that says "I care about changes to the PathStore". Whenever this store (or any store being listened to) changes, getStateFromStores() is called on your component in order to retrieve the data out of the store that you want available on this.state. If/When that data changes, render() is called again, and you re-read this.state, and apply your changes.
This is exactly our pattern for applying certain "active" CSS classes on header tabs, or anywhere else in the application that depends on the current URL.
Note that we use webpack to bundle our CommonJS modules, so there may be some assumptions about the environment that may/may not work for you.
I didn't answer this question a month ago when I first saw it because I assumed there was a better way, but perhaps our solution can help others with the same issue.
Related
I'm looking to render some firebase data to the HomeFeed component. I update the state in the componentDidMount method. You can see what that looks like below. It's an array. Should I just map over that using the map function? How do I access the specific info like "title", "link", "type", etc. to be able to render it?
Thanks a lot!
var React = require('react');
var Rebase = require('re-base');
var base = Rebase.createClass("https://nimbus-8ea70.firebaseio.com/");
// TODO: Render Firebase data to screen.
// Home
// <Home />
var HomeContainer = React.createClass({
render : function() {
return (
<div className="homeContainer">
<HomeFeed />
</div>
);
}
});
// Home Feed
// <HomeFeed />
var HomeFeed = React.createClass({
componentDidMount: function() {
base.fetch('article', {
context: this,
asArray: true,
then(data){
console.log(data);
this.setState({
feed: data
})
}
});
},
getInitialState: function() {
return {
feed: []
}
},
render : function() {
return (
<div className="homeFeed">
{/* Use map function here? */}
</div>
);
}
});
module.exports = HomeContainer;
render will run whenever state has been changed (unless you modify this behavior with, say, shouldComponentUpdate) so as long as you use setState properly your component will automatically update when its state changes.
If you're asking specifically how to turn an array into something that render understands, then yes, map is a very common way to do that. It might look something like this:
render : function() {
return (
<div className="homeFeed">
{this.state.feed.map(function(ea){
return <div>{ea.someProperty}</div>
})}
</div>
);
}
Note that you have to wrap ea.someProperty in curly braces because you're basically inserting JSX inside of a JavaScript expression inside of even more JSX. This kind of nested JSX/Expression/JSX structure is something you'll have to get comfortable with in React I'm afraid.
More about array.map
I have a React.js app that is constructed like the following:
// App component - represents the whole app
App = React.createClass({
render() {
return (
<div>
<Landing />
<Description />
<Skills/>
</div>
);
}
});
where "Landing", "Description" and "Skills" are all children components of the App component.
In Landing, I have a child component called Social Menu, that gets called using:
<SocialMenu items={ ['Home', 'Services', 'About', 'Contact us']} />
It looks like this:
SocialMenu = React.createClass({
getInitialState: function(){
return { focused: 0 };
},
componentDidMount: function() {
MichaelReactStore.addChangeListener(this.state.focused);
},
clicked: function(index){
// The click handler will update the state with
// the index of the focused menu entry
this.setState({focused: index});
},
render: function() {
// Here we will read the items property, which was passed
// as an attribute when the component was created
var self = this;
// The map method will loop over the array of menu entries,
// and will return a new array with <li> elements.
return (
<div>
<ul className="testblocks">{ this.props.items.map(function(m, index){
var style = '';
if(self.state.focused == index){
style = 'focused';
}
// Notice the use of the bind() method. It makes the
// index available to the clicked function:
return <li key={index} className={style} onClick={self.clicked.bind(self, index)}>{m}</li>;
}) }
</ul>
<p>Selected: {this.props.items[this.state.focused]}</p>
<ItemDetails item={ this.props.items[this.state.focused] } />
</div>
);
}
});
ItemDetails = React.createClass({
render: function() {
return (
<div>{this.props.item}</div>
);
}
});
What I would like to do is "send the state up" to the App component from the social menu. Then I would like to send that data down as a prop to the Skills component, where it will show some dynamic data depending on that state.
How would I do this? Thank you!
(I know this isn't sustainable for a larger app, but for this app, I just need a simple solution)
I would manage the state in the root component and make focused a property (this.props.focused) in all the components you need it. Where you now do the setState you call an callback, like so:
this.props.onFocusChanged(index)
You give this callback as a property to the Landing, and in the Landing you give it as a property to the SocialMenu. Your App would look something like this:
App = React.createClass({
getInitialState: function(){
return { focused: 0 };
},
clicked: (index) => {
this.setState({focused: index});
},
render() {
return (
<div>
<Landing onFocusChanged={this.clicked} focused={this.state.focused} />
<Description />
<Skills focused={this.state.focused}/>
</div>
);
}
});
I want to pass a value to a component this way, but when I try to console log this.props.vouch it returns an undefined value.
I know it will work if I put:
<Something onClick={this.log} vouch=this.props.vouch />
and
ReactDOM.render(<List vouch="value 1"/>, document.getElementById('react-app'))
But I will want to use different vouch value later in the code and be able to reuse Something component.
var Something = React.createClass({
propTypes:{
vouch: React.PropTypes.string,
},
render: function() {
return (
<div>
<h1 onClick={this.props.onClick} vouch={this.props.vouch}>Click!</h1>
</div>
);
}
});
var List = React.createClass({
log: function() {
console.log(this.props.vouch);
},
render: function () {
return (
<Something onClick={this.log} vouch="value 1" />
<Something onClick={this.log} vouch="value 2" />
);
}
});
ReactDOM.render(<List />, document.getElementById('react-app'));
You can't set this.props from child component, but you can pass data using data attributes, like this
<h1 onClick={this.props.onClick} data-vouch={this.props.vouch}>Click!</h1>
...
log: function (e) {
console.log(e.target.dataset.vouch);
},
Example
or using .bind, like this
<h1 onClick={this.props.onClick.bind(null, this.props.vouch)}>Click!</h1>
...
log: function (vouch) {
console.log(vouch);
},
Example
or call callback in child component and pass props, like this
handleClick: function () {
this.props.onClick(this.props.vouch)
},
render: function() {
return (<div>
<h1 onClick={this.handleClick}>Click!</h1>
</div>)
}
...
log: function (vouch) {
console.log(vouch);
},
Example
You're not passing this.props.vouch to List, so your log will return undefined.
var Something = React.createClass({
propTypes:{
vouch: React.PropTypes.string,
},
onClick: function() {
this.props.onClick( this.props.vouch )
},
render: function() {
return (
<div>
<h1 onClick={this.onClick.bind( this )} vouch={this.props.vouch}>Click!</h1>
</div>
);
}
});
var List = React.createClass({
log: function( vouch ) {
console.log( vouch );
},
render: function () {
return this.props.vouch.map( vouch => <Something onClick={ log } vouch = { vouch } /> )
}
});
var vouch = [
{
value: 'foo'
},
{
value: 'bar'
}
]
ReactDOM.render(<List vouch={ vouch } />, document.getElementById('react-app'));
The actual problem of your log not working could also be solved by passing List.log to Something (which you do already) and then invoking it in the context of Something by using <h1 onClick={ this.props.onClick.call( this ) and having log console.log( this.props.vouch ) but this solution would be nasty from a maintainability standpoint.
It is important to understand the parent->child relationship between components that you are creating. At any point you can grab your vouch data and inject it but by injecting it at the List component you keep all children pure i.e. when you render you are passing the state of the system, you arent attempting to grab state or worse, mutate, state during the life-cycle of a render.
I have a component that has several nested components, specifically tabs, pills, etc. No all are visible at any one time.
I want to be able to set their default state/props based on the URL parameters (or URL) but I'm not clear how to achieve this in React.
For example, I have a component with two tabs (bootstrap). When the user selects the second tab, can i modify the URL with a parameter so that is the page is refreshed, the state of the currently selected tab is retained.
I've included the Router but cannot find any simple examples of what I'm trying to achieve.
What I'd like to be able to do is access/parse the query string parameters and set the state and ultimately properties of the child components.
Any advice is really welcome.
I can provide example of the tab component, but don't think it would add any value to the question.
The way I've gone about this it to take advantage of React contexts. It's an undocumented but powerful feature that kind of acts like mutable props that can be accessed by a component and all its children.
I suggest Googling about it. It's a bit tough to wrap your mind around, but starts to make sense once you play with it. Here is an example excerpt from some code I am working on right now:
var React = require('react');
var Router = require('react-router'),
Route = Router.Route,
RouteHandler = Router.RouteHandler;
var ReactBootstrap = require('react-bootstrap'),
DropdownButton = ReactBootstrap.DropdownButton,
Nav = ReactBootstrap.Nav,
Navbar = ReactBootstrap.Navbar;
var ReactRouterBootstrap = require('react-router-bootstrap'),
MenuItemLink = ReactRouterBootstrap.MenuItemLink;
var Nav = React.createClass({
render: function() {
return (
<Navbar brand='My Cool Page!' inverse toggleNavKey={0}>
<Nav right eventKey={0}> {}
<DropdownButton eventKey={2} title='Projects'>
<MenuItemLink eventKey="1" to="myCoolPage" params={{ awesomeName: "matt" }}>Go to My Cool Page!</MenuItemLink>
</DropdownButton>
</Nav>
</Navbar>
);
}
);
var MyCoolPage = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function() {
return {
awesomeName: ''
};
},
componentWillMount: function() {
// Called once before the first render
this.getAwesomeName();
},
componentWillReceiveProps: function(nextProps) {
// Called when props change after the first render
// From my testing, this is also called when a context changes
this.getAwesomeName();
},
getAwesomeName: function() {
var { router } = this.context;
var awesomeName = router.getCurrentParams().awesomeName;
this.setState({awesomeName: awesomeName});
},
render: function() {
return (
<h1>My awesome name is: {this.state.awesomeName}</h1>
);
}
});
var App = React.createClass({
render: function() {
return (
<div>
<Nav />
<RouteHandler {...props} />
</div>
);
}
});
var routes = (
<Route handler={App}>
<Route name="myCoolPage" path="myCoolPage/:awesomeName" handler={MyCoolPage} />
</Route>
);
Router.run(routes, Router.HashLocation, function (Root) {
React.render(<Root/>, document.body);
});
I have a requirement to fetch data from a server everytime a page is rendered.
Usually this (ajax) request should be performed inside componentDidMount, but with ReactRouter, componentDidMount is not fired when a url has changed.
This is my attempt so far:
// App.js
var App = React.createClass({
render: function() {
return (
<RouteHandler />
);
}
var routes = (
<Route handler={App} path="/" >
...
<Route handler={PageA} name="PageA" path="/pagea" />
...
</Route>
);
Router.run(routes, Router.HashLocation, function(Handler) {
React.render(<Handler/>, document.body);
});
});
// PageA.js
var PageA = React.createClass({
statics: {
willTransitionTo: function (transition, params, query, callback) {
console.log("willTransitionTo PageA");
this.handleGet(callback);
}
, willTransitionFrom: function (transition, component) {
console.log("willTransitionFrom PageA");
}
}
, handleGet(callback) { ... }
...
}
I would like to intercept an event 'page will appear' and willTransitionTo (and willTransitionFrom) seems to do the job as expected. However, the problem is I can't access PageA.handleGet inside that method. I would appreciate if you can guide me to a right path for achieving this task as I might misunderstand some important concepts of using a router.
P.S. I didn't use Flux (yet).
I found a workaround at here where fetched data is passed down, as props, to a target component (PageA) by using a wrapper. Instead of managing ajax requests in the target component's componentDidMount, I have to do it in the wrapper component's componentDidMount. This way, App.js can become very large if there are many pages dealing with pre-loading requests.
// App.js
var PageAWrapper = React.createClass({
getInitialState: function() {
return {
data: []
};
}
, handleGet: function() {
var promise = $.ajax({...});
promise.done(function(data) {
this.setState({data: data});
}.bind(this));
}
, componentDidMount: function() {
this.handleGet();
}
, render: function () {
return (
<PageA data={this.state.data} />
);
}
});
...
var routes = (
<Route handler={App} path="/" >
...
<Route handler={PageAWrapper} name="PageA" path="/pagea" />
...
</Route>
);
EDIT
Because data is mutable inside PageA, but I am forced to pass it as props. Therefore, I also have to modify PageA slightly.
var PageA = React.createClass({
getInitialState: function() {
return {
data: []
};
}
, componentWillReceiveProps: function(nextProps) {
this.setState({data: nextProps.data})
}
, render: function() {
... use this.state.data to render ...
}
});