How to pass props with HOC invocation - reactjs

So here's my issue. I'm using HOC inside my React Router. So it looks like that:
<BrowserRouter>
<div className="main__container">
<Route exact path="/" component={authHOC(MainView)} />
<Route path="/article" component={authHOC(Article)} />
<Route path="/apps" component={authHOC(AppList)} />
</div>
</BrowserRouter>
Now, I'd like to pass some props to my wrapped components. I'd like to have something like this:
component={authHOC(Article({ header: true })}
so to pass props to my component. Above won't work. Is there a way to pass it?
My HOC component looks like this:
export default function(ProtectedComponent, addProps) {
return class LoginPage extends Component {
checkUserAuth = async() => {
const token = await api.getAuthToken();
this.setState({ isUserLoggedIn: !!token, loading: false });
};
redirectToAuth = () => <Redirect to="/login" />;
render() {
const { isUserLoggedIn, loading } = this.state;
if(!loading) {
return isUserLoggedIn ? (
<ProtectedComponent {...this.props} {...addProps} />
) : this.redirectToAuth();
}
return null;
}
};
}

Components shouldn't be called directly like Article({ header: true }), unless this is done on purpose.
A higher-order component can accept a component and additional arguments that are used in wrapped component, as shown in the guide, e.g.:
<Route exact path="/" component={authHOC(MainView, { header: true })} />
and
const authHOC = (Comp, props) => <Comp {...props}/>;
In case authHOC is third-party HOC that cannot be modified, it should be provided with enhanced component:
<Route exact path="/" component={
authHOC(props => <MainView {...props} header={true} />)
} />

Related

React Testing Library Invariant failed: You should not use <Route> outside a <Router>

I'm testing if my components render with Redux successfully with React Testing Library. I'm having trouble having my utility component to pass the renderWithRedux test. This is my App component.
function App() {
return (
<>
<Router>
<NavBar />
<div className="container">
<Switch>
<Route exact path='/' component={Home}/>
<AuthRoute exact path='/login' component={Login} />
<AuthRoute exact path='/signup' component={Signup} />
<Route exact path='/users/:handle' component={UserProfile} />
<Route exact path='/users/:handle/post/:postId' component={UserProfile} />
</Switch>
</div>
</Router>
</>
);
}
Here is my AuthRoute utility component.
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
// if authenticated, redirect to homepage, otherwise redirect to signup or login
<Route
{...rest}
render={(props) =>
authenticated === true ? <Redirect to='/' /> : <Component {...props} />
}
/>
);
AuthRoute.test.js
const renderWithRedux = () => render(
<Provider store={myStore}>
<AuthRoute />
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(<AuthRoute />);
});
I've attempted the solutions from Invariant failed: You should not use <Route> outside a <Router>, but to no avail. I appreciate any help, thank you.
Render the component under test into a router
import { MemoryRouter } from 'react-router-dom';
const renderWithRedux = ({ children }) => render(
<Provider store={myStore}>
{children}
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(
<MemoryRouter>
<AuthRoute />
</MemoryRouter>
);
});
Just like the Provider to wrap redux things you have to wrap your components with routes using MemoryRouter for the tests.
import { MemoryRouter } from 'react-router';
Basically, you have two wrapper elements. It should go something like this, for example, renderWithReduxWrapp => renderWithRouter => YourTestingComponent.
I had a similar issue when trying to test Button render (which has a Link) depending on props, and was able to solve it by creating some helper functions.
Here is the example:
This is the main component, UserCard.js, which renders user data from redux, and only shows a button if withButton props is passed.
import React from "react";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
const CardComponent = ({ withButton }) => {
const userInfo = useSelector((state) => getUserSelector(state));
return (
<div>
<div>{userInfo}</div>
{withButton && (
<Link to="/settings" className="button-link">
<Button block>EDIT CONTACT INFO</Button>
</Link>
)}
</div>
);
};
export default CardComponent;
This is a CardComponent.test.js file.
First, you need to add these lines of code
const ReduxWrapper = ({ children }) => {
<Provider store={store}>{children} </Provider>;
}
const AppWrapper = ({ children }) => (
<BrowserRouter>
<ReduxWrapper>{children}</ReduxWrapper>
</BrowserRouter>
);
const renderWithRouter = (ui, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return render(ui, { wrapper: AppWrapper });
};
After that, you need to start your test with renderWithRouter instead of just render method.
it('should render settings button if prop withButton is passed', () => {
renderWithRouter(<CardComponent withButton />, { wrapper: ReduxWrapper });
// apply you code here. I only needed to check if the button is renederd or not.
const settingsButton = screen.queryByText(/edit contact info/i);
expect(settingsButton).toBeInTheDocument();
});

