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:
Related
I'm working on the application with React + Firebase. The app requires authentication that users accesses to their own timeline. So I want the app separated between LoggedIn screens and LoggedOut screens.
In App.js, I set up <Auth /> to the screens require authentication. Like this:
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<Switch>
<Auth>
<Route path="/timeline" component={Timeline} />
</Auth>
</Switch>
<Route path="/" component={Home} />
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
export default App;
And added Auth.
import React from "react";
import { Redirect } from "react-router-dom";
import firebase from "../config/firebase";
class Auth extends React.Component {
constructor(props) {
super(props);
this.state = {
user: false
};
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState({ user });
}
});
}
render() {
if (!this.state.user) {
return (
<div>{this.state.user ? this.props.children : <Redirect to="/" />}</div>
);
}
}
}
export default Auth;
But it is not working. It has no error message but I can't access any components...
Router Issue
As mentioned in this thread, nested Switch components is not the intended way that react-router was designed and should probably be avoided.
I think you have overcomplicated the App component here. I would recommend something more like the below example because your routing logic doesn't need to care about authentication. You can wrap the Timeline component with the Auth component inside the implementation of Timeline, and then the router just does the routing, nothing else.
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<Route path="/timeline" component={Timeline} />
<Route path="/" component={Home} />
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
export default App;
An alternative, as mentioned in the linked thread above, is to write your own Switch logic. This might allow you to keep all of the authenticated routes all visible upfront, but you may not want to get into messing around with that.
You need an 'authenticating' state
As touched on in Soroush's answer, you also have an issue with the state management in the Auth component. At the moment, it goes like this:
Component renders with user = false
Redirect component is rendered and page redirects to '/'
Do you see how there is never an opportunity to render the child of auth properly for an authenticated user because the initial render will always trigger the redirect? Firebase never has a chance to log the user in before they are redirected.
To fix this, you Auth to load the page into some sort of 'authenticating' state and only redirect the user after you know whether they are logged in or not. Remember, onAuthStateChanged will only ever be triggered after the initial render.
Good luck!
In this case and your authentication structure I recommended to you using programmatically navigate.
import { withRouter } from "react-router";
class Auth extends React.Component {
constructor(props) {
super(props);
this.state = {
user: false,
authenticating: true,
};
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (!user) {
this.props.history.push('/login');
} else {
this.setState({ authenticating: false });
}
});
}
render() {
return (
<div>{authenticating ? <p>Authenticating...</p> : this.props.children}</div>
);
}
}
export default withRouter(Auth);
I am using a similar code like this to redirect in my app after users logged in. The code looks like the following:
import React, { Component } from 'react'
import { Redirect } from 'react-router'
export default class LoginForm extends Component {
constructor () {
super();
this.state = {
fireRedirect: false
}
}
submitForm = (e) => {
e.preventDefault()
//if login success
this.setState({ fireRedirect: true })
}
render () {
const { from } = this.props.location.state || '/'
const { fireRedirect } = this.state
return (
<div>
<form onSubmit={this.submitForm}>
<button type="submit">Submit</button>
</form>
{fireRedirect && (
<Redirect to={from || '/home'}/>
)}
</div>
)
}
}
Works fine when a successful login has been triggered. But there is the case, that logged in users enter the login page and should be automatically redirected to the "home" page (or whatever other page).
How can I use the Redirect component without rendering the current component and without (as far as I understand discouraged) imperative pushing to the history (e.g. in componentWillMount)?
Solution 1
You could use withRouter HOC to access history via props.
Import withRouter.
import {
withRouter
} from 'react-router-dom';
Then wrap with HOC.
// Example code
export default withRouter(connect(...))(Component)
Now you can access this.props.history. For example use it with componentDidMount().
componentDidMount() {
const { history } = this.props;
if (this.props.authenticated) {
history.push('/private-route');
}
}
Solution 2 Much better
Here is example on reacttraining.
Which would perfectly work for you.
But you just need to create LoginRoute to handle problem you described.
const LoginRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Redirect to={{
pathname: '/private-route',
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
and inside <Router /> just replace
<Route path="/login" component={Login}/>
with
<LoginRoute path="/login" component={Login}/>
Now everytime somebody will try to access /login route as authenticated user, he will be redirected to /private-route. It's even better solution because it doesn't mount your LoginComponent if condition isn't met.
Here is another solution which doesn't touch React stuff at all. E.g. if you need to navigate inside redux-saga.
Have file history.js:
import {createBrowserHistory} from 'history';
export default createBrowserHistory();
Somewhere where you define routes, don't use browser router but just general <Router/>:
import history from 'utils/history';
...
<Router history={history}>
<Route path="/" component={App}/>
</Router>
That's it. Now you can use same history import and push new route.
In any part of your app:
import history from 'utils/history';
history.push('/foo');
In saga:
import {call} from 'redux-saga/effects';
import history from 'utils/history';
...
history.push('/foo');
yield call(history.push, '/foo');
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 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
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.