Why is my React component mounting after each action? - javascript

I have a React component called Home which is calling an action to fetch some groups when the component mounts.
I am calling an action as follows:
componentDidMount() {
const { fetchRecentGroups } = this.props;
fetchRecentGroups();
}
My reducer is picking up each action perfectly fine and is returning a state as follows:
switch(action.type) {
case REQUEST_GROUPS:
return {
...state,
loadState: FETCHING
};
case REQUEST_GROUPS_SUCCESS:
return {
...state,
loadState: SUCCESS,
groups: action.data.groups,
totalResults: action.data.totalResults
};
default:
return state;
}
I am also using the connect HOC on this component as follows:
const mapStateToProps = (state) => {
return {}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchRecentGroups: () => {
dispatch(actions.fetchRecentGroups())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
The Home component is placed inside a Route like this:
<Route
exact={true}
path="/"
component={Home}
/>
My problem is that each time the reducer returns a state, the componentDidMount is called again and again in a loop. I would expect the mount to happen only once at the first load.
If I put componentDidUpdate and componentWillReceiveProps functions in my component, they are never called (only componentDidMount) so I am not able to compare props.
Does anyone know why this may be happening?
EDIT:
I have found my problem to be caused by this piece of code in my route:
const RouteBlock = () => {
if(errorSettings) {
return <Error {...errorSettings} />
}
return (
<div className={styles.RouteBlock}>
<Route exact={true} path="/" component={Home} />
<Route path="/search" render={() => <div>SEARCH</div>} />
</div>
);
};
return <Router><RouteBlock /></Router>
I changed it to:
return <Router>{RouteBlock()}</Router>

Every time you render your component, you are immediately calling function that is setting new state and you are triggering re-rendering of your component. Maybe you should use shouldComponentUpdate life cycle method that will check is your old state the same as new one.
Check out official docs: https://reactjs.org/docs/react-component.html#shouldcomponentupdate

Related

React router doesn't update sub components params

In my React application, I have trouble making a sub-component update based on props.
the sub-component gets the props from a <Link/> tag that is exposed to store state
const CallPortfolioManagement= (props) => {
const { portfolio } = props;
return (
<div>
<Link
to={{pathname: `/portfolios/${portfolio.name}`,state: { portfolio: portfolio},}}>
{portfolio.name}</Link>
</div>
);
};
const mapStateToProps = (state) => {
return {
portfolio: getPortfolio(state),
};
};
export default connect(mapStateToProps)(CallPortfolioManagemnt);
the PortfolioManagement component is:
const PortfolioManagement = (props) => {
const portfolio = useLocation().state.portfolio;
return (
<>
{portfolio.stocks.map((stock, index) => (
<div key={stock.symbol}>
<h1>
{stock.symbol}
</h1>
</div>
))}
</>
);
};
export default PortfolioManagement;
a component that got a direct subscription to the state and rerenders when a new stock symbol is added:
const RenderLastStock= (props) => {
const renderLast () => {
var stocks;
if (props.portfolio) {
stocks = props.portfolio["stocks"];
return <button>{stocks[stocks.length - 1]].symbol}</button>;
}
};
return (
<>
renderLast ()}
</>
);
};
const mapStateToProps = (state) => {
return { tasks: getLoadingTasks(state), portfolios: getPortfolios(state) };
};
export default connect(mapStateToProps)(RenderLastStock);
the route declared here and calls PortfolioManagement when clicked:
function App(props) {
useEffect(() => {
props.getPortfolios();
}, []);
return (
<Router>
<div className="App">
<Switch>
<PrivateRoute>
<Route path="/portfolios/:id" component={PortfolioManagement} />
</PrivateRoute>
</Switch> </div>
</Router>
);
}
the problem is that PortfolioManagement gets the params but does no rerender when the state is changed - when I add stock symbols.
I update the store's state with Object.assign and other components that are subscribed to this state do rerender! (so there aren't any immutability problems)
looking in the redux devtools I can see the state is updated correctly, I suspect that PortfolioManagement does not rerender because react does not refer to Link's Params as props and does not know it should trigger a rerender.
please help:(
instead of using useLocation, you can use withRouter at PortfolioManagement -
import { withRouter } from 'react-router-dom'
const PortfolioManagement = (props) => {
console.log(props.location && props.location.state)
...rest code...
}
export default withRouter(PortfolioManagement);
I know its hacky, but anyhow now state comes from props and component will re-render
Edit
The usage of Link and the state location object you can send with, works on a way that the context won't be exist if the component wasn't called through the link, consider send the props through regular props at Router decoration (that I assuming is a component connected to redux store)
<Route path="/portfolios/:id" render={()=> <PortfolioManagement props={...props} />} />
didn't find a solution with react router, I solved it by cheating and giving portfolioManagement direct access to the store

Trouble passing props to componentDidMount in child component (React)

I'm having issues passing a prop to a componentDidMount() call in a child component on my React application.
In my App.js I am passing props via Router like below:
App.js
class App extends Component {
state = {
city: ""
}
componentDidMount() {
this.setState({city: this.props.city});
}
render() {
return (
<div>
<Route path="/" exact render = {() => <Projections city={this.state.city} />} />
<Route path="/:id" component={FullPage} />
</div>
);
}
}
In my Projections.js I have the following:
Projections.js
constructor(props) {
super(props);
this.state = {
location: this.props.city
}
}
componentDidMount () {
console.log(this.state.location);
console.log(this.props.city);
}
console.log(this.state);' returns an empty string.console.log(this.props.city);` returns an empty string as well.
But I need to access the value of the city prop within componentDidMount(). console.log(this.props.city); within render() returns the prop, but not in componentDidMount()
Why is this and how do I return props within componentDidMount()?
In the constructor you should reference props, not this.props:
location: props.city
<Route path="/" exact render = {() => <Projections city={this.state.city} {...this.props} />} />
Try passing rest of props in route
this is because you assigned props in constructor that time it may or may not receive actual value. And it gets called only once in a component lifecycle.
You can use componentWillReceiveProps to get props whenever it receive and update state accordingly.
Inside Projections.js
UNSAFE_componentWillReceiveProps(nextProps){
if(nextProps.city){
this.setState({location:nextProps.city})
}
}
Here is working codesand

React fetch data HoC that depends on router params

I have a HoC that fetches data and either returns a loading screen or the underlying component with the data injected.
Now the problem is that the data being fetched depends on a) current URL and b) URL params. I'm using React Router v4. So what I've done is basically put a lot of switch cases in that component. Which works and does what I want it to do, but I'd rather not have the switch cases in this HoC.
const fetchesData = (WrappedComponent) => {
class FetchesData extends React.Component {
constructor(props) {
super(props);
this.fetchData = this.fetchData.bind(this);
this.state = {isLoading: true};
}
fetchData() {
this.setState({isLoading: true});
const {match, dispatch} = this.props;
const {params} = match;
let action = () => {};
switch (match.path) {
case '/': {
action = () => dispatch(
fetchPopularArticles()
);
break;
}
case '/artists/:slug': {
action = () => dispatch(
fetchArtistWithArticles(params.slug)
);
break;
}
// ... more
}
action()
.then((res) => {
this.setState({
...this.state,
isLoading: false,
});
});
}
componentDidMount() {
this.fetchData();
}
render() {
return (
!!this.state.isLoading ?
<LoadingComponent/> :
<WrappedComponent
{...this.props}
/>
);
}
}
return withRouter(connect()(FetchesData));
};
I'd prefer to somehow inject the fetchData() function from the underlying component. Or maybe from parent (router) component.
The first I'm not sure if possible since it would have to mount the underlying component first, which brings more trouble than anything else.
And the former I'm not sure how I would go about doing either since I would need to know the params of the route.
My route rendering looks something like this:
[
<Route
exact={true}
key={0}
path={'/'}
render={(props) => (
<fetchesData(Home)
{...props}/>
)}/>,
// ... more routes
]
What's a good practice for this?
If it helps here's the source:
HoC
Route rendering
Route definitions
As in react data flows down the preferred way would be to pass fetchData method from Render component. You could pass the method to fetchesData like this
const FetchedHome = fetchesData(Home, fetchPopularArticles)
const FetchedArtists = fetchesData(Home, fetchArtistWithArticles)
// ....
<Route
exact={true}
key={0}
path={'/'}
render={(props) => (
< FetchedHome {...props}/>
)}/>
And then inside fetchesData call the passed method
const fetchesData = (WrappedComponent, fetchMethod){
//....
componentDidMount() {
const {match, dispatch} = this.props
dispatch(fetchMethod(match.params));
}
}
Change the action to accept an object
const fetchArtistWithArticles = ({slug: artistSlug})
If you don't want to change your actions you could pass a mapping object from match.params to function attributes you want to send.

