React call parent method from child of a child - reactjs

i'm facing a small problem with my react app.
I'm using bluprintjs Toaster, and i need to display them on top of all other component, no matter what. Like this if there is an error during login or logout the user will always see the toast even if there is a redirection.
My problem is, that i have a middle component that is used to protect access to unAuthenticated user.
On my app class i have a ref to the Toaster and can easily call renderToaster to display a toast. So the method is working correctly.
But when i pass it to my ProtectedRoute and then to MyForm Component i can't call it in the MyFrom component.
From App -> ProtectedRoute -> MyForm if i print this.props i can see the renderToaster() Method, but i think the link from MyFrom -> ProtectedRoute -> App is somehow broken because on MyFrom i have the this.toaster is undefined error.
How can i call my parent parent method. Or how can i create a link between app and MyForm compenent passing through ProtectedRoute?
Thank you for your help.
My App class:
class App extends Component {
renderToaster(intent, message) {
this.toaster.show({
intent: intent,
message: message
});
}
<React.Fragment>
<NavBarComponent />
<Switch>
<ProtectedRoute
exact
path="/path1"
name="path1"
location="/path1"
renderToaster={this.renderToaster}
component={MyForm}
/>
<ProtectedRoute
exact
path="/path2"
name="path2"
location="/path2"
component={params => <MyForm {...params} renderToaster={this.renderToaster} />}
/>
</Switch>
<Toaster
position={Position.BOTTOM}
ref={element => {
this.toaster = element;
}}
/>
</React.Fragment>
}
My ProtectedRoute class:
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { AuthContext } from '../providers/AuthProvider';
class ProtectedRoute extends Component {
state = {};
render() {
const { component, ...rest } = this.props;
const Component = component;
return (
<AuthContext>
{({ user }) => {
return user ? (
<Route render={params => <Component {...params} {...rest} />} />
) : (
<Redirect to="/" />
);
}}
</AuthContext>
);
}
}
export default ProtectedRoute;
And on my last class (MyForm passed to the protected Route) i call my renderToaster Method like this:
/**
* Component did Mount
*/
componentDidMount() {
this.props.renderToaster(Intent.PRIMARY, 'helloo');
}

You either need to bind renderToaster in the class constructor:
constructor(){
this.renderToaser = this.renderToaster.bind(this);
}
or declare renderToaser as an ES7 class property.
renderToaster = (intent, message) => {
this.toaster.show({
intent: intent,
message: message
});
}
The problem is this in renderToaster isn't pointing where you think it is when the method is passed to the child component. If you use either of these methods, then this will refer back to the class.
See the official docs for more detail: https://reactjs.org/docs/handling-events.html

Related

Redux state is not being passed to nested router component despite fetching the state directly in that component

