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.
Related
I am trying to create a filter feature in my Reactjs app (which also includes some Redux). I am a newbie so I'm still trying to get the hang of ReactJs.I want to be able to search through my list of matters, return either the original list, or the resulted search. My matter list is inside my MattersContainer.js.
In my app, I have my SearchBar.js as child component of App.js.
class App extends React.Component {
state = {
filteredMatter: [],
}
componentDidMount() {
this.props.fetchMatters();
}
onSearchSubmit = (searchTerm) => {
const filterResults = this.props.matters.filter(matter => matter.case_title.toLowerCase()
=== searchTerm.toLowerCase())
this.setState({filteredMatter: filterResults})
}
render() {
return (
<React.Fragment>
<Router>
<NavBar />
<SearchBar onSubmit={this.onSearchSubmit}/>
<Sidebar />
<Switch>
<div className="matters-container">
<Route exact path='/' component={Home} />
<Route exact path="/matters">
<MattersContainer matters={this.state.filteredMatter.length > 0 ? this.state.filteredMatter : this.props.matters}/>
</Route>
<Route exact path="/matters/new" render={(routerProps) => <MatterForm {...routerProps} />} />
<Route exact path="/matters/:id" render={(routerProps) => {
const matterId = parseInt(routerProps.match.params.id)
// debugger
const matterObj = this.props.matters.find(matterArrObj => matterArrObj.id === matterId)
if (matterObj) {
return (
<Matter key={matterObj.id}
matters={matterObj}
{...routerProps}
/>
)
}
}
}/>
<Route path='/tasks' component={TasksContainer} />
</div>
</Switch>
</Router>
</React.Fragment>
) } }
const mapStateToProps = state => {
return ({
matters: state.matterReducer.matters
})
}
export default connect(mapStateToProps, {fetchMatters})(App)`
> My SearchBar.js looks like this:
import React, { Component } from 'react'
export default class SearchBar extends Component {
constructor() {
super()
this.state = {
searchTerm: ''
}
}
onInputChange = (event) => {
this.setState({
searchTerm: event.target.value
})
}
onFormSubmit = (event) => {
debugger
event.preventDefault();
this.props.onSubmit(this.state.searchTerm)
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<input
type="text"
value={this.state.searchTerm}
onChange={this.onInputChange}
/>
<button type="Submit">Search for a matter</button>
</form>
</div>
)
}
}
This is my MattersContainer:
class MattersContainer extends Component {
render() {
return (
<>
<div className="matter" >
<h3>Your Matters</h3>
<Paper style={{ overflow:'hidden',margin: '5px', display: 'flex',justifyContent: 'space-between' }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Matter</TableCell>
<TableCell>Client</TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.props.mattersArr.map(matter => (
<TableRow key={matter.id} >
<TableCell>
<Link to={`/matters/${matter.id}`}>{matter.case_title} </Link>
</TableCell>
<TableCell>
{matter.client}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
</div>
</>
)
}
}
const mSTP = state => {
return {mattersArr: state.matterReducer.matters};
}
const mDTP = dispatch => ({
deleteMatter: id => dispatch({type: "DELETE_MATTER", id})
});
export default connect(mSTP, mDTP)(MattersContainer);
If you don't use Redux to dispatch state, you must be initing state searchTemp in App.js
this.state = {
searchTemp: null,
}
After that you create functions set state searchTemp:
const setSearchTemp =(searchKey)=>{
this.setState({ searchTemp: searchKey });
}
And send functions setSearchTemp to component SearchBar to get keyword filter
I'm trying to complete my app, have learned react, redux, react router all in one, now I'm just confused a bit when it comes to putting it all together.
Say I have a Nav component that's included in a header that's included globally on all pages and it calls a redux action which then runs a reducer and returns some search results.
When one searches from the navigation bar, how do I get it to redirect a search page that then returns the search results?
Nav component
class Nav extends React.Component {
render() {
const { search } = this.props;
return (
<header>
<SearchBox search={search} />
</header>
)
}
}
that includes a search component
class SearchBox extends React.Component {
constructor() {
super();
this.state = {
name: ''
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
}
handleSubmit = event => {
event.preventDefault();
this.props.search(JSON.stringify({name: this.state.name}))
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" id="name" onChange={this.handleChange} placeholder="Search" />
<button type="submit">Search</button>
</form>
)
}
}
my layouts are like
index.js
const Index = () => {
return (
<Nav />
... Home Content
)
}
profile.js
const Profile = () => {
return (
<Nav />
... Profile Content
)
}
search.js
const Users = (props) => {
let list = null;
list = props.users.map((user, index)=> {
const { name } = user.profile;
const { username } = user;
return (
<li key={index}>
<h3><a href={'/'+username}>{name}</a></h3>
</li>
)
});
return <section id="search"><ul>{list}</ul></section>;
}
class Search extends React.Component {
render() {
const { searchResults } = this.props;
return (
<Nav />
<div>
{
/* show 'No results when no results found' */
searchResults !== ''
? seachResults.length == 0
? 'No results found'
: <Users users={searchResults} />
: null
}
</div>
)
}
}
const mapStateToProps = state => ({
searchResults: state.store.searchResults,
});
the user action is
export const search = (name) => dispatch => {
...
dispatch({
type: SEARCH_USER,
payload: res
})
the reducer is
const initialState = {
searchResults: ''
};
case SEARCH_USER:
return {
...state,
searchResults: action.payload.search
}
}
index.js
class App extends React.Component {
render() {
return (
<Router>
<Switch>
<Route path="/" exact={true} component={Index} />
<Route path="/profile" component={Profile} />
<Route path="/search" component={Search} />
</Switch>
</Router>
)
}
}
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;
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" />);
}}
/>
);
I've created a login page that takes a user from public to authenticated routes which works well. If there is an error with login (eg. email not found) I get the error in console Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the LoginForm component..
I think it might be related to how I use createContainer in the App.
I believe the problem is related to the way meteor passes Meteor.loggingIn(); equals true before it has heard back from the server. If there is an error the true quickly changes to false, which I think leads to the page reloading.
I would like to be able to use this.setState({ loginError: error.reason }); so I can tell the user what went wrong.
Any suggestions.
Path: App.jsx
const App = appProps => (
<Router>
<Grid className="main-page-container">
<Switch>
<Authenticated exact path="/" component={Home} {...appProps} />
<Public exact path="/login" component={Login} {...appProps} />
</Switch>
</Grid>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
authenticated: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
authenticated: !loggingIn && !!Meteor.userId()
};
}, App);
Path: Public.jsx
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return !authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/" />);
}}
/>
);
Public.propTypes = {
loggingIn: PropTypes.bool,
authenticated: PropTypes.bool,
component: PropTypes.func
};
export default Public;
Path: LoginForm.jsx
export default class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
errors: {},
password: '',
loginError: ''
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox'
? target.checked
: target.value;
const name = target.name;
this.setState({[name]: value});
}
handleSubmit(event) {
event.preventDefault();
this.setState({
errors: {}
}, function() {
var data = {
email: this.state.email,
password: this.state.password
};
var email = this.state.email;
var password = this.state.password;
const errors = loginUserValidation(data);
if (errors) {
this.setState({errors: errors});
} else {
Meteor.loginWithPassword(email, password, (error) => {
if (error) {
this.setState({ loginError: error.reason });
}
});
}
});
}
render() {
return (
<div className="registration-form-container">
<Row>
<Col sm={8} smOffset={2} md={6} mdOffset={3}>
<div className="paper">
<Form onSubmit={this.handleSubmit}>
<section className="form-title">
<h3 className="text-center">Login</h3>
</section>
<hr />
<section className="form-content-login-or-registration">
{this.state.loginError &&
<div className="alert alert-danger">
<p>{this.state.loginError}</p>
</div>
}
<SingleInput
name={'email'}
inputType={'email'}
controlFunc={this.handleInputChange}
content={this.state.email}
placeholder={'Email'}
bsSize={null}
error={this.state.errors && this.state.errors.email}
/>
<SingleInput
name={'password'}
inputType={'password'}
controlFunc={this.handleInputChange}
content={this.state.password}
placeholder={'Password'}
bsSize={null}
error={this.state.errors && this.state.errors.password}
/>
</section>
<section className="form-buttons">
<Button type="submit" className="btn btn-primary" block>Login</Button>
</section>
</Form>
</div>
</Col>
</Row>
</div>
)
}
}
When Meteor.loginWithPassword changes your logginIn value to true, your <Public /> component unmounts your wrapped <LoginForm />
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />; // <--- this right here
return !authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/" />);
}}
/>
);
So loggingIn values changes causing <Public /> to re-render. Now that loggingIn is true, you render a div instead of the component that was, unmounting it and making setStateunavailable when the errors callback tries to invoke it.
EDIT: In response to your comment...
To prevent this you can handle the error display inside the <LoginForm />
Remove if (loggingIn) return <div />; from your <Route /> in your <Public /> component.
Inside your <LoginForm /> you can handle the error condition. Do this by making a component that displays your errors and include it in your <LoginForm /> component where you want it displayed. Make the <ErrorDisplay /> component return nothing if there are no errors.
Example <ErrorDisplay /> component
const ErrorDisplay = ({ errors }) => {
errors && <div className="error-container">{ errors }</div>
};
That is obviously barebones, but I hope it helps you understand!