this.props out of date inside componentDidUpdate() using Higher Order Component

I have a higher order component which sets some values and then passes those as props to a wrappedComponent, however within that wrapped component when I access "this.props" from componentDidMount() the values are blank. If I place logs "this.props" from the render method in the wrappedComponent however I get the desired results, though i assume this is because of a re-render. What am i doing wrong here?
Home.js
import React, { Component } from 'react'
// eslint-disable-next-line
import { BrowserRouter as Router } from 'react-router-dom'
import { Route, Switch } from 'react-router-dom'
import BlogSummaryContainer from './utility/BlogSummaryContainer'
import BlogPost from './utility/BlogPost'
import EditableBlogPost from './utility/EditableBlogPost'
function withBlogPostData (WrappedComponent) {
return class BlogPostContainer extends React.Component {
constructor () {
super()
this.state = { title: '', content: '', catchPhrase: '' }
}
componentDidMount () {
fetch(`/api/posts/${this.props.match.params.id}`)
.then(res => {
return res.json()
})
.then(blogPost => {
// this setState doesnt reach the wrappedComponent in time even if i dont do a fetch and simply hard code a value, whats going on?
this.setState({
title: blogPost.title,
content: blogPost.content,
catchPhrase: blogPost.catchPhrase
})
})
}
render () {
return (
<WrappedComponent
id={this.props.match.params.id}
title={this.state.title}
content={this.state.content}
catchPhrase={this.state.catchPhrase}
/>
)
}
}
}
class Home extends Component {
... other code
render () {
return (
<Switch>
<Route
exact
path={`${this.props.match.url}`}
render={() => {
return <BlogSummaryContainer posts={this.state.blogPosts} />
}}
/>
<Route
exact
path={`${this.props.match.url}/:id`}
component={withBlogPostData(BlogPost)}
/>
<Route
exact
path={`${this.props.match.url}/:id/edit`}
component={withBlogPostData(EditableBlogPost)}
/>
<Route
exact
path={`${this.props.match.url}/new/post`}
render={() => {
return <EditableBlogPost isNew />
}}
/>
</Switch>
)
}
}
export default Home
EditableBlogPost.js
componentDidMount (props) {
const { title, catchPhrase, content } = this.props
console.log('this.props', this.props) // this.props = {title: "", content: "", ... }
}
I think this is just an asynchronous problem - when your HOC mounts it is calling fetch() which isn't resolved instantly so that is why on the first render this.state.x are their initial empty values.
When the Promise is resolved, the values are set and the subsequent render will have the expected values.
You could conditionally render to avoid rendering the wrapped component until the fetch() has resolved:
render () {
if(this.state.title.length === 0) {
return <div>Loading...</div>; //or some nice <Loading> component
}
return (
<WrappedComponent
id={this.props.match.params.id}
title={this.state.title}
content={this.state.content}
catchPhrase={this.state.catchPhrase}
/>
)
}

