React Router 4 Match returns undefined - reactjs

I'm using react router 4 and I'm having trouble accessing the id from a url using params. I've followed the react router 4 documentation however when I console.log match.params.id it returns Cannot read property 'params' of undefined. The URL contains the id so I'm lost. You can find the console.log in Path: Container
What am I doing wrong?
Path: App
const App = appProps => (
<Router>
<div className="bgColor">
<NavBar {...appProps} />
<Grid className="main-page-container">
<Switch>
<Admin exact path="/admin/candidate_profile/:id" component={AdminCandidateProfileContainer} {...appProps} />
</Switch>
</Grid>
</div>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
authenticatedCandidate: PropTypes.bool,
authenticatedAdmin: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
authenticatedCandidate: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'Candidate'),
authenticatedAdmin: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'Admin')
};
}, App);
Path: AdminRoute
const Admin = ({ loggingIn, authenticatedAdmin, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticatedAdmin ?
(<Component loggingIn={loggingIn} authenticatedAdmin={authenticatedAdmin} {...rest} />) :
(<Redirect to="/login" />);
}}
/>
);
Admin.propTypes = {
loggingIn: PropTypes.bool,
authenticatedAdmin: PropTypes.bool,
component: PropTypes.func
};
export default Admin;
Path: Container.js
export default CandidateProfileContainer = createContainer(({ match }) => {
console.log('match', match.params.id);
const profileCandidateCollectionHandle = Meteor.subscribe('admin.candidateProfile');
const loading = !profileCandidateCollectionHandle.ready();
const profileCandidateCollection = ProfileCandidate.findOne({ userId: Meteor.userId() });
const profileCandidateCollectionExist = !loading && !!profileCandidateCollection;
return {
loading,
profileCandidateCollection,
profileCandidateCollectionExist,
profileCandidate: profileCandidateCollectionExist ? profileCandidateCollection : {}
};
}, CandidateProfilePage);

You're not passing props from render
const Admin = ({ loggingIn, authenticatedAdmin, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticatedAdmin ?
(<Component
loggingIn={loggingIn}
authenticatedAdmin={authenticatedAdmin}
{...rest}
{...props} <--- match, location are here
/>) :
(<Redirect to="/login" />);
}}
/>
);

Related

No history property in react app when building protected routes

