I am trying to build a Higher Order Component that would be used to protect routes.It would check (via a network call, not from cache) for the current user, and if not found, would redirect to /login.
Any ideas on how to do so?
First understand how React HOC works. I'll try to answer this using the pseudo code.
HOC.jsx
export default (Component) => class HOC extends React.Component {
constructor(){
super()
this.state = {
waiting: true,
}
}
componentDidMount() {
this.props.actions.validateUser() // your network call to validate user
.then(() => {
// do your stuff
this.setState({
waiting: false,
})
})
.catch(() => {
// handle redirect using react-router
})
}
render() {
if(this.state.waiting) {
// Add a loader here
return <h1>Loading...</h1>
}
return <Component {...this.props} />
}
}
Component.jsx
import HOC from './HOC.jsx'
class Comp extends React.Component {
render() {
return <h1>I'm a valid user</h1>
}
}
export default HOC(Comp)
When any component uses the HOC, the network call will be executed on mount (can be moved to componentWillMount as well) and based on the response the component will be rendered.
Hope this helps!
Related
I am creating Higher Order Components in React and making sure that user cannot access the protected routes. Everything works fine but I am unsure where to put the code for checking the redux state. Currently, in the code below I have placed all the code in componentDidMount. This is because componentWillMount is deprecated. Is this the best place to check because componentDidMount is triggered after the component has been mounted.
import React, { Component } from 'react';
import { connect } from 'react-redux'
export default function(ComposedComponent) {
class Authenticate extends Component {
componentDidMount() {
if(!this.props.isAuthenticated) {
this.props.history.push('/')
}
}
render() {
return (
<ComposedComponent {...this.props} />
)
}
}
const mapStateToProps = (state) => {
return {
isAuthenticated: state.isAuthenticated
}
}
return connect(mapStateToProps)(Authenticate)
}
Assuming the correct state is available at construction, you can do the redirect in the constructor:
class Authenticate extends Component {
constructor(props) {
super(props);
if(!props.isAuthenticated) {
props.history.push('/')
}
}
...
}
This is the purpose of React Router Redirect component:
render() {
return !this.props.isAuthenticated ? (
<Redirect to="/" />
) : (
<ComposedComponent {...this.props} />
)
}
I'm new to React Router and trying to do a redirect from inside a provider using the new Conext API. basically my provider looks like this.
/* AuthContext.js */
class AuthProvider extends React.Component {
state = { isLoggedIn: false }
constructor() {
super()
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
}
login() {
this.setState({ isLoggedIn: true })
// Need to redirect to Dashboard Here
}
logout() {
this.setState({ isLoggedIn: false })
}
render() {
return (
<AuthContext.Provider
value={{
isLoggedIn: this.state.isLoggedIn,
login: this.login,
logout: this.logout
}}
>
{this.props.children}
</AuthContext.Provider>
)
}
}
const AuthConsumer = AuthContext.Consumer
export { AuthProvider, AuthConsumer }
I've read a lot about how to pass the history object using props and how to use a component but I can't see how these approaches would work here. My context provider sits at the top of the tree so it's not a child of the Router so I can't pass props. It's also not a standard component so I can't just insert a component, unless I've misunderstood something (which is very possible).
Edit: Looks like the way to go is withRouter, but how to export my AuthProvider in the code above so that history.push is available in my login function? As you can see I'm exporting multiple components wrapped in {} so can you wrap one of these in a HOC and do you have to explicitly pass history in or is it always available inside the component that's being wrapped?
use withRouter, sth like this to get access of history.
const AuthButton = withRouter( ({ history }) =>history.push("/"));
Try This:
import { Route } from "react-router-dom";
class AuthProvider extends React.Component {
yourFunction = () => {
doSomeAsyncAction(() =>
this.props.history.push('/dashboard')
)
}
render() {
return (
<div>
<Form onSubmit={ this.yourFunction } />
</div>
)
}
}
export default withRouter(AuthProvider);
Best explanation can be found here: Programmatically navigate using react router
I want to know what's the best way to handle setting a parent's state when the Apollo <Query> component finishes loading? I have an id that I sometimes have to query for. I wonder what's the best way to handle this case?
Currently I have it where the child component will listen for prop changes and if I notice that the prop for the data I'm looking for changes I'll call a function to update the state.
Is there a better way to handle this without needing the child component to listen to updates?
This is a pseudo code of what I'm currently doing
import * as React from 'react';
import { Query } from 'react-apollo';
class FolderFetcher extends React.Component<Props, { id: ?string}> {
constructor(props) {
super(props);
this.state = {
id: props.id
}
}
setId = (id) => {
this.setState({ id });
};
render() {
const { id } = this.props;
return (
<Query skip={...} query={...}>
((data) => {
<ChildComponent id={id} newId={data.id} setId={this.setId} />
})
</Query>
);
}
}
class ChildComponent extends React.Component<Props> {
componentDidUpdate(prevProps) {
if (prevProps.newId !== this.props.newId &&
this.props.id !== this.props.newId) {
this.props.setId(this.props.newId);
}
}
render() {
...
}
}
you can export child as wrapped with HoC from apollo
import { graphql } from 'react-apollo'
class ChildComponent extends React.Component {
// inside props you have now handy `this.props.data` where you can check for `this.props.data.loading == true | false`, initially it's true, so when you will assert for false you have check if the loading was finished.
}
export default graphql(Query)(ChildComponent)
Another option would be to get client manually, and run client.query() which will return promise and you can chain for the then() method.
I have an initial redux state like this:
{
loggedInUserId: null,
comments: []
}
Here's how my React App looks like:
class App extends Component {
componentWillMount() {
this.props.getLoggedInUserId();
}
render() {
return (
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/comments" component={Comments} />
</Switch>
);
}
}
In my App, I dispatch an action getLoggedInUserId() which asynchronously fills the loggedInUserId in the state.
The HomePage is a dumb component showing some text. I start the app (route is now '/'), see the HomePage component, then I navigate to the Comments page, which has:
componentWillMount() {
this.props.fetchComments(this.props.loggedInUserId); // Dispatch action to do API call to fetch user's comments
}
render() {
// Show this.props.comments nicely formatted
}
Everything works, I see the list of comments in the Comments component.
But if I refresh the page on the route /comments, then by the time the Comments runs componentWillMount, the loggedInUserId has not been loaded yet, so it will call fetchComments(null).
Right now, to fix this, I'm doing in my Comments component:
componentWillMount() {
if (!this.props.loggedInUserId) return;
this.props.fetchComments(this.props.loggedInUserId);
}
componentWillReceiveProps(nextProps) {
if (!this.props.loggedInUserId && nextProps.loggedInUserId) {
nextProps.fetchComments(nextProps.loggedInUserId);
}
}
which works well. But I'm doing this in 10+ components, and it seems like a lot of work which can be factorized, but I didn't find an elegant way to do it.
So I'm asking you how do you generally deal with this kind of situation? Any idea is welcome:
HOC
side-effects
other libraries
I'm using wrapper around Route, which checks if users are logged in and if not, redirect them to login page. Wrapped routes are rendered only after userId of authenticated user is fetched.
import * as React from 'react'
import { Route, Redirect } from 'react-router-dom'
import URLSearchParams from 'url-search-params'
class AuthRoute extends React.Component {
componentDidMount() {
if (!this.props.isLoading) {
this.props.getLoggedInUserId()
}
}
render() {
if (this.props.isLoading) {
// first request is fired to fetch authenticated user
return null // or spinner
} else if (this.props.isAuthenticated) {
// user is authenticated
return <Route {...this.props} />
} else {
// invalid user or authentication expired
// redirect to login page and remember original location
const search = new URLSearchParams({
next: this.props.location.pathname,
})
const next =
this.props.location.pathname !== '/' ? `?${search.toString()}` : ''
return <Redirect to={`/login${next}`} />
}
}
}
You need to update your reducer which handle getLoggedInUserId action to store also isLoading state.
You probably want the initial state to be rendered by the server into 'index.html' (or what have you) and hydrated on the client.
This initial state would include loggedInUserId and data for the /comments page.
Check out https://redux.js.org/docs/recipes/ServerRendering.html
I think using HOC will be clean here. As all the common logic will be at the same place. Use composition here
Let say you have components A, B, C, D
Now you want to write some common function on the componentWillReceiveProps lifecycle of all the components.
Write a HOC like:
class HOC extends React.Component {
componentWillReceiveProps(nextProps) {
//Your commomn logic
}
render() {
const childrenWithProps = React.Children.map(this.props.children,
child => React.cloneElement(child, {
...this.props,
})
return (
<div>
{childrenWithProps}
</div>
)
}
}
Write your components like this:
class A extends React.Component {
componentWillReceiveProps(nextProps) {
//your uncommone logic
}
render(){
return (
<HOC {...this.props}>
<div>
//Your page jsx
</div>
</HOC>
)
}
}
same way write for component B, C, and D. This pattern is useful when there is lot common among components. So better have a look at your usecase
OP writing. After reading nice ideas here, I decided to go with a custom HOC:
import React, { Component } from 'react';
const requireProp = (As, propsSelector, propsToDispatch) =>
class Wrapper extends Component {
componentWillMount() {
if (!propsSelector(this.props) && typeof propsToDispatch === 'function') {
propsToDispatch(this.props);
}
}
render() {
const { ...props } = this.props;
return !!propsSelector(this.props) && <As {...props} />;
}
};
export default requireProp;
To see how I use it, see this gist.
In my APP, which is the parent component of my react-router, I have this:
export default class APP extends Component {
static propTypes = {
children: PropTypes.element
};
constructor(props) {
super(props);
this.state = {
clientId: 'A134567897',
..........................
};
}
componentDidCount() {
CompanyInfo.getGenlInfo()
.then((response) => {
const data = response.data;
this.setState(data);
});
}
renderChild = () =>
React.cloneElement(this.props.children, {
// this needs to be passed to all child routes
...this.state
});
render() {
return (
<section>
<Header { ...this.state } />
{/* This is where the dynamic content goes */}
{ React.Children.map(this.props.children, this.renderChild) }
<Footer { ...this.state } />
</section>
);
}
}
In my router, a path of '/' brings up the WelcomePage (main page) component.
The WelcomePage component does appear as expected, and this.props.clientId does have a value, but if I code on the WelcomePage ....
WelcomePage.propTypes = {
clientId: PropTypes.string.isRequired
};
Required prop clientId was not specified in WelcomePage. Check the render method of RoutingContext.
I thought I took care of passing the clientId props to the WelcomePage through the APP's renderChild() method with '...this.state', didn't I?
Does anyone see where the error lies? Again, the clientID value does successfully pass to the WelcomePage, but I receive an error if I make it required on the WelcomePage.
Many Thanks
You can't use .isRequired on propTypes for route components.
This is because React checks propTypes at the time of element creation. React Router will initially create all of the route component elements with just the React Router props; by the time you get to the cloneElement call, this validation has already happened and failed.
We've recently added a note to the upgrade guide detailing this: https://github.com/rackt/react-router/blob/master/UPGRADE_GUIDE.md#routehandler
For more details see: https://github.com/facebook/react/issues/4494#issuecomment-125068868