I have a react-redux state that is a fetched array of 'products' objects from a backend database. I have a 'ProductList' component that receives the fetched objects and creates a list of products. I am directly calling my 'fetchProducts()' method within this 'ProductList' component. This 'ProductList' component is able to be refreshed and the fetched products will persist and reappear after each refresh.
However, I have another 'ViewProduct' component that is in a nested Router route in which I am also fetching the same 'products' array. The problem is in this component, on refresh, the 'products' state does not persist and returns undefined despite the fact that I am also directly calling 'fetchProducts()' inside the component.
I am passing the products array directly into the 'ProductView' component yet it does not persist on refresh the same way my 'ProductsList' component does.
Here is my 'AllProductsList' component that SUCCESSFULLY persists the products data on refresh. It is located in a route '/products/all':
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchProducts } from '../actions/productAction.js';
import ProductCard from '../components/ProductCard.js';
class AllProductsList extends Component {
componentDidMount() {
this.props.fetchProducts()
}
renderDiv = () => {
return this.props.products.map((product) =>
<ProductCard key={product.id} product={product} />
)
}
render() {
return(
<div>
{this.renderDiv()}
</div>
)
}
}
AllProductsList.propTypes = {
fetchProducts: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
}
const mapStateToProps = state => ({
products: state.products.items,
})
export default connect(mapStateToProps, { fetchProducts })(AllProductsList)
And here is the problem 'ProductView' component that does NOT persist the product data after refresh. This component is located on route /product/:id :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchProducts } from '../actions/productAction.js';
class ProductView extends Component {
constructor(props) {
super(props)
this.state = {
buttonMessage: 'Add to cart',
quantity: 0,
}
}
componentDidMount() {
this.props.fetchProducts()
}
quantityChangeReader = (e) => {
this.setState({ quantity: e.target.value })
}
render() {
return(
<div>
<ul>
<li>this.props.products[this.props.match.params.productId].name</li>
<li>this.props.products[this.props.match.params.productId].comments</li>
<li>this.props.products[this.props.match.params.productId].photos[1].url</li>
</ul>
</div>
)
}
}
ProductView.propTypes = {
fetchProducts: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
}
const mapStateToProps = state => ({
products: state.products.items,
})
export default connect(mapStateToProps, { fetchProducts })(ProductView)
Parent component of 'ProductView', which is routed to in the App.js:
import React from 'react';
import ProductView from '../components/ProductView.js';
import { Route } from 'react-router-dom';
const ProductViewContainer = ({ match, products }) => (
<div>
<Route path={`${match.url}/:productId`} render={routerProps => <ProductView {...routerProps} /> }/>
</div>
)
export default ProductViewContainer
In this second scenario, for component ProductView, on refresh, it will return the error message, 'Cannot read .name of undefined.' Yet in the first component ('AllProductsList') refreshing will successfully return the array again.
As requested, here is the router:
<Provider store={store}>
<Router>
<NavBar />
<Route exact path="/" component={Carousel} />
<Route exact path="/about" component={OurStory} />
<Route exact path="/products/necklaces" component={NecklacesList} />
<Route exact path="/products/bracelets" component={BraceletsList} />
<Route exact path="/products/earrings" component={EarringsList} />
<Route exact path="/products/all" component={AllProductsList} />
<Route exact path="/events" component={Events} />
<Route exact path="/cart" component={Cart} />
<Route path='/product' render={routerProps => <ProductViewContainer {...routerProps} />} />
<Credits />
</Router>
</Provider>
In the ProductView component, this.props.products will be undefined when the component renders for the first time. This will happen on page refresh or when you hit the product id route directly in the browser, either of which will reset your redux state.
To curb this, you'll need to add a check in your component to confirm if the products prop has been populated.
Like this...
render() {
const { products } = this.props;
if (products.length) {
// render the rest of your view
} else {
// products is still fetching...
}
}
React-redux does not persist data when you refresh page. If you want to persist data, store in localstorage.
When you refresh page, initially this.props.products will be null. Once data get fetch, this.props.products will have remote data, and component will get re-render. You can put if condition in ProductView component

React Context API + withRouter - can we use them together?