React Router v4: Sending requests when navigation changes

I'm coding an authentication with react-router v4 and I'm using the PrivateRoute with render props, like the documentation: Redirects (Auth)
What I'm trying to do is: Whenever the user navigates to a route, I want to dispatch an action to make a request to the backend to verify if he's logged in.
Like this:
// App.js
class App extends Component {
checkAuth = () => {
const { dispatch, } = this.props;
// callback to dispatch
}
render() {
const props = this.props;
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute
exact
path="/dashboard"
component={Dashboard}
checkIsLoggedIn={this.checkAuth}
/>
{/* ... other private routes here */}
</Switch>
</div>
</Router>
);
}
In PrivateRoute.js I'm listening the route to check if it changes, but when a route changes, this function is called too many times, and that's a problem to dispatch an action to make a request.
// PrivateRoute.js
const PrivateRoute = ({ component: Component, auth, checkIsLoggedIn, ...rest }) => (
<Route
{...rest}
render={props => {
props.history.listen((location, action) => {
if (checkIsLoggedIn) {
// Here I check if the route changed, but it render too many times to make a request
checkIsLoggedIn(); // here is the callback props
}
});
if (auth.login.isLoggedIn) {
return <Component {...props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
}
}
/>
);
I need a help to figure it out a good way to call the backend whenever the route changes.
Creating a Higher Order Component (HOC) is a very clean way to do this. This way, you won't need to create a separate PrivateRoute component, and it would take only one line of change to convert any Component from public to protected, or vice versa.
Something like this should work:
import React from 'react';
import { Redirect } from "react-router-dom";
export function withAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLoggedIn: false,
isLoading: true
};
}
componentDidMount() {
// Check for authentication when the component is mounted
this.checkAuthentication();
}
checkAuthentication() {
// Put some logic here to check authentication
// You can make a server call if you wish,
// but it will be faster if you read the logged-in state
// from cookies or something.
// Making a server call before every protected component,
// will be very expensive, and will be a poor user experience.
this.setState({
isUserLoggedIn: true, // Set to true or false depending upon the result of your auth check logic
isLoading: false
});
}
render() {
// Optionally, you can add logic here to show a common loading animation,
// or anything really, while the component checks for auth status.
// You can also return null, if you don't want any special handling here.
if (this.state.isLoading) return (<LoadingAnimation />);
// This part will load your component if user is logged in,
// else it will redirect to the login route
if (this.state.isUserLoggedIn) {
return <WrappedComponent authData={this.state} {...this.props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />;
}
}
}
}
Once you have that component in place, all you need to do is use the HOC in any component that you wish to have protected. For example, in your case, the export line in your Dashboard file would be something like this:
/* Dashboard.js */
class Dashboard extends React.Component { ... }
export default withAuth(Dashboard);
and in your App, you can use a simple Route component:
<Route exact path='/dashboard' component={Dashboard} />
Your App does not need to care about which routes are protected, and which ones aren't. In fact, only the actual components need to know that they are protected.
Hope this helps. Cheers! :)

Categories