I am using this approach to make some routes in my application only accessible after log in.
So in App.js I have some ProtectedRoutes.
<ProtectedRoute path='/projects' auth={this.props} component={Projects} />
<ProtectedRoute path='/search' auth={this.props} component={Search} />
<ProtectedRoute path='/admin' auth={this.props} component={Admin} />
And my ProtectedRoute function is like this:
const ProtectedRoute = ({ component: Comp, auth, path, ...rest }) => {
const isAuthenticated = auth.isAuthenticated;
const sendprops = { ...auth, ...rest };
return (
<Route
path={path}
{...rest}
render={props => {
return isAuthenticated ? (
<Comp {...sendprops} />
) : (
<Redirect to='/' />
);
}}
/>
);
};
export default ProtectedRoute;
But having done that, I've totally broken my Search component, which relies on being able to see and push to the history property.
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery}&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
When I try to console.log out the properties in the Search component, I can see location still, but I've lost history. So in what feels like a somewhat ham fisted, ignorant fashion, I've wrapped my Search export in withRouter.
export default withRouter(Search);
And now I can see the history again. But I have no idea why I lost it in the first place? Insights would be greatly appreciated.
As requested, here is the Search component.
import React, { Component } from 'react';
import ResultList from './SearchResultList';
import { withRouter } from 'react-router'
class Search extends Component {
constructor(props) {
super(props);
console.log(props);
let url_query = '';
let url_search_type = '';
let url_search_page = 1;
let loading = false;
if(this.props.location.search) {
const params = new URLSearchParams(this.props.location.search);
url_query = params.get('query');
url_search_type = params.get('search_type');
url_search_page = params.get('page') || 1;
loading = true;
}
this.state = {
searchQuery: url_query,
searchType: url_search_type,
searchPage: url_search_page,
searchResult: {
results: [],
pages_left: '',
pages_right: '',
page: '',
type: ''
},
isLoading: loading
}
}
componentDidMount() {
if(this.state.isLoading){
this.doSearch();
}
}
handleChange = (e) => {
const search_name = e.target.name;
const search_value = e.target.value.trim();
this.setState({
[search_name]: search_value,
})
}
handleSubmit = (e) => {
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery }&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
this.setState({
isLoading: true,
searchPage: 1
}, this.doSearch);
}
changePage = (e) => {
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery }&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
this.setState({
searchPage: e,
isLoading: true
}, this.doSearch);
}
doSearch = () => {
//console.log(this.state);
const endpoint = 'http://urlurlurllll/search?query=' + this.state.searchQuery + '&search_type=' + this.state.searchType + '&page=' + this.state.searchPage;
//console.log(endpoint);
fetch(endpoint)
.then(data => data.json())
.then(jdata => {
this.setState({
searchResult: {
results: jdata.results,
pages_left: jdata.pages_left,
pages_right: jdata.pages_right,
page: jdata.page,
type: this.state.searchType
},
isLoading: false
})
})
.catch(error => console.log(error));
}
render (){
const isLoading = this.state.isLoading;
const searchResult = this.state.searchResult;
return (
<React.Fragment>
<div>
<h1>Search</h1>
<form>
<label>Search Term:</label>
<input type="text" id="input-search" name="searchQuery" value={this.state.searchQuery} onChange={ event => this.handleChange(event) } />
<label>Search Type:</label>
<select id="select-type" name="searchType" value={this.state.searchType} onChange={ event => this.handleChange(event) }>
<option value=''>Select Type</option>
<option value='hashtag'>hashtag</option>
<option value='handle'>handle</option>
<option value='keyword'>keyword</option>
</select>
<button type="button" id="btn-search" onClick={event => this.handleSubmit(event)}>Search</button>
</form>
</div>
<div>
{ isLoading ? (
<div>
<p>Loading...</p>
</div>
) : (
<React.Fragment>
<h2>Search Results</h2>
{searchResult.results.length ? (
<ResultList searchResult={searchResult} changePage={this.changePage} />
) : (
<p>Use the little form up there to start searching!</p>
)}
</React.Fragment>
)}
</div>
</React.Fragment>
);
}
}
export default withRouter(Search);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.3.1/umd/react-dom.production.min.js"></script>
Issue
I think the issue is the route props from render aren't passed along to the component. (Though it is unclear to me how the location route prop was still working)
Solution
Spread the route props (match, location, and history) provided from render into the component being rendered.
const ProtectedRoute = ({ component: Comp, auth, path, ...rest }) => {
const isAuthenticated = auth.isAuthenticated;
const sendprops = { ...auth, ...rest };
return (
<Route
path={path}
{...rest}
render={(props) => // <-- route props need to be
isAuthenticated ? (
<Comp {...sendprops} {...props} /> // <-- passed to component
) : (
<Redirect to="/" />
)
}
/>
);
};
I just checked that tutorial/lesson you linked to see what they were doing and they pass the route props through as well.

Prevent user from directly accessing URL in React application?