I built a large application where a single button on the navbar opens a modal.
I'm keeping track of the modalOpen state using context API.
So, user clicks button on navbar. Modal Opens. Modal has container called QuoteCalculator.
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.props.history.push('/quote');
// ..
};
render() {
//...
return(<Question {...props} next={this.onSubmit} />;)
}
}
export default withRouter(QuoteCalculator);
Now, everything works as expected. When the user submits, I go to the right route. I just see the following warning on the console
index.js:1446 Warning: withRouter(QuoteCalculator): Function
components do not support contextType.
I'm tempted to ignore the warning, but I don't think its a good idea.
I tried using Redirect alternatively. So something like
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.setState({done: true});
// ..
};
render() {
let toDisplay;
if(this.state.done) {
toDisplay = <Redirect to="/quote"/>
} else {
toDipslay = <Question {...props} next={this.onSubmit} />;
}
return(<>{toDisplay}</>)
}
}
export default QuoteCalculator;
The problem with this approach is that I kept on getting the error
You tried to redirect to the same route you're currently on
Also, I'd rather not use this approach, just because then I'd have to undo the state done (otherwise when user clicks button again, done is true, and we'll just get redirected) ...
Any idea whats going on with withRouter and history.push?
Here's my app
class App extends Component {
render() {
return (
<Layout>
<Switch>
<Route path="/quote" component={Quote} />
<Route path="/pricing" component={Pricing} />
<Route path="/about" component={About} />
<Route path="/faq" component={FAQ} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
</Layout>
);
}
}
Source of the warning
Unlike most higher order components, withRouter is wrapping the component you pass inside a functional component instead of a class component. But it's still calling hoistStatics, which is taking your contextType static and moving it to the function component returned by withRouter. That should usually be fine, but you've found an instance where it's not. You can check the repo code for more details, but it's short so I'm just going to drop the relevant lines here for you:
function withRouter(Component) {
// this is a functional component
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<Route
children={routeComponentProps => (
<Component
{...remainingProps}
{...routeComponentProps}
ref={wrappedComponentRef}
/>
)}
/>
);
};
// ...
// hoistStatics moves statics from Component to C
return hoistStatics(C, Component);
}
It really shouldn't negatively impact anything. Your context will still work and will just be ignored on the wrapping component returned from withRouter. However, it's not difficult to alter things to remove that problem.
Possible Solutions
Simplest
Since all you need in your modal is history.push, you could just pass that as a prop from the modal's parent component. Given the setup you described, I'm guessing the modal is included in one place in the app, fairly high up in the component tree. If the component that includes your modal is already a Route component, then it has access to history and can just pass push along to the modal. If it's not, then wrap the parent component in withRouter to get access to the router props.
Not bad
You could also make your modal component a simple wrapper around your modal content/functionality, using the ModalContext.Consumer component to pass the needed context down as props instead of using contextType.
const Modal = () => (
<ModalContext.Consumer>
{value => <ModalContent {...value} />}
</ModalContext.Consumer>
)
class ModalContent extends React.Component {
onSubmit = () => {
// ...
this.props.toggleModal()
this.props.history.push('/quote')
// ..
}
// ...
}

Error while using history object

When i use history.push(/myappointment), the page route to the MyAppointment component. Now in MyAppointment component, there is a button, on clicking that button i call a function "Click" in which i do history.push(/newAppointment) to go to the "newAppointment" component, but this time the error is shown as :-
MyAppointment.tsx:53 Uncaught TypeError: Cannot read property 'history' of undefined
Some Few lines of both components are:-
The code for the first component is:-
interface ILoginPageProps {
history: any;
}
class Login extends React.Component<ILoginPageProps, {}> {
componentDidUpdate() {
const { history } = this.props;
history.push({"/myappointment"});
}
}
The code for the MyAppointment is :-
interface IProps {
history: any;
}
rightClick() {
//console.log("hii");
const { history } = this.props;
history.push("/newAppointment");
}
The Route file code is :-
export default class App extends React.Component {
render() {
return (
<MuiThemeProvider muiTheme={getMuiTheme(constants)}>
<Switch>
<Route exact path="/" component={Login} />
<Route path="/myAppointment" component={MyAppointment} />
<Route
path="/myAppointment/newAppointment"
component={NewAppointment}
/>
</Switch>
</MuiThemeProvider>
);
}
}
Thanks for the help in advance.
You probably have a typo in this part. You are trying to destruct history from this.props.history.
const { history } = this.props.history; // this probably should be `this.props` instead
history.push("/newAppointment");
You either want a Link component to route to another page.
Provides declarative, accessible navigation around your application
import { Link } from "react-router-dom";
...
<Link to="/newAppointment" />
Or if you want to push programatically you must wrap your component with the withRouter HOC
You can get access to the history object’s properties and the closest
Route's match via the withRouter higher-order component. withRouter
will pass updated match, location, and history props to the wrapped
component whenever it renders.
#withRouter
class Login extends React.Component
....
or if you prefer
class Login extends React.Component
...
export default withRouter(Login);

Conditional Routing and Conditional Rendering in ReactJS and React router

