I have a very simple React component, in the componentDidMount() method I fire a call to Firebases firestore to get a document. The first render call displays a template with the caption 'No Items' when the Firebase call has completed it re renders the component successfully with the items data.
However in the process it throws an error onto the console
index.js:2178 Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the HomePage component.
I have tried looking at other articles but as far I can see I am doing it the right way. Can anyone tell me what I am doing wrong?
import React from 'react';
import { firestore } from '../firebase/firebase'
class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
items: ''
}
}
componentDidMount() {
firestore.collection('item').doc('P1zi3sqkgFuJ6Jw243SA').get().then( o => {
this.setState({items: o.data()});
});
}
render() {
const item = this.state.items
const template = item ? <h1>{item.title}</h1> : <h1>No item</h1>
return(
<div>
<h1>Home Page</h1>
{ template}
</div>
);
}
}
export default HomePage;
I managed to figure out why this error appears even though react is rendering the returned items from Firebase just fine.
When the page is refreshed on the component it throws the error, if I navigated to another component then to this one it would not appear. I also noticed that when the bundle is recompiled and that home component is refreshed it fires two request to Firebase.
Not sure what the underlying issue is but at least its a step in the right direction
Related
I have a users component that just displays a list of users. I have tried to wrap it in a HOC loading component so that it only displays once the users are loaded, otherwise shows a loading spinner (well just text for now)
this is my HOC:
const Loading = (propName) => (WrappedComponent) => {
return class extends React.Component{
render(){
return this.props[propName].length === 0 ? <div> Loading... </div> : <WrappedComponent {...this.props} />
}
}
}
export default Loading;
at the bottom of my users component I have this:
export default connect(
mapStateToProps,
mapDispatchToProps
)(Loading('users')(Users));
currently, the word Loading... is just staying on screen. and propName is coming through as undefined
I think for some reason the users component is never getting populated. what have i done wrong?
Update after comments
My answer below is a misleading one since I hadn't understood your intention properly at that time. Also, my explanation about not getting props is somehow wrong. It is true if we don't render the components but here you are doing it. So, the problem was not that.
The problem here is your Loading component isn't rendered again after fetching users. Actually, you never fetch the users :) Here are the steps of your app (probably).
You are exporting a HOC function, not the Wrapped one here. It comes from your Users file but it does not export the real Users component. This is important.
Your parent renders the first time and it renders the exported HOC component.
Your child component renders and fall into Loading one not the Users one.
In Loading your users prop is empty, so you see Loading....
Your Users component never renders again. So, fetching the users there don't update the state.
Your solution is extracting the fetch out of Users and feed this component. Probably in a parent one. So:
Parent fetches the users then renders itself and all its children.
Your Loading HOC component renders a second time.
I don't know how do you plan to use this HOC but if I understood right (since I'm not so experienced with HOC) in your case the problem is you are not passing any prop to the Loading function. This is because you are not using it as a regular component here. It is a function and propName here is just an argument.
When we render a stateless function like this:
<Loading propName="foo" />
then there will be a props argument for our function. If we don't render it like that there will be no props argument and no props.propName. If this is wrong please somebody fix this and explain the right logic. So, you want to do something like this probably:
class App extends React.Component {
render() {
return (
<div><FooWithLoading /></div>
);
}
}
const Loading = (users) => (WrappedComponent) => {
return class extends React.Component {
render() {
return (
users.length === 0 ? <div> Loading... </div> :
<WrappedComponent
users={users}
/>
);
}
}
};
const Foo = props => {
return (
<div>
Users: {props.users}
</div>
);
}
const FooWithLoading = Loading("foobar")(Foo);
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
So in your case:
export default connect(
mapStateToProps,
mapDispatchToProps
)(Loading('users')(Users));
should work?
Or you need to render your component properly in a suitable place of your app.
I have a react app that ties into localStorage of the browser. On the startup of the app, the localStorage is populated with all the data that is needed to run the app. This data is pulled with AJAX from XML files and constructed to form a localStorageObject that the web app can use as its "database" of information to pull content from...
At the moment, The main component's state is set to the localstorage. So essentially I have the following:
constructor(props) {
super(props);
this.state = {
courseData : JSON.parse(localStorage.getItem("storageID"));,
}
}
The state contains an object that is the entirety of the localStorage. Now I have many children components, who also have children components themselves. Some are components that just need to render once, while others are going to need to rerender with interaction from the user.
After reading, it seems there are many ways to implement a solution. I could have all the components have state, but that's not needed. I could just have the main component have state, and no other component have state. And whenever the state of the main component changes, the props will be based down and reupdated.
Is there a specific method that is best?
This method works, but.
First of all, localStorage calls should be on a componentDidMount function. Otherwise, it wouldn't work on a server-side-rendering case.
Secondly, I'd implement all the initial data fetching on a parent function and then pass down data to the root of react tree:
const localStorageData = localStorage.getItem('some_data')
ReactDom.render(
document.getElementById('my-element'),
<MyComponent
localStorageData={localStorageData}
/>
)
if have many children components it will be difficult to manage state because of deep nesting.
I would recommend using Higher Order Component for your local storage implementation And Pass it down to children. Here How I would do it:
import React from 'react';
var HigherOrderComponent = (Component) =>
class extends React.Component {
state={locStorage:{}}
componentDidMount(){
this.setState({locStorage:window.localStorage.getItem("data")})
}
render() {
return (
<Component
locStorage={this.state.locStorage}
/>
)
}
};
export default HigherOrderComponent;
import HigherOrderComponent from './HigherOrderComponent'
const ChildComponent = ({locStorage}) => {
console.log(locStorage)
return (
<div>
</div>
);
};
export default HigherOrderComponent(ChildComponent);
I noticed that whenever I navigate to another page using the navigate props available to my component, it triggers a re-render of the component and componentDidMount is being called whenever I navigate to a screen that has rendered before.
For instance, when I navigate a user to their profile page and they decided to go back to the dashboard, the dashboard component which has been initially rendered is being rendered again and componentDidMount is being called thereby slowing down the application.
import { StackNavigator } from 'react-navigation';
const Routes = StackNavigator({
home: {
screen: HomeScreen
},
dashboard: {
screen: Dashboard
},
profile: {
screen: Profile
}
},
{
headerMode: 'none',
});
In my component I navigate the user with this.props.navigation.navigate('ScreenName')
I would appreciate any help to stop the component from re-rendering when navigating back to it. Thanks
I would have a state variable in your constructor that keeps track if you navigated. State is only relevant to the current component. So if you navigate to 'ScreenName' multiple times, the stack builds and each ScreenName component has its own state.
constructor(props)
super(props)
this.state = {
navigatedAway : false
}
Then before you navigate to your 'ScreenName' screen update the state
this.setState({
navigatedAway : true
},
() => {
this.props.navigation.navigate('ScreenName');
}
);
Use syntax above to make sure state isUpdated THEN navigate. Then like Dan said in comments above if your function shouldComponentUdate have a condition statement.
shouldComponentUpdate(newProps){
// return true if you want to update
// return false if you do not
}
* Side Note *
When you navigate I don't believe the component is unmounted. You could verify this by simply printing to console. Correct me if I am wrong though, I am fairly new to react native.
componentDidMount() {
console.log("COMPONENT_CONTENT_MOUNTED")
}
componentWillUnmount({
console.log("COMPONENT_CONTENT_UNMOUNTED")
}
If you are using React Navigation 5.X, just do the following:
import { useIsFocused } from '#react-navigation/native'
export default function App(){
const isFocused = useIsFocused()
useEffect(() => {
//Update the state you want to be updated
} , [isFocused])
}
If I understand your question correctly, when you navigate away, the component is unmounted.
When you navigate back, it must be re-mounted, hence re-rendered.
In general, any UI change necessitates a re-render. No way around that - It's kind of "by definition".
You might be able to cache the page.
Or use the reselect library to cache expensive to obtain data, so the calculations for re-rendering are quick and minimal.
If react/react-native thinks the props have changed (in an already mounted/rendered component), it will also re-render, but you can influence this decision via shouldComponentUpdate().
Just add React.memo to your export of component that reload each time.
So instead of
export default component
you would have:
export default React.memo(component);
Which does a comparison of props and only re-renders if props change (so not on navigate but on actual changes, which is what you want)
I'm making a site that is to store variables from a JSON file, then I can show that data. I'm attempting to pull data from an object, but I keep getting undefined or empty arrays/objects.
Here's content from my parent component.
export default class App extends Component {
constructor(){
super();
this.state = {
arrival: {}
}
}
axiosFunc = () => {
axios.get('https://api.warframestat.us/pc').then(results => {
this.setState({
arrival: results.data.voidTrader.activation,
});
setTimeout(this.axiosFunc,1000 * 60);
})
}
componentDidMount() {
this.axiosFunc();
}
}
Next, in the child component, I use props to store the arrival data.
<Baro arrival={this.state.arrival}/>
However, when I switch to the child component's file to show the data, I get an empty object...
componentDidMount(){
console.log(this.props.arrival)
}
How can I make the proper data show?
Your child component may be mounting before the GET request has resolved. That console log you showed is in componentDidMount. Try console logging it from componentWillReceiveProps or componentDidUpdate and see if the data is getting there a little later.
Alternatively, install the excellent React Dev Tools Chrome extension and watch the component's props in real time.
Update at the bottom of post
I have a React container component, AppContainer that detects if the user is authenticated. If the user is authenticated, it displays the routes, app, header, etc. If the user is un-authenticated, it displays a Login component.
The AppContainer is a connected component (using react-redux). The mapStateToProps and mapDispatchToProps are as follows:
const mapStateToProps = function(state) {
return {
isAuthenticated: state.Login.isAuthenticated,
}
}
const mapDispatchToProps = function(dispatch, ownProps) {
return {
loginSuccess: (user) => {
console.log("before dispatch")
dispatch(loginSuccess(user))
},
}
}
The loginSuccess function that is being dispatched is an action creator that simply stores the user information in the redux store. The default state of Login.isAuthenticated is false.
In componentDidMount() I check if this.props.isAuthenticated (from the user information in the redux store) is true. If not, I check if the tokenId is in the localStorage. If the token is in localStorage, I dispatch the loginSuccess action to add that information to the redux store.
Then, since that info is in the Redux store, the component will update and show the protected material. This works fine.
My componentDidMount function is as follows:
componentDidMount() {
if (this.props.isAuthenticated) {
console.log("REDUX AUTH'D")
} else {
if (localStorage.getItem("isAuthenticated") && !this.props.isAuthenticated) {
console.log("BROWSER AUTHD, fire redux action")
this.props.loginSuccess({
profileObj: localStorage.getItem("profileObj"),
tokenObj: localStorage.getItem("tokenObj"),
tokenId: localStorage.getItem("tokenId"),
})
}
}
}
The only issue is that I am getting the following warning:
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 t component.
Though the error given indicates a problem with setState(), I am not calling setState() anywhere in my entire program, so... But removing the this.props.loginSuccess({ ... in componentDidMount also removes the error.
The log statements in my code print before the error and the component does render the protected information as intended if the auth is present. Why does this error occur if the component seems to be working?
Update:
Looking at the stack trace shows that it is coming from the google-login utility I am using.
This is the code for that component: https://github.com/anthonyjgrove/react-google-login/blob/master/src/google.js
This was a problem with the google-login React component provided by a NPM package. I fixed this by rendering the google-login component conditionally (in its own container component, not featured in the original question) based on the isAuthenticated value in the Redux state.