Is there a way to stop the user from directly accessing a URL on my application? For example, we have a page that is accessed as localhost:3000/scheduling but I want to re-route back to the homepage. I couldn't find many helpful articles that could achieve this. I am using React by the way.
Thanks!
You can do it in many ways, this is just an example :
const location = useLocation();
let history = useHistory();
if(location.state == undefined || location.state == null || location.state == ''){
history.push("/");
}
'/' is by default your home page.
You can check this example:
import React from 'react'
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from 'react-router-dom'
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true
setTimeout(cb, 100)
},
signout(cb) {
this.isAuthenticated = false
setTimeout(cb, 100)
}
}
const Public = () => <h3>Public</h3>
const Protected = () => <h3>Protected</h3>
class Login extends React.Component {
state = {
redirectToReferrer: false
}
login = () => {
fakeAuth.authenticate(() => {
this.setState(() => ({
redirectToReferrer: true
}))
})
}
render() {
const { from } = this.props.location.state || { from: { pathname: '/' } }
const { redirectToReferrer } = this.state
if (redirectToReferrer === true) {
return <Redirect to={from} />
}
return (
<div>
<p>You must log in to view the page</p>
<button onClick={this.login}>Log in</button>
</div>
)
}
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
fakeAuth.isAuthenticated === true
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
export default function AuthExample () {
return (
<Router>
<div>
<ul>
<li><Link to="/public">Public Page</Link></li>
<li><Link to="/protected">Protected Page</Link></li>
</ul>
<Route path="/public" component={Public}/>
<Route path="/login" component={Login}/>
<PrivateRoute path='/protected' component={Protected} />
</div>
</Router>
)
}
Source
We can use Conditional rendering tracing the history.
You can also add conditions using this.props.history.location.key or this.props.history.action
Key exists and action is 'PUSH' when we redirect user using this.props.history.push
Key property doesn't exist and action is 'POP' when a user tries to access the URL directly
return this.props.history.location.key ? (<div></div>) : null

on BackButton: Component does not update although url is changed

When clicking on the back button , the url gets changed, but the component that should be rendered for this url, is not rendered. Rerendering seems to be blocked. I am using connected-react-router. Before implementing connected-react-router, the back button worked fine. I tried a suggested solution , wrapping my connect function with the redux store with withRouter, but still see no result.
index.js
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'),
);
App.js
class App extends Component {
componentDidMount() {
const { dispatch } = this.props;
dispatch(getInitialBookData());
}
render() {
return (
<div>
<Navigation />
</div>
);
}
}
export default connect()(App);
Navigation.js
const Navigation = () => {
return (
<Fragment>
<Navbar color="light" light expand="md">
<Collapse className="navbar-collapse">
<Nav className="ml-sm-auto navbar-nav">
<NavItem className="p-2">
<NavLink activeClassName="active" to="/">Dashboard</NavLink>
</NavItem>
<NavItem className="p-2">
<NavLink activeClassName="active" to="/addbook">Add Book</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>
<Container className="content mt-8">
<Switch>
<Route exact path="/" component={Dashboard} />
<Route exact path="/editbook/:id" component={CreateEditBookContainer} />
<Route exact path="/addbook" component={CreateEditBookContainer} />
<Route component={Page404} />
</Switch>
</Container>
</Fragment>
);
};
Dashboard.js
const Dashboard = ({ loading, error, success }) => {
return (
<Fragment>
{ error.status === true
? <ErrorAlert message={error.message} />
: null }
{ success.status === true
? <SuccessAlert message={success.message} />
: null }
<Row className="mt-3">
<h2 className="mb-5">Welcome to Dashboard</h2>
{ loading === true
? null
: <BookListContainer /> }
</Row>
</Fragment>
);
};
const mapStateToProps = ({fetching, errors, success}) => {
return {
loading: fetching.status,
error: errors,
success: success,
};
}
Dashboard.propTypes = {
loading: PropTypes.bool.isRequired,
error: PropTypes.object.isRequired,
success: PropTypes.object.isRequired,
};
export default withRouter(connect(mapStateToProps)(Dashboard));
BookListContainer.js
class BookListContainer extends Component{
state = {
navigateToEditBook: false,
};
idBookToEdit = null;
goToEditComponent = (id) => {
this.idBookToEdit = id;
this.setState({ navigateToEditBook: true });
}
render() {
let idBookToEdit = this.idBookToEdit;
if (this.state.navigateToEditBook === true) {
return <Redirect push to={{
pathname:`/editBook/${idBookToEdit}`,
state: { referrer: '/', bookId: idBookToEdit }
}} />
}
const { books } = this.props;
return(
<Row>
{ books.map((book) => {
return (
<Book
key={book._id}
book={book}
handleEditBook={this.goToEditComponent}
/>
)
}) }
</Row>
)};
};
const mapStateToProps = (props) => {
return {
books: props.books.books,
location: props.router.location,
};
}
BookListContainer.propTypes = {
books: PropTypes.array.isRequired,
};
export default withRouter((connect(mapStateToProps)(BookListContainer)));
CreateEditBookContainer.js
class CreateEditBookContainer extends Component {
render() {
const bookId = this.props.location.state
? this.props.location.state.bookId
:null
const { books, error, success } = this.props;
let book = null;
if(bookId){
book = books.filter(book => book._id === bookId)
}
return(
<Col sm="12" md="6" className="m-auto pt-5">
<CreateEditBookForm
{ ...book ? book = { ...book[0] } : null }
/>
{ error.status === true
? <ErrorAlert message={error.message} />
: null }
{ success.status === true
? <SuccessAlert message={success.message} />
: null }
</Col>
)}
}
const mapStateToProps = ({ books, errors, success }) => {
return {
books: books.books,
error: errors,
success: success,
}
}
CreateEditBookContainer.propTypes = {
books: PropTypes.array.isRequired,
error: PropTypes.object.isRequired,
success: PropTypes.object.isRequired,
};
export default withRouter(connect(mapStateToProps)(CreateEditBookContainer));
When clicking on EditBook Button, CreateEditBookContainer.js gets correctly rendered, but when clicking on the back button, this components remains, while the Dashboard component should actually get rendered since it corresponds to the path '/', that is correctly set in the url. When clicking on the Route links in specified in Navigation, all components are rendered correctly. It ONLY fails on the back button. Thank you for any suggestions.

Update context value after API call

