While using the MUI useMediaQuery hook, I noticed my react app being buggy and throwing errors because the hook initially does not recognise the correct breakpoint, then the page quickly re-renders with the correct value.
Example:
const mobile = useMediaQuery((theme) => theme.breakpoints.only('mobile'));
console.log(mobile)
Console:
false
true
What's going on?
This is the expected behaviour of useMediaQuery hook. It's explained in the MUI docs:
To perform the server-side hydration, the hook needs to render twice. A first time with false, the value of the server, and a second time with the resolved value. This double pass rendering cycle comes with a drawback. It's slower. You can set this option to true if you are doing client-side only rendering.
So to get the correct value on the first page render the noSsr option in the useMediaQuery hook needs to be true.
There are two options:
1) Per component:
const mobile = useMediaQuery((theme) => theme.breakpoints.only('mobile'), {noSsr: true});
2) Globally in the theme object:
const theme = createTheme({
components: {
MuiUseMediaQuery: {
defaultProps: {
noSsr: true,
},
},
}
Obviously, this will only work without server-side rendering.
The original answer below works in Material-UI v4, but the Hidden component has been deprecated in v5.
Original answer:
I realised that by removing the media query and replacing it with the Material-UI <Hidden /> component it works how I want it to.
export const ResponsiveMenuItem = forwardRef((props, ref) => {
const { children, ...other } = props;
return (
<>
<Hidden smUp>
<option ref={ref} {...other}>
{children}
</option>
</Hidden>
<Hidden only="xs">
<MenuItem ref={ref} {...other}>
{children}
</MenuItem>
</Hidden>
</>
);
});
Related
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.
I am trying to use the DatePicker component from MUI version 5. I have included it exactly as it is specified in the codesandbox example from the MUI docs. So my component looks like this:
const MonthPicker: FC = () => {
const [value, setValue] = React.useState<Date | null>(new Date());
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
views={['year', 'month']}
label="Year and Month"
minDate={new Date('2012-03-01')}
maxDate={new Date('2023-06-01')}
value={value}
onChange={(newValue) => {
setValue(newValue);
}}
renderInput={(props) => <TextField {...props} size='small' helperText={null} />}
/>
</LocalizationProvider>
)
}
No matter what I tried, I always got the error message: React does not recognize the renderInput prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase renderinput instead.
If I write it in lowercase, then the code doesn't even render. What am I doing wrong here?
This is a bug from MUI, you can see it happens in the official docs too when you open the MonthPicker. It's because they forgot to filter the renderInput callback before passing the rest of the props to the DOM element.
You can see from the source that the YearPicker doesn't have this problem because it passes every props down manually, while the MonthPicker chooses to spread the remaining props which includes renderInput - this is an invalid prop because the HTML attribute doesn't know anything about JS callback object.
This error is just a warning from ReactJS when it thinks you're doing something wrong, but you're not because this is an upstream bug, and it doesn't affect anything else functionality-wise, so ignore it.
I am using 'Material UI' Autocomplete component to render a dropdown in my form. However, in case the user wants to edit an object then the dropdown should be displayed as autofilled with whatever value that's being fetched from the database.
I've tried to mock the situation using below code
import React, { Component } from 'react';
import Autocomplete from '#material-ui/lab/Autocomplete';
import TextField from '#material-ui/core/TextField';
export default class Sizes extends Component {
state = {
default: []
}
componentDidMount() {
setTimeout(() => {
this.setState({ default: [...this.state.default, top100Films[37]]})
})
}
render() {
return (
<Autocomplete
id="size-small-standard"
size="small"
options={top100Films}
getOptionLabel={option => option.title}
defaultValue={this.state.default}
renderInput={params => (
<TextField
{...params}
variant="standard"
label="Size small"
placeholder="Favorites"
fullWidth
/>
)}
/>
);
}
}
Here after the component is mounted, I'm setting a timeout and returning the default value that should be displayed in the dropdown
However, it's unable to display the value in the dropdown and I'm seeing this error in console -
index.js:1375 Material-UI: the `getOptionLabel` method of useAutocomplete do not handle the options correctly.
The component expect a string but received undefined.
For the input option: [], `getOptionLabel` returns: undefined.
Apparently the state is getting updated when componentDidMount is getting called but the Autocomplete component's defaultValue prop is unable to read the same
Any idea what I might be getting wrong here?
Code sandbox link - https://codesandbox.io/s/dazzling-dirac-scxpr?fontsize=14&hidenavigation=1&theme=dark
For anyone else that comes across this, the solution to just use value rather than defaultValue is not sufficient. As soon as the Autocomplete loses focus it will revert back to the original value.
Manually setting the state will work however:
https://codesandbox.io/s/elegant-leavitt-v2i0h
Following reasons where your code went wrong:
1] defaultValue takes the value which has to be selected by default, an array shouldn't be passed to this prop.
2] Until your autocomplete is not multiple, an array can't be passed to the value or inputValue prop.
Ok I was actually able to make this work. Turns out I was using the wrong prop. I just changed defaultValue to value and it worked.
Updated code pen link - codesandbox.io/s/dazzling-dirac-scxpr
I have the following simple component:
const Dashboard = () => {
const [{ data, loading, hasError, errors }] = useApiCall(true)
if (hasError) {
return null
}
return (
<Fragment>
<ActivityFeedTitle>
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
}
export default Dashboard
I would like to prevent ALL re-renders of the ActivityFeedTitle component, so that it only renders once, on load. My understanding is that I should be able to use the React.useMemo hook with an empty dependencies array to achieve this. I changed by return to be:
return (
<Fragment>
{React.useMemo(() => <ActivityFeedTitle>, [])}
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
As far as I'm concerned, this should prevent all re-renders of that component? However, the ActivityFeedTitle component still re-renders on every render of the Dashboard component.
What am I missing?
EDIT:
Using React.memo still causes the same issue. I tried memoizing my ActivityFeedTitle component as follows:
const Memo = React.memo(() => (
<ActivityFeedTitle />
))
And then used it like this in my return:
return (
<Fragment>
{<Memo />}
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
Same problem occurs. I also tried passing in () => false the following as the second argument of React.memo, but that also didn't work.
Use React.memo() instead to memoized components based on props.
React.memo(function ActivityFeedTitle(props) {
return <span>{props.title}</span>
})
Take note:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
The second argument passed to React.memo would need to return true in order to prevent a re-render. Rather than computing whether the component should update, it's determining whether the props being passed are equal.
Your usage of useMemo is incorrect.
From react hooks doc:
Pass a “create” function and an array of dependencies. useMemo will
only recompute the memoized value when one of the dependencies has
changed. This optimization helps to avoid expensive calculations on
every render.
If no array is provided, a new value will be computed on every render.
You need to use useMemo like useEffect here for computation of value rather than rendering the component.
React.memo
React.memo() is the one you are looking for. It prevents re-rendering unless the props change.
You can use React.memo and use it where your define your component not where you are making an instance of the component, You can do this in your ActivityFeedTitle component as
const ActivityFeedTitle = React.memo(() => {
return (
//your return
)
})
Hope it helps
It's because rendering a parent causes it's children to re-render for the most part. The better optimization here would be to place your data fetching logic either in the ActivityFeed component or into a HOC that you wrap ActivityFeed in.
So I'm working on a react app using, among other things, react-router. I'm having a problem where I'm changing the route, the change is reflected in the URL, but the mounted component doesn't change. Here is the component, it's one of my main container components:
class AppContent extends Component {
state = {
isStarted: false
};
componentDidMount = async () => {
try {
await this.props.checkIsScanning();
this.setState({ isStarted: true });
}
catch (ex) {
this.props.showErrorAlert(ex.message);
}
};
componentWillUpdate(nextProps) {
if (nextProps.history.location.pathname !== '/' && !nextProps.isScanning) {
this.props.history.push('/');
}
}
render() {
return (
<div>
<VideoNavbar />
{
this.state.isStarted &&
<Container>
<Row>
<Col xs={{ size: 8, offset: 2 }}>
<Alert />
</Col>
</Row>
<Switch>
<Route
path="/scanning"
exact
render={ (props) => (
<Scanning
{ ...props }
isScanning={ this.props.isScanning }
checkIsScanning={ this.props.checkIsScanning }
/>
) }
/>
<Route path="/"
exact
render={ (props) => (
<VideoListLayout
{ ...props }
isScanning={ this.props.isScanning }
/>
) }
/>
</Switch>
</Container>
}
</div>
);
}
}
const mapStateToProps = (state) => ({
isScanning: state.scanning.isScanning
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
checkIsScanning,
showErrorAlert
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(AppContent));
So, let's focus on the important stuff. There is a redux property, isScanning, that is essential to the behavior here. By default, when I open the app, the route is "/", and the VideoListLayout component displays properly. From there, I click a button to start a scan, which changes the route to "/scanning", and displays the Scanning component. The Scanning component, among other things, calls the server on an interval to check if the scan is done. When it is complete, it sets "isScanning" to false. When AppContent re-renders, if "isScanning" is false, it pushes "/" onto the history to send the user back to the main page.
Almost everything here works. The Scanning component shows up when I start the scan, and it polls the server just fine. When the scan is complete, redux is properly updated so "isScanning" now is false. The componentWillUpdate() function in AppContent works properly, and it successfully pushes "/" onto the history. The URL changes from "/scanning" to "/", so the route is being changed.
However, the Scanning component remains mounted, and the VideoListLayout component is not. I can't for the life of me figure out why this is happening. I would've thought that once the route was changed, the components would change as well.
I'm sure I'm doing something wrong here, but I can't figure out what that is. Help would be appreciated.
I'm pretty sure you're running into this issue described in the react-router docs where react-redux's shouldComponentUpdate keeps your component from re-rendering on route change: https://reacttraining.com/react-router/core/guides/redux-integration/blocked-updates. This can definitely be a pain and pretty confusing!
Generally, React Router and Redux work just fine
together. Occasionally though, an app can have a component that
doesn’t update when the location changes (child routes or active nav
links don’t update).This happens if: The component is connected to
redux via connect()(Comp). The component is not a “route component”,
meaning it is not rendered like so: The problem is that Redux implements
shouldComponentUpdate and there’s no indication that anything has
changed if it isn’t receiving props from the router. This is
straightforward to fix. Find where you connect your component and wrap
it in withRouter.
So in your case, you just need to swap the order of connect and withRouter:
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContent));