I have a perplexing problem that I cant figure out. I am new to react + flux + react router.
I have the following routes:
<Router history={browserHistory} >
<Route path="/" component={App}>
<IndexRoute component={Search}/>
<Route path="/login" component={Login} />
<Route name="client" path="/client(/:clientid)" component={Client}/>
{/* <Route name="tel" path="/tel/:telid" component={Tel}/>*/}
</Route>
</Router>
I want to be able to pass specific params to specific routes. I know i can pass all params to all routes using:
{this.props.children && React.cloneElement(this.props.children, {...})}
however I dont want to allow sub components access to other component states. Ideally I'd like to pass the loginState to the login component, the searchState to the search component and the clientState to the client component. with the above method the client component can access all component states.
I currently have this workaround but it feels dirty and not very future proof:
var React = require('react');
var AppStore = require('../stores/AppStore');
var browserHistory = require('react-router').browserHistory;
// Flux cart view
var App = React.createClass({
getInitialState() {
return AppStore.getState();
},
componentWillMount() {
if (!this.state.auth.loggedIn) {
browserHistory.push('/login');
}
},
// Add change listeners to stores
componentDidMount() {
AppStore.addChangeListener(this._onChange);
},
// Remove change listers from stores
componentWillUnmount() {
AppStore.removeChangeListener(this._onChange);
},
// Method to setState based upon Store changes
_onChange(){
this.setState(AppStore.getState());
},
// Render cart view
render() {
console.log(this.props.children);
var name = this.props.children.type.displayName;
var p;
switch(name) {
case 'client':
p = {clientState: 'test'}
break;
case 'login':
p = {auth: this.state.auth}
break;
}
return (
<div>
{this.props.children && React.cloneElement(this.props.children, p)}
</div>
);
}
});
module.exports = App;
Any thoughts on how to achieve this correctly? I have looked at a lot of google results but most harken back to the old versions of react and react-router.
you can use named components
...
<Route path="/login" components={{login:Login}} />
<Route path="/client(/:clientid)" components={{client:Client}}/>
...
And in App component
class App extends Component {
constructor(props){
super(props);
this.setProps= this.setProps.bind(this);
}
setProps(comp, props){
if(!comp) return null;
return React.cloneElement(comp, props);
}
render(){
const {client, login}=this.props;
const {clientData, loginData}=this.state;
return (<div>
{this.setProps(client,clientData)}
{this.setProps(login,loginData)}
</div>);
}
}
export default App;
it also usefull when on page more then one component per route
Related
I am trying to reload onto the same route without having to refresh the page. For this specific case, using history.pushState(), but I'm getting an error:
TypeError: history.pushState is not a function.
Here is my code:
import React from 'react';
import PropTypes from 'prop-types';
import { Container } from 'kawax-js';
import { Switch, Route } from 'react-router-dom';
import File from './FileContainer';
import Folder from './FolderContainer';
import HomeContainer from './HomeContainer';
class RootContainer extends React.Component {
static stateToProps = ({ ownProps, select }) => {
const files = select('files');
const lastFile = _.last(files);
return ({
lastFile: lastFile || {}
})
};
static propTypes = {
history: PropTypes.object.isRequired
};
static defaultProps = {
lastFile: {}
};
render() {
const { lastFile, history } = this.props;
if( lastFile === {} || !lastFile.isUploaded
|| lastFile.isUploaded === null) {
return (
<Switch>
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
);
}
return history.pushState(null, "/:folderName")
}
}
export default Container(RootContainer);
Is there a better way of doing this or am I missing something here?
You may get the desired result by forcing the component to rerender, take a look at the documentation here. I see you are extending React.Component so you should be able to do the following:
...
constructor() {
this.reload = this.reload.bind(this);
}
...
reload() {
this.forceUpdate();
}
...
I know it does not use history but there will be no other code required as it is included with the Component class.
please use this code
Router.browserHistory.push('/');
instaed of history.pushState(null, "/:folderName")
You have few possibilities to do that, currently my favorite way to do that is using anonymous function in component prop:
<Switch>
<Route exact path="/" component={()=><HomeContainer/>} />
<Route exact path="/file/:itemPath/:refHash" component={()=><File/>} />
<Route exact path="/:folderName" component ={()=><Folder/>}/>
</Switch>
Or if you want to refresh with current url params, you'll need extra route (reload), and play a little with router stack:
reload = ()=>{
const current = props.location.pathname;
this.props.history.replace(`/reload`);
setTimeout(() => {
this.props.history.replace(current);
});
}
<Switch>
<Route path="/reload" component={null} key="reload" />
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
<div onCLick={this.reload}>Reload</div>
I know there are quite a few questions on this, but I cannot seem to get it to work: I need to access "history" from the child components that are rendered through the Routes. (It receives props from a redux container).
I need to pass down the history object to the components that are rendered in each Route, so that I can this.props.history.push('/route') in the child components. This application was less dynamic before, so each Route was hardcoded with a component={someComponent}; but I found that in doing the Routes dynamically, you need to use render={() => <someComponent />}.
After changing the Routes from component={} to render={() => ...} I lost history in the child components.
My code is something like:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Link, Route, Redirect } from 'react-router-dom';
import { Nav_Item } from '.'
import DynamicTab from '.';
export default class NavBar extends React.Component {
constructor(props) {
super(props);
this.state={}
}
render() {
let tabs = [];
let routes = [];
this.props.tabs.forEach( function(tab, index) {
tabs.push(<Nav_Item key={tab.name} path_name={'/' + tab.name} tab_text={tab.label} />);
routes.push(<Route key={tab.name} path={'/' + tab.name} render={() => <DynamicTab tabName={tab.name} tabSpecs={tab} />} />);
})
return (
<Router>
<div>
<div>
<ol>
{ tabs }
</ol>
</div>
<Redirect to={'/' + this.props.tabs[0].name} />
{ routes }
</div>
</Router>
)
}
}
When you use render you actually get props. In the docs they have an example like this:
<Route {...rest} render={props => (
<FadeIn>
<Component {...props}/>
</FadeIn>
)}/>
So you should be able to access history from those props.
Another solution would be to conditionally render a <Redirect/> component. So maybe you have some internal state that you use like this:
// in your components render function..
if(this.state.shouldRedirect) {
return (<Redirect to={yourURL} />);
}
this.props.router and you have access to whatever you want.
in your case you can just do:
this.props.router.push("/newurl");
There is no need to pass the history as a separate property if this component is rendered by the router.
I created a Meteor createContainer to wrap the entire App to just look for Meteor.users():
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import App from './../App.js';
export default AccountContainer = createContainer(() => {
const _id = Meteor.userId();
const accountHandle = Meteor.subscribe('userData', _id);
const loading = !accountHandle.ready();
const user = Meteor.users.findOne(_id);
const userExist = !loading && !!user;
return {
loading,
userExist,
user
};
}, App);
I need to use this data all around the App to show / hide elements and to take control of User private pages like a control panel.
I'm using Redux to store the data coming from createContainer and actually I'm doing it in the App.js file:
// a few imports here...
class App extends Component {
render() {
const { loading, userExist, user } = this.props;
this.props.updateUser({ loading, userExist, user });
return (
<Router history={history}>
<div className="app">
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/account" component={Account} />
<Route exact path="/account/pictures" component={Pictures} />
</Switch>
<Newsletter />
<Footer />
</div>
</Router>
);
}
};
export default connect(null, { updateUser })(App);
As you can see here I call this.props.updateUser({ loading, userExist, user }); and it update my redux store.
The issue is that I do receive the following error:
Warning: setState(...): Cannot update during an existing state
transition (such as within render or another component's
constructor). Render methods should be a pure function of props and
state; constructor side-effects are an anti-pattern, but can be moved
to componentWillMount.
This because I'm updating Redux state within the render() App method.
I tried wrapping it between a setTimeout:
setTimeout(() => {
this.props.updateUser({ loading, userExist, user });
});
This solve my issue but I think it is not the best solution to fix it.
I'm confused about how and when to update Redux state using Meteor createContainer.
What I wish to achieve is to be able share the { loading, userExist, user } data all around the App taking that information using for ie in Account.js:
const mapStateToProps = (state) => {
return state.user;
}
export default connect(mapStateToProps)(Account);
Thanks.
EDIT:
Just moved the Redux update frm render() to componentDidMount() method.
It doesn't show the error anymore, it update well in App.js but in Account.js I don't get it updated, it looks like it miss it:
I have recently updated to react router version 4. In the previous version I was using the onUpdate call back to trigger a function to make the page scroll to the top when the route has been changed.
It appears that onUpdate has been deprecated and I cannot find anywhere in the docs what it has been replaced with.
Has anyone else come across this issue?
const handlePageChange = () => {
window.scrollTo(0, 0);
};
ReactDOM.render(
<Provider store={store}>
<Router onUpdate={handlePageChange} history={browserHistory}>
<Redirect from="/" to="/music" />
{routes}
</Router>
</Provider>,
document.getElementById('root')
);
"onUpdate" is depreciated. You can use "onEnter" property in "Routes".
<Router history={browserHistory}>
<Route path="/" component={App} >
<IndexRoute component={Home} />
<Route path="/contact-us" component={ContactUs} onEnter={handlePageChange}/>
</Route>
</Router>
Also need to modify your "handlePageChange" function as below:
const handlePageChange = () => {
window.scrollTo(0, 0);
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
}
#Alireza's answer was in the right direction, but it's not quite complete.
To be able to access the router in using React's Context API, the component both has to be a child of the Router, and it should define the contextTypes property:
static contextTypes = {
router: PropTypes.object
};
That will make sure that the Router is attached to that component.
Furthermore, you can not (or no longer?) subscribe to the router. However, you can attach a listener to the History:
this.context.router.history.listen(handlePageChange)
You'll probably want to do that in the component's componentDidMount lifecycle method.
Another option is to scroll the page when your page component mounts:
class NotFoundView extends React.Component {
componentDidMount() {
window.scrollTo(0, 0);
}
render() {
var props = this.props;
return (
<div className="NotFound">
<HeaderContainer />
<h1>Coming Soon!</h1>
</div>
)
}
}
export default NotFoundView;
There is something called context.router.subscribe as replacement...
you can use something like this:
import React from 'react';
class App extends React.Component {
//something like this in your code
componentDidMount() {
this.context.router.subscribe(() => {
console.log("change router");
})
}
render() {
return <button>hi</button>;
}
}
export default App;
I have an app using react #0.14, redux #3.05, react-router #1.0.3, and redux-simple-router #2.0.2. I'm trying to configure onEnter transitions for some of my routes based on store state. The transition hooks successfully fire and push new state to my store, which changes the url. However, the actual component that is rendered on the page is the original component handler from the route match, not the new component handler for the new url.
Here is what my routes.js file looks like
export default function configRoutes(store) {
const authTransition = function authTransition(location, replaceWith) {
const state = store.getState()
const user = state.user
if (!user.isAuthenticated) {
store.dispatch(routeActions.push('/login'))
}
}
return (
<Route component={App}>
<Route path="/" component={Home}/>
<Route path="/login" component={Login}/>
<Route path="/dashboard" component={Dashboard} onEnter={authTransition}/>
<Route path="/workouts" component={Workout} onEnter={authTransition}>
<IndexRoute component={WorkoutsView}/>
<Route path="/workouts/create" component={WorkoutCreate}/>
</Route>
</Route>
)
}
Here is my Root.js component that gets inserted into the DOM
export default class Root extends React.Component {
render() {
const { store, history } = this.props
const routes = configRoutes(store)
return (
<Provider store={store}>
<div>
{isDev ? <DevTools /> : null}
<Router history={history} children={routes} />
</div>
</Provider>
)
}
}
To clarify, if I go to '/workouts', it will fire the onEnter authTransition hook, dispatch the redux-simple-router push action, change the url to '/login', but will display the Workout component on the page. Looking in Redux DevTools shows that state -> router -> location -> pathname is '/login'.
The state flow is
##INIT
##ROUTER/UPDATE_LOCATION (/workouts)
##ROUTER/UPDATE_LOCATION (/login)
Am I passing the store to the routes incorrectly? I can't figure out why the next Router/Update_Location doesn't work
Turns out you want to use the react-router api (replace), not the redux-simple-router to control your transitions.
const authTransition = function authTransition(nextState, replace, callback) {
const state = store.getState()
const user = state.user
// todo: in react-router 2.0, you can pass a single object to replace :)
if (!user.isAuthenticated) {
replace({ nextPathname: nextState.location.pathname }, '/login', nextState.location.query)
}
callback()
}
Also, be careful. I saw a lot of documentation out there for the react-router replace where you pass a single object. That's for react-router 2.0-rc*. If you're using react-router 1.0, you're going to want to pass replace 3 separate arguments.