Why am I getting the following React Native warning: Can't perform a React state update on an unmounted component - javascript

I'm trying to build a React Native app, but am still kind of new to the React/RN ecosystem, so I may just be misunderstanding something obvious with the problem I'm having.
I have an app where a lot of the pages are structured as follows:
<View>
<NavComponent />
<View>
{/* Page component structure/logic here */}
</View>
</View>
NavComponent loads a toggleable nav menu with TouchableOpacity elements like the following:
Go to Screen #1
Go to Screen #2
Go to Screen #3
The problem I'm having (and maybe this isn't a problem so much as just how React/RN works) is that if I start on screen #1, open the nav, go to screen #2, open the nav again, and then go back to screen #1, even though screen #1 shows up again, the actual rendering function for screen #1 doesn't seem to be called again, and since NavComponent is part of the rendering of each screen, when I try to open the nav from screen #1 again, I get the following warning:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a
useEffect cleanup function ...
Again, maybe my approach with the app is flawed to begin with, but essentially, when I go from one screen to another from the nav, I always want the new screen to re-render from scratch (including the initial Ajax call for data).
Here's a more fleshed-out example of the render function for a screen (they all follow this same basic pattern):
const screen1 = ({ navigation }) => {
const [serverData, setServerData] = useState(null);
useEffect(() => {
// getPageData is a custom method I added to axios.
axios.getPageData('/api/url/here', (data) => {
setServerData(data);
});
}, []);
if (serverData) {
const { meta, user, data } = serverData;
return (
<View>
<NavComponent />
<View style={styles.container}>
{/* Page component structure/logic here */}
</View>
</View>
);
}
return null;
};
If, for example, I added a console.log to the beginning of the render function above, it's called the first time the screen is loaded, but if I go to screen #2 and then come back to screen #1 via the nav component, the console.log isn't output again. Why?
And for what it's worth, I'm using the standard navigation.navigate('ScreenName') in NavComponent to go from screen to screen.
Any advice on how to fix the warning (and/or just better design the app) so that I can have that nav on every page would be greatly appreciated. Thank you.