React wrapper to return child on <Route> component

Is it possible in React to conditionally render a inside a using this kind of component?
import PropTypes from 'prop-types'
import { useApplication } from 'ice-components'
const canEdit = (currentUser) => {
if (currentUser.settings.canEditSpecRec.value === 'False' || !currentUser.location.isSpecimenReceptionType) {
return false
}
return true
}
const Permissions = ({ children }) => {
const { currentUser } = useApplication()
if (!canEdit(currentUser)) {
return null
}
return children
}
Permissions.propTypes = {
children: PropTypes.node,
}
export { Permissions as default, canEdit }
And the route is made in this way:
<Switch>
<Route exact path component={component_1} />
<Route path='/anotherPath' component={component_2} />
<Switch>
I tried to wrap the Permissions component around a single Route component but it breaks. Any ideas?
A good way to conditionally render a Route is the one provided by the section Auth of the react-router documentation:
https://reacttraining.com/react-router/web/example/auth-workflow
What's different in your application is that you're not authorizing based on authentication but on some user permission, you could do something like this:
function PrivateRoute({ children, ...rest }) {
return (
<Route
{...rest}
render={({ location }) =>
canEdit(currentUser) ? (
children
) : (
<Redirect
to={{
pathname: "/",
}}
/>
)
}
/>
);
}
Wrapping the Route in a PrivateRoute component that will return your route if user has permission to or something else if not (maybe a redirect).
export default function AuthExample() {
return (
<Router>
<div>
<Switch>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<PrivateRoute path="/protected">
<ProtectedPage />
</PrivateRoute>
</Switch>
</div>
</Router>
);
}

How to set global prop in react apollo client

I am working with react and react-apollo client.
Here is my main route file
const PrivateRoute = ({ component, isAuthed, ...rest }) => {
console.log({ isAuthed })
return (
<Route {...rest} exact
render = {(props) => (
isAuthed ? (
<div>
{React.createElement(component, props)}
</div>
) :
(
<Redirect
to={{
pathname: '/login',
state: { from: props.location }
}}
/>
)
)}
/>
)
}
class App extends Component {
state = {
isAuthed: false
}
async componentWillMount() {
this.setState({ isAuthed: true })
}
render() {
const { isAuthed } = this.state
return (
<div style={{ direction: direction }}>
<Header {...this.props} history={history}/>
<Router history={history}>
<Switch>
<Route exact path="/" render={() => <Redirect to="/dashboard" />} />
<Route path="/login" component={Login}/>
<PrivateRoute isAuthed={isAuthed} path="/dashboard" component={Dashboard} />
<PrivateRoute isAuthed={isAuthed} path="/AdminManagement" component={Admin} />
</Switch>
</Router>
</div>
)
}
}
export default compose(
graphql(SET_SESSION, { name: 'setSession' })
)((withNamespaces)('common')(App))
Now when I do login inside the login component I need to set isAuthed to true which is inside my main route file(above one)
How can I do this with react apollo client?
Unfortunately, there's no such thing as a "global" prop in React or Apollo. If you want to achieve something similar to your example (i.e. update the state on the root component from your login component), have you considered passing a method down to said component and firing it when your GraphQL mutation resolves?
I'm going to take a stab at this, but please note this is all pseudo code and just outlines one of the many approaches you could take to address this:
App.js
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
userInfo: {},
authed: false,
}
this.modifyUserInfo = this.modifyUserInfo.bind(this);
}
modifyUserInfo(userInfo) {
this.setState(state => ({
...state,
userInfo,
authed: true,
}))
}
render() {
return (
// Render all your routes and everything...
<LoginComponent loginCb={modifyUserInfo} />
)
}
}
Login Component
const Login = props => {
return (
<Mutation mutation={loginMutation}>
{(login) => {
<form onSubmit={(e) => {
e.preventDefault();
login()
.then(res => {
if (res.userInfo) {
props.loginCb(res.userInfo);
}
})
.catch(err => {
console.log(err)
})
}}>
{/* Add the rest of your login form */}
<button type="submit"/>
</form>
}}
</Mutation>
)
}
Rather than storing your user authentication information in your root state, have you considered using your Apollo Cache and injecting the user information into the relevant components? Like I said, there are many, many different ways to approach this.

high order component throws an invalid react element error (reactjs)