I have a question, I tried looking online and couldn't find what I needed but that's probably because I couldn't word my question properly.
I have a React project I'm putting together and I want to have user profile pages for people. Here is what I have so far.
<Route path="/profile/:user" render={(routeProps) => (<Profile {...routeProps} {...this.state} /> )}/>
This is in my index.js, to set the route for my profile pages for different users and the following is what I have for my profile page
import React from 'react';
import ReactDOM from 'react-dom';
import { NavLink } from 'react-router-dom';
export class Profile extends React.Component{
constructor(props){
super(props);
this.state={
user: this.props.user,
fullname: this.props.fullname,
picture: this.props.picture
};
}
render(){
return(
<h1>{this.state.fullname}</h1>
);
}
}
Now the question I have is this. I want the profile page to only load and render the users full name if the URL matches the userID given by 'user' in the state
in my index.js I have hardcoded user and fullname values to test this, they are set like this
constructor(props){
super(props);
this.state = {
user:"AFC12345",
fullname:"Bob Ross"
};
I want to only have the Profile page rendered when I visit "http://localhost:8080/#/Profile/AFC12345" currently it renders the profile page for any "Profile/xxxx" I go to.
Another way would be to consider that Profilecontainer as a protected URL and use the same dynamic as the authentication flow.
import React from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedProfile = ({ component: Component, ...rest }) => (
<Route {...rest} render={ props => (
props.user === props.match.params.user ? (
<Component {...props} /> ) : (
<Redirect to={{ pathname: '/404' }} /> )
)} />
);
Then in your App/index.js
<ProtectedProfile path="/profile/:user" component={Profile} />
i would do something like this in the render function
let viewToRender
if (this.state.user === this.props.match.params.user) {
viewToRender = <p>{this.state.fullname}</p>
}else{
viewToRender = <p>Ids don't match</p>
}
....
return (){ viewToRender }

How to redirect an user to a different url when history object of react-router is not accessible

How should I redirect an user to a different url when I cannot get an access to history props of react-router?
What I want to do is when an user clicks an log-out link on the navigation menu, the user get redirected to the root path '/'.
handleAuthentication(event) {
this.props.toggleAuthenticationStatus(() => {
// I want to redirect an user to the root path '/' in this callback function.
});
}
handleAuthentication method is called when an user clicks an login/logout link on the navigation menu.
toggleAuthenticationStatus(callback) {
this.setState((prevState, props) => {
return { isLoggedIn: !prevState.isLoggedIn }
},
callback()
);
}
Then, when handleAuthentication method in the NavigationMenu Component, it calls toggleAuthenticationStatus method in App Component that changes the state of Login/Logout and run callback function which is defined in the handleAuthentication method in the NavigationMenu Component.
Is it ok to run "window.location.href = '/'" directly?
Does it mess up the react-router history object???
Could anyone please how I should implement user redirect in a right way?
App Component
import React, { Component } from 'react';
import NavigationMenu from './NavigationMenu';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Secret from './Secret';
import Top from './Top';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false
};
this.toggleAuthenticationStatus = this.toggleAuthenticationStatus.bind(this);
}
toggleAuthenticationStatus(callback) {
this.setState((prevState, props) => {
return { isLoggedIn: !prevState.isLoggedIn }
},
callback()
);
}
render() {
return (
<BrowserRouter>
<div>
<NavigationMenu isLoggedIn={this.state.isLoggedIn} toggleAuthenticationStatus={this.toggleAuthenticationStatus} />
<Switch>
<Route path='/secret' render={(props) => <Secret isLoggedIn={this.state.isLoggedIn} {...props} />} />
<Route path='/' component={Top} />
</Switch>
</div>
</BrowserRouter>
)
}
}
NavigationMenu Component
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class NavigationMenu extends Component {
constructor(props) {
super(props);
this.handleAuthentication = this.handleAuthentication.bind(this);
}
handleAuthentication(event) {
this.props.toggleAuthenticationStatus(() => {
// I want to redirect an user to the root path '/' in this callback function.
});
}
render() {
return (
<ul>
<li><Link to='/'>Top</Link></li>
<li><Link to='/secret'>Secret</Link></li>
<li><Link to='/login' onClick={this.handleAuthentication}>
{this.props.isLoggedIn === true ? 'Logout' : 'Login'}
</Link></li>
</ul>
)
}
}
export default NavigationMenu;
I found the method 'withRouter' in react-router.
This seems the solution in my situation.
I'm going to try using it.
https://reacttraining.com/react-router/web/api/withRouter
You can get access to the history object’s properties and the closest
's match via the withRouter higher-order component. withRouter
will re-render its component every time the route changes with the
same props as render props: { match, location, history }.

Resources