I'm using React 16.3 Context API, I'm setting loggedin: bool & user: Object value using context, also using PrivateRoute for logged in user.
Here is a brief code.
// AuthContext JS
import React from "react";
const AuthContext = React.createContext();
class AuthProvider extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
user : null
};
this.setAuth = this.setAuth.bind(this);
};
setAuth(isLoggedIn = false, userData = null) {
this.setState({
isLoggedIn: isLoggedIn,
user : userData
});
}
render() {
return (
<AuthContext.Provider
value={ {...this.state, setAuth: this.setAuth} }>
{ this.props.children }
</AuthContext.Provider>
);
}
}
const AuthUser = AuthContext.Consumer;
export {AuthContext, AuthProvider, AuthUser};
function PrivateRoute({component: Component, ...rest}) {
return (
<AuthUser>
{
({isLoggedIn}) => (
<Route
{ ...rest }
render={ props =>
(
isLoggedIn ? (
<Component { ...props } />
) : (
<Redirect
to={ {
pathname: "/login",
state : {from: props.location}
} }
/>
)
)
}
/>
)
}
</AuthUser>
);
}
// App JS
class App extends Component {
render() {
return (
<HashRouter>
<AuthProvider>
<Switch>
<Route exact path="/login" name="Login Page" component={ Login } />
<Route exact path="/register" name="Register Page" component={ Register } />
<Route exact path="/404" name="Page 404" component={ Page404 } />
<Route exact path="/500" name="Page 500" component={ Page500 } />
<PrivateRoute path="/" component={ DefaultLayout } />
</Switch>
</AuthProvider>
</HashRouter>
);
}
}
export default App;
// Login JS
class Login extends Component {
handleSubmit(values) {
const opts = {
"email" : "test#example.com",
"password": "test123"
};
let _this = this;
fetch("API_URL", {
method: "post",
body : JSON.stringify(opts)
})
.then(
(response) => {
return response.json();
}
).then(
(data) => {
_this.setState({
isAuth: true,
user : data.data.user
});
_this.props.history.replace("/dashboard");
}
);
}
render() {
console.log(this.state.isAuth);
return (
<AuthUser>
{
({isLoggedIn, setAuth}) =>
(
<Redirect to="/dashboard" />
) : ( <div > // Login Page </div>
)
}
</AuthUser>
);
}
}
How do I update/call setAuth function of consumer
If I call setAuth from render function, it will give warning & loop over setState
Any Help!
In the handleSubmit function in the Login file, instead of calling
this.setState({
isAuth: true,
user: data.data.user
});
you should call the setAuth function provided by the context and update the user auth and data in the context there:
this.context.setAuth(true, data.data.user)
In order to use this.context, you may need to change from using context consumer to contextType:
static contextType = AuthContext
You have implement a higher order component that help component consume context value as props.
The following withContextAsProps HOC provides an example:
function withContextAsProps(Context, Component) {
function WithContextAsProps(prop) {
return (
<Context>
{value => <Component {...value} />}
</Context>
);
}
const componentName = Component.displayName || Component.name || 'Component';
const contextName = Context.displayName || Context.name || 'Context';
WithContextAsProps.displayName = `With${contextName}Context(${componentName})`;
return WithContextAsProps;
}
In Login component, the HOC can be used to make isAuth and setAuth value from AuthUser context consumer available as props in the Login component.
class Login extends Component {
handleSubmit = values => {
//...
fetch("API_URL", {
method: "post",
body : JSON.stringify(opts)
})
.then(response => response.json())
.then(
data => {
this.props.setAuth(true, data.data.user);
this.props.location.assign("/dashboard");
}
);
}
render() {
return this.props.isAuth ?
<Redirect to="/dashboard" />
: <div>Login Page</div>;
}
}
export default withContextAsProps(AuthUser, Login);

Building Membership based website with React

I want to build a membership-based web app using React. The users would sign-up and pay before they can access exclusive content. If they have already registered or signed up they can just login and access the content.
How would I go about making such a web app with React? What would I need to implement?
You can use react-router, found in npm packages : https://www.npmjs.com/package/react-router
See this example to private routes https://reacttraining.com/react-router/web/example/auth-workflow
import React from "react";
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from "react-router-dom";
////////////////////////////////////////////////////////////
// 1. Click the public page
// 2. Click the protected page
// 3. Log in
// 4. Click the back button, note the URL each time
const AuthExample = () => (
<Router>
<div>
<AuthButton />
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
<Route path="/public" component={Public} />
<Route path="/login" component={Login} />
<PrivateRoute path="/protected" component={Protected} />
</div>
</Router>
);
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
this.isAuthenticated = false;
setTimeout(cb, 100);
}
};
const AuthButton = withRouter(
({ history }) =>
fakeAuth.isAuthenticated ? (
<p>
Welcome!{" "}
<button
onClick={() => {
fakeAuth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
)
);
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
const Public = () => <h3>Public</h3>;
const Protected = () => <h3>Protected</h3>;
class Login extends React.Component {
state = {
redirectToReferrer: false
};
login = () => {
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};
render() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const { redirectToReferrer } = this.state;
if (redirectToReferrer) {
return <Redirect to={from} />;
}
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={this.login}>Log in</button>
</div>
);
}
}
export default AuthExample;

Resources