So I've been reading about HOCs lately and decided to use them in my application to pass down authorisation logic to the child components.
I'm trying to render a <Route /> component through the HOC but it logs the error:
Uncaught Error: AuthRoute(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.
Here's the code for the HOC:
const AuthRoute = ({ component: Component }) => {
class AuthComponent extends Component {
// Authorisation logic here
render() {
return (
<Route render={props => <Component {...props}/>} />
)
}
}
return AuthComponent;
};
And then I'm using this HOC as an alias of <Route /> component like this:
<BrowserRouter>
<AuthRoute path="/account" component={PrivateComponent} />
</BrowserRouter>
EDIT:
But this approach works fine:
const AuthRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
checkAuth() ? (<Component {...props}/>) : (<Redirect to={{pathname: '/', state: { from: props.location }}}/>)
)}/>
);
<BrowserRouter>
<AuthRoute path="/account" component={PrivateComponent} />
</BrowserRouter>
You need to review your architecture . Indeed, Your approach is totally contradictory regarding HOC pattern . You may need to do the following instead :
<BrowserRouter>
<Route path="/account" component={AuthRoute(PrivateComponent)} />
</BrowserRouter>
If you agree with this design of calling the HOC, The HOC implementation will be :
const AuthRoute = (Composed) => {
class AuthComponent extends React.Component {
// Authorisation logic here
componentWillMount() {
if (!checkAuth()) {
this.props.history.push({pathname: '/', state: { from: this.props.location }});
}
}
render() {
return (
<Composed {...this.props} />
)
}
}
return AuthComponent;
};
In the first case you are returning a class instance
const AuthRoute = ({ component: Component }) => {
class AuthComponent extends Component {
// Authorisation logic here
render() {
return (
<Route render={props => <Component {...props}/>} />
)
}
}
return AuthComponent; // returning the class object here and not an instance
};
So if your wish to use it you would need to write
<BrowserRouter>
<Route path="/account" component={AuthRoute(PrivateComponent)} />
</BrowserRouter>
where AuthRoute(PrivateComponent) is a class object and Route creates an instance out of it internally
However in the second case, its not an HOC, but a functional component that returns a valid React Element,
const AuthRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
checkAuth() ? (<Component {...props}/>) : (<Redirect to={{pathname: '/', state: { from: props.location }}}/>)
)}/>
);
and hence
using <AuthRoute path="/account" component={PrivateComponent} /> , you called a component instance whereby props path and component are received by the functional component.
I’m not very familiar with the syntax you wrote. But spotted one thing. Your parameter is component but inside you are using Component which in this context is undefined
It should be :
const AuthRoute = (Component ) => { // <-- Component which refers to PrivateComponent
class AuthComponent extends React.Component { // <-- React.Component
....
and Not :
const AuthRoute = ({ component: Component }) => {
class AuthComponent extends Component {
...
Also check out my other answer to have elegant HOC .

React router v4 with redux protected routes and auth

I'm using react router v4 with redux and i want to make use of private and protected route to redirect if user is not logged in.
i have this Routes component:
class Routes extends Component {
render() {
const { auth } = this.props;
// so checks against isAuthenticated can pass
if (auth.isAuthenticated !== undefined) {
return (
<div>
<Route exact component={() => <Home />} />
<PublicRoute
authed={() => auth.isAuthenticated}
path="/login"
component={(routerProps) => <Login {...routerProps} />}
/>
<PublicRoute
authed={() => auth.isAuthenticated}
path="/register"
component={(routerProps) => <Register {...routerProps} />}
/>
<PrivateRoute
authed={() => auth.isAuthenticated}
path="/dashboard"
component={Dashboard}
/>
</div>
);
} else {
return <div></div>
}
}
}
function mapStateToProps(state) {
return {
auth: state.auth
}
}
export default withRouter(connect(mapStateToProps)(Routes));
it is implemented like this:
class Main extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
store.dispatch(checkAuth());
}
render() {
return (
<Provider store={store}>
<Router>
<Theme>
<Routes />
</Theme>
</Router>
</Provider>
);
}
}
This is the PrivateRoute:
export function PrivateRoute({ component: Component, authed, ...rest }) {
const isAuthenticated = authed();
return (
<Route
{...rest}
render={props =>
isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
What is the best way to pass that auth prop, is it ok if i connect the Routes component and pass the auth from there, or should i be passed from somewhere else, maybe connect each component that needs it and read from there? Also how would this approach play with nested routes, like /dashboard/settings? Thanks
Actually, it is ok to use this type of private route in react, but you should check two moments:
I should check, that you do not have exact attribute, so all your routes like /dashboard/panel1, /dashboard/panel2 will be private to
auth.isAuthenticated}
path="/dashboard"
component={Dashboard}
/>
You will have some problem with connect. There is a simple fix for that:
export default connect(mapStateToProps, null, null, {
pure: false,
})(PrivateRoute);
more information here:
React router private routes / redirect not working

Resources