your api call is resulting in the warning, in the react/native ecosystem, when a component is removed from the tree, the developer needs to cancel all subscriptions (listeners to events) and async tasks(fetching data from the web), those function need to be canceld by the developer as react/native cant do that for you.
to handle that in a class based component, you need to impelment componentWillUnmount and remove the subscriptions there
class MyClass extends Component {
componentWillUnmount() {
// remove listeners and cancel requests
}
but in a modern hook component , you need to return a cleanup function, a function to return in useEffect that will be called by react to cancel any subscriptions you have made, in your case, just return a cleanup function should remove that warning for you
const [mounted, setIsMounted] useState(false)
useEffect(() => {
// getPageData is a custom method I added to axios.
setIsMounted(true)
axios.getPageData('/api/url/here', (data) => {
if(isMounted)
setServerData(data);
});
return () => {
setIsMounted(false)
}
}, []);

Related

How do I stop the re-rendering of my react component? It rerenders for every object in a drop down

After building the homepage of my website I finally figured out how to dynamically navigate to other pages. I wanted the browser to render the State homepage when a user clicked on a dropdown and selected a state. The navigation works, but it re-renders the component 50 times which I do not understand. I suspect it is due to the map function that is creating the menuitems. I could build out 50 individual menuitems but that is really ugly.
I am just starting out learning React. I have 7 YRS experience in backend development, but I am still trying to get a handle on React development. I have created a wepage with Material UI that has a dropdown that looks like this
<FormControl>
<InputLabel>Select a State</InputLabel>
<Select value={location} onChange={selectionChangeHandler}>
{locations.map((value) => (
<MenuItem value={value.toLowerCase()} key={value.toLowerCase()} component={Link} to={`${value.toLowerCase()}/home`} >
{value}
</MenuItem>
))}
</Select>
</FormControl>
This returns a dropdown with the 50 states in it. When the user clicks on a state I want the program to route to that page on click. This dynamic routing works BUT. It re-renders my component 50 times. I think this is happening because the dropdown is being built inside of a .map functions and there are 50 entries in that list.
I can remove the map function and hardcode in 50 menuitems but that is ugly.
Here is my onChange Function
const selectionChangeHandler = (event) => {
console.log(event.target.value)
}
I have also tried removing the component={Link} and using the useNavigate hook in the selectionChangeHandler like so
const selectionChangeHandler = (event) => {
console.log(event.target.value)
setlocation(event.target.value)
link = `${event.target.value}/home`
navigate(link)
}
This works but also renders 50 times. I am at a loss.
I cross posted the above to reddit and then I researched a little more. It turns out in React. When a parent component's state is updated it re-renders all child components. This may be what is going on here, but I do not know how to fix it. Even if I pass the state as a prop to the child component I still have to link to the correct page.
I am kind of nervous about posting I really tried to put work into solving my own problem before reaching out for help, and I might be reaching out for help a lot as I learn. I am committed to learning, but some problems I just cannot figure out on my own.
Link to Code Link to Code
The problem here is inside StateHome.js. You use a naked axios.get call directly in your component so it's going to re-render anytime the component changes from state change.
When you get the results you set state inside the same component which re-renders the component, which then re-runs the axios.get call, causing the loop.
I understand you want to call that endpoint when someone lands on the /alabama/home page. To tell React "Do this one time when the component loads" you must use a useEffect with an empty dependency array. So instead of this:
const StateHome = () => {
const { location } = useParams();
const [PageData, SetPageData] = useState();
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
return <h1>This is my State Home Page Component for State {location}</h1>;
};
You need to use this:
const StateHome = () => {
console.log("StateHome");
const { location } = useParams();
const [PageData, SetPageData] = useState();
useEffect(() => {
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
}, []); // <--- indicates "on mount", loads once regadless of other side-effects (setState)
return <h1>This is my State Home Page Component for State {location}</h1>;
};

EXPO useEffect not called on navigating to same screen

I have one screen which called useEffect when rendered first time in the flow.
The second time I navigate to the screen in the flow , use Effect is not called but I want to call a function upon first time before , after or during render function is called when we navigate a second time to the same screen.
Here is the navigate call for everytime navigating to this screen from various screens
navigation.navigate("x", { u:type, s:title});
The following is the structure for my screen. I am not using components but functions with navigation
const x = ({ navigation}) => {
...
return (
<View style={styles.a}>
...
</View>
);
};
export default x;
The problem here is that a screen remains mounted in react-native after the initial navigation. Thus, a useEffect with an empty dependency array won't be called subsequently on navigation.
Notice that this behavior differs from the web.
If you are coming to react-navigation from a web background, you may assume that when user navigates from route A to route B, A will unmount (its componentWillUnmount is called) and A will mount again when user comes back to it. While these React lifecycle methods are still valid and are used in react-navigation, their usage differs from the web.
The recommended way to solve this is described here. For your case this can be solved as follows.
import { useFocusEffect } from '#react-navigation/native';
const x = ({ navigation}) => {
useFocusEffect(
React.useCallback(() => {
// called when screen is focused, thus everytime on navigation
return () => {
// unfocus... cleanup ... or whatever
};
}, [])
);
...
return (
<View style={styles.a}>
...
</View>
);
};
export default x;
useEffect will be called on first load of your functional component or when some dependency will be changed. Everything remains the same, so your useEffect will not work. Consider using useFocusEffect from react-navigation lib instead.

"VirtualizedList: You have a large list that is slow to update" warning even though all components are optimized

I've been having lots of trouble trying to avoid getting the "VirtualizedList: You have a large list that is slow to update" warning when using a <FlatList> component with React-Native.
I've already done loads of research and Google-Fu to try a solution, but none of the solutions helped, not even the solution on this GitHub issue.
Things to keep in mind:
I'm using shouldComponentUpdate() { return false; } in my List Item component, so they are not updating at all.
The component that renders the FlatList is already a PureComponent
I added console.logs in the render methods of my components and can confirm they don't re-render.
I'm not using anonymous functions, so the renderItem component is not being re-initialized on each call.
Important: The warning only seems to occur after switching to a separate tab using my BottomTabNavigator and then coming back and scrolling through my list; but this is confusing because when I do this, the FlatList screen component is not re-rendering and neither are the list items. Since the components aren't re-rendering when browsing to another tab, why would this error happen?
Here's my exact code:
App.tsx
const AppRoutes = [
{ name: "Home", Component: HomeScreen },
{ name: "Catalog", Component: CatalogScreen },
{ name: "Cart", Component: CartScreen }
];
export const StoreApp = () => {
return (
<NavigationContainer>
<StatusBar barStyle="dark-content" />
<Tabs.Navigator>
{AppRoutes.map((route, index) =>
<Tabs.Screen
key={index}
name={route.name}
component={route.Component}
options={{
headerShown: (route.name !== "Home") ?? false,
tabBarIcon: props => <TabIcon icon={route.name} {...props} />
}}
/>
)}
</Tabs.Navigator>
</NavigationContainer>
);
};
CatalogScreen.tsx
import React from "react";
import { FlatList, SafeAreaView, Text, View, StyleSheet } from "react-native";
import { LoadingSpinnerOverlay } from "../components/LoadingSpinnerOverlay";
import { getAllProducts, ProductListData } from "../api/catalog";
class ProductItem extends React.Component<{ item: ProductListData }> {
shouldComponentUpdate() {
return false;
}
render() {
return (
<View>
{console.log(`Rendered ${this.props.item.name}-${Math.random()}`)}
<Text style={{height: 100}}>{this.props.item.name}</Text>
</View>
);
}
}
export class CatalogScreen extends React.PureComponent {
state = {
productData: []
};
componentDidMount() {
getAllProducts()
.then(response => {
this.setState({ productData: response.data });
})
.catch(err => {
console.log(err);
});
}
private renderItem = (props: any) => <ProductItem {...props} />;
private keyExtractor = (product: any) => `${product.id}`;
private listItemLayout = (data: any, index: number) => ({
length: 100,
offset: 100 * index,
index
});
render() {
const { productData } = this.state;
console.log("CATALOG RENDERED");
return (
<SafeAreaView style={styles.pageView}>
{!productData.length && <LoadingSpinnerOverlay text="Loading products..." />}
<View style={{backgroundColor: "red", height: "50%"}}>
<FlatList
data={productData}
removeClippedSubviews
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
getItemLayout={this.listItemLayout}
/>
</View>
</SafeAreaView>
);
}
};
const styles = StyleSheet.create({
pageView: {
height: "100%",
position: "relative",
}
});
Since my components and lists are optimized and I'm still receiving the error, I'm starting to believe that this may be an actual issue with React Native - but if anyone can see what I'm doing wrong, or any workarounds, this would help greatly!
Additional Findings:
I found that the warning no longer occurs if the CatalogScreen component is contained inside of a NativeStackNavigator with a single Screen. I believe this may indicate that this is a problem with the BottomTabNavigator module.
For example, the no warning no longer occurs if I make the following changes:
App.tsx
const AppRoutes = [
{ name: "Home", Component: HomeScreen },
{ name: "Catalog", Component: CatalogPage }, // Changed CatalogScreen to CatalogPage
{ name: "Cart", Component: CartScreen }
];
CatalogScreen.tsx
const Stack = createNativeStackNavigator();
export class CatalogPage extends React.PureComponent {
render() {
return (
<Stack.Navigator>
<Stack.Screen
name="CatalogStack"
options={{ headerShown: false }}
component={CatalogScreen}
/>
</Stack.Navigator>
);
}
}
With this workaround, I'm rendering the Stack Navigation component instead of the CatalogScreen component directly. This resolves the problem, but I don't understand why this would make a difference. Does React Native handle memory objects differently in Stack Navigation screens as opposed to BottomTabNavigator screens?
This is an old problem with FlatLists in React Native. FlatList in React Native is not known for its performance. However this could be mitigated by checking for unnecessary re-renders or nested components within the FlatList and making sure that your FlatList is not nested by a ScrollView. But you already stated that you are not re-rendering the list when returning to the screen and it is not nested by a ScrollView so it's probably the component itself.
Honestly I recently have had the same problem as you, but my FlatList is more complex than yours. When running it on a real device I noticed I haven't had a problem, for me this was only an issue on Expo Go. I've done my share of research but I haven't hit the nail on the head so I can only offer suggetions.
Suggestion 1
Check out this RecyclerListView package, it seems very promising.
Suggestion 2
There is also this package, but I'm a bit skeptical about this one, I've heard complains React Native Optimized Flatlist package. I'm pretty sure I tried it and it did nothing for me.
Also, check out these two pages on the topic:
FlatList Performance Tips
GitHub Issue #13413
I'm just going to take a random shot at an answer, but it's at best a guess.
The video you've linked in the comments to your question made it a lot more clear what's happening, something strange is going on there.
With normal lists like those, especially on mobile, you want to render and load only the items that's currently being displayed and visible, you don't want to keep the entire list of all possible items in memory and rendered all the time. That's why you'd use something like a callback to render the item, so that the list can invoke the callback as it's being scrolled and render only the items it needs to.
That being said here are a couple of random things I can think of:
Make sure that every single component, including and especially the item component, are pure components so they don't rerender. Make really sure that item component isn't rendering again.
Try to alter your props destructure so that you directly receive the props, i.e. ({prop1, prop2}) instead of props with ...props. If you destructure the props like that it will create a new object every time an item is loaded. This could potentially be one of the culprits causing your issue, if the flatlist is constantly invoking the callback and creating tons of new objects. This could also potentially be an issue for the product item, if it sees that it's received a new prop reference, which means it's a different object, then it will have to do a shallow comparison (even if it's a pure component), on hundreds of props. That could really slow it down. The result is that it won't actually rerender it but it will still do hundreds of shallow object comparisons which is a slow process. So fix the destructuring, I'd bet something like this is causing the performance issue.
Make sure that you don't double render the view, don't render the flatlist before the data is actually loaded, before the state has been set only return the fallback spinner or loader and once the state has been set then return the full child. Otherwise you're double rendering the whole thing, even if there's no data.
Try and see if it makes a difference if you use an anonymous function as the renderItem callback, instead of using react class functions. I don't think it should make a difference but it's worth a shot if nothing else helps, since I'm not sure how the renderItem is utilizing this.
If all else fails I'd suggest you ask react native on github and in the meantime try using an alternative if there is one. On the other hand I don't really see any performance issues from the video, so perhaps it's also an error that can be safely ignored.

component is not unmounting react-native

In my code I have a problem with unmount a component. I am using drawer-navigator and when i navigating between drawer-screens the previous screen is not dying and when i open that screen again everything is still there.
But i want to re-render or unmount and mount the component again when i navigate between drawer-screens. Is there a way for that? Or how can i make my component unmount manually? I know there is a lot of question about this but i couldn't find that i want.
My react native version is 0.63
react navigation never destroy the screen. Its mean your screen will never unMount or when you go back it will never remount (didMount). To Perform such see the example below
import React, { useCallback } from 'react';
import { useFocusEffect } from '#react-navigation/native';
const Home = () => {
useFocusEffect(
useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, [])
);
return <Home />;
}
Reference : Navigation Lifecycle
You should check this page of react-navigation documentation
Consider a stack navigator with screens A and B. After navigating to
A, its componentDidMount is called. When pushing B, its
componentDidMount is also called, but A remains mounted on the stack
and its componentWillUnmount is therefore not called.
Conclusion, the screens are not detroyed and re-created everytime you navigate, it's by design.
If you want to execute some code when you leave or open again a page you should use the lifecycle events presented in the same documentation

Run function from a different class in react native

I have a very simple issue with my react native application, I just want to execute a function everytime a button is clicked, it become complicated when there's separate classes and components.
I have 2 screens Dashboard and Search and 2 components Navbar and Results
In Dashboard I grab some user input and store it in selectedIngredients variable
and with the Navbar component I execute a function located in the same file.
<Navbar handle={() => this.switcher()} />
This function is where should the magic happens (or maybe in Search.js screen?)
switcher() {
const { navigate } = this.props.navigation;
navigate('Select', {passedData:this.state.selectedIngredients });
Alert.alert("send data to Search.js")
}
Select is Search.js screen
Everything workin fine and I move to the expected screen with the expected user input selectedIngredients, this is the first render of Search.js screen.
componentDidMount() {
this.apiCall();
Alert.alert("did mount search.js")
}
After that I'm stuck because everytime I click on the btn on my Navbar and execute the switcher() function, componentDidMount do not run anymore so I have to refresh the page by clicking on another button, this is exactly what I'm trying to avoid because it's bad for UX, like really bad. I am not looking to update Results component automatically but just update it with one function.
The code below is not that important it only shows apiCall function and render of Results component, I don't know if I should put more information. Please someone help
apiCall() {
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
this.setState({
data: responseJson.results,
});
})
}
componentDidMount() {
this.apiCall();
Alert.alert("did mount search.js")
}
render() {
return (
<View>
<Navbar title="Results" />
<Results results={this.state.data} />
</View>
);
}
}
My attempt was to add this.props.apiCall() in switcher function but got undefined error, something like hey react native! please send this data to Search.js screen and when you arrive please execute apiCall function, it's located there, not here.
Since you are using react navigation, in Search.js you have to bind didFocus listener, so the api will be called every time your screen is focused
componentDidMount() {
const { navigation } = this.props;
navigation.addListener( 'didFocus', () => this.apiCall() );
this.apiCall();
Alert.alert("did mount search.js")
}

Categories