I'm using react-virtuoso library to render a simple virtual list. The code is very straightforward. I pass this overscan props and expect the virtual list to render n items above and below the viewport but it's not working.
The ExpensiveComponents still renders 'loading...' text when I'm scrolling up and down a little. Here is the code:
import { Virtuoso } from "react-virtuoso";
import { useEffect, useState, useRef, PropsWithChildren } from "react";
function ExpensiveComponent({ children }: PropsWithChildren<{}>) {
const [render, setRender] = useState(false);
const mountRef = useRef(false);
useEffect(() => {
mountRef.current = true;
setTimeout(() => {
if (mountRef.current) {
setRender(true);
}
}, 150);
return () => void (mountRef.current = false);
}, []);
return (
<div style={{ height: 150, border: "1px solid pink" }}>
{render ? children : "Loading..."}
</div>
);
}
Usage
function App() {
return (
<Virtuoso
style={{ height: "400px" }}
totalCount={200}
overscan={3} // ----------> this line does not work
itemContent={(index) => {
return <ExpensiveComponent>{index}</ExpensiveComponent>;
}}
/>
);
}
I missed this detail from the docs. Unlike react-window API, the overscan unit is pixel instead of row in virtual list, in my case I need to increase the overscan to 900px and it seems to be working now.
<Virtuoso
style={{ height: "400px" }}
totalCount={200}
overscan={900}
itemContent={(index) => {
return <ExpensiveComponent>{index}</ExpensiveComponent>;
}}
/>
Related
I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}
I am developing an app which allows the user to search for books and then display it in the search results. For displaying the results, I am using a FlatList with 3 columns and displaying the book cover and some basic information about the book.
I am storing the results from the API response in state without the comoponent. As more results are added, the memory consumption increases but the data is in JSON format, no images are store in state.
I have tried, using removeClippedSubviews and few other options that allow setting the window size but that has little to no difference on the memory usage.
Am I missing something here or is there a way to optimise this? Sample code is uploaded to this github repo
Here is the code snippet I am using:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* #format
* #flow strict-local
*/
import type { Node } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
FlatList,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
useColorScheme,
View,
} from 'react-native';
import { Button, SearchBar, useTheme } from 'react-native-elements';
import { searchBooks } from './api/GoogleBooksService';
import HttpClient from './network/HttpClient';
import BookCard from './components/BookCard';
const searchParamsInitialState = {
startIndex: 1,
maxResults: 12,
totalItems: null,
};
let debounceTimer;
const debounce = (callback, time) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(callback, time);
};
const isEndOfList = searchParams => {
const { startIndex, maxResults, totalItems } = searchParams;
if (totalItems == null) {
return false;
}
console.log('isEndOfList', totalItems - (startIndex - 1 + maxResults) < 0);
return totalItems - (startIndex - 1 + maxResults) < 0;
};
const App: () => Node = () => {
const isDarkMode = useColorScheme() === 'dark';
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [globalSearchResults, setGlobalSearchResults] = useState([]);
const [searchParams, setSearchParams] = useState(searchParamsInitialState);
let searchCancelToken;
let searchCancelTokenSource;
// This ref will be used to track if the search Term has changed when tab is switched
const searchRef = useRef();
const clearSearch = () => {
console.log('Clear everything!');
searchRef.current = null;
setGlobalSearchResults([]);
setSearchParams(searchParamsInitialState);
setIsLoading(false);
searchCancelTokenSource?.cancel();
searchCancelToken = null;
searchCancelTokenSource = null;
};
useEffect(() => {
debounce(async () => {
setIsLoading(true);
await searchGlobal(searchTerm);
setIsLoading(false);
}, 1000);
}, [searchTerm]);
/**
* Search method
*/
const searchGlobal = async text => {
if (!text) {
// Clear everything
clearSearch();
return;
}
setIsLoading(true);
try {
// Use the initial state values if the search term has changed
let params = searchParams;
if (searchRef.current !== searchTerm) {
params = searchParamsInitialState;
}
const { items, totalItems } = await searchBooks(
text,
params.startIndex,
params.maxResults,
searchCancelTokenSource?.token,
);
if (searchRef.current === searchTerm) {
console.log('Search term has not changed. Appending data');
setGlobalSearchResults(prevState => prevState.concat(items));
setSearchParams(prevState => ({
...prevState,
startIndex: prevState.startIndex + prevState.maxResults,
totalItems,
}));
} else {
console.log(
'Search term has changed. Updating data',
searchTerm,
);
if (!searchTerm) {
console.log('!searchTerm', searchTerm);
clearSearch();
return;
}
setGlobalSearchResults(items);
setSearchParams({
...searchParamsInitialState,
startIndex:
searchParamsInitialState.startIndex +
searchParamsInitialState.maxResults,
totalItems,
});
}
searchRef.current = text;
} catch (err) {
if (HttpClient.isCancel(err)) {
console.error('Cancelled', err.message);
}
console.error(`Error searching for "${text}"`, err);
}
setIsLoading(false);
};
const renderGlobalItems = ({ item }) => {
return <BookCard book={item} />;
};
const { theme } = useTheme();
return (
<SafeAreaView style={styles.backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
/>
<View style={styles.container}>
<SearchBar
showLoading={isLoading}
placeholder="Enter search term here"
onChangeText={text => {
setSearchTerm(text);
}}
value={searchTerm}
platform={Platform.OS}
/>
{isLoading && globalSearchResults.length <= 0 && (
<ActivityIndicator animating style={styles.loader} />
)}
{globalSearchResults.length > 0 && (
<FlatList
removeClippedSubviews
columnWrapperStyle={styles.columnWrapper}
data={globalSearchResults}
numColumns={3}
showsHorizontalScrollIndicator={false}
keyExtractor={item => item + item.id}
renderItem={renderGlobalItems}
ListFooterComponent={
<>
{!isLoading &&
!isEndOfList(searchParams) &&
searchParams.totalItems > 0 && (
<Button
type="clear"
title="Load more..."
onPress={async () => {
await searchGlobal(searchTerm);
}}
/>
)}
{isLoading && searchParams.totalItems != null && (
<ActivityIndicator
size="large"
style={{
justifyContent: 'center',
}}
color={theme.colors.primary}
/>
)}
</>
}
/>
)}
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
backgroundStyle: 'white',
container: {
height: '100%',
width: '100%',
},
columnWrapper: {
flex: 1,
},
loader: {
flex: 1,
justifyContent: 'center',
},
});
export default App;
There is something called PureComponent in react native. If you create FlatList as PureComponent, you can see lot of improvement.
It will not rerender items until data has been changed.
for example:
class MyList extends React.PureComponent {
}
For more reference check this
Can you try to chuck your array of list items into small sub-arrays, this package uses this mechanism https://github.com/bolan9999/react-native-largelist
The package has been praised by complex app teams including the Discord Mobile Team - https://discord.com/blog/how-discord-achieves-native-ios-performance-with-react-native
Can't resize React-Player Video.
I change the width to any number and the video stays unchanged.
I am trying to optimize it for computer view and then add breakpoints to resize for smaller screens like phones.
Bellow is the file where I render the React-Player video inside a on which I apply the desired height and width I would like my react-player video to adopt.
import React, { useContext, useEffect, useState } from 'react'
import { makeStyles } from '#material-ui/core/styles'
import Modal from '#material-ui/core/Modal'
import { MovieContext } from './MovieContext'
import ReactPlayer from 'react-player'
import { getTrailer } from '../utils/movieDB'
// potential of adding controls
// import { Slider, Direction } from 'react-player-controls'
//slider to be implemented
//https://www.npmjs.com/package/react-player-controls#playericon-
const useStyles = makeStyles((theme) => ({
video: {
width: 'auto',
height: 'auto',
top: '25%',
right: '25%',
position: 'fixed',
[theme.breakpoints.down('xs')]: {},
},
}))
const styles = {
player: {
width: '300px',
},
}
export default function SimpleModal({ open }) {
const classes = useStyles(),
//receives movie from Home > DisplayCard > MovieContext
{ setOpenTrailer, movie, setMovie } = useContext(MovieContext),
[trailer, setTrailer] = useState(),
[key, setKey] = useState(),
[modalStyle] = useState()
useEffect(() => {
if (movie) {
getTrailer(movie).then((data) => {
setKey(data.videos.results[0].key)
setTrailer(data)
})
}
}, [movie])
const handleOpen = () => {
setOpenTrailer(true)
}
const handleClose = () => {
setOpenTrailer(false)
setMovie(undefined)
setTrailer(undefined)
setKey(undefined)
}
const renderVideo = (
<>
{key && (
<div className={classes.video}>
<ReactPlayer style={styles.player} url={`https://www.youtube.com/watch?v=${key}`} />
</div>
)}
</>
)
return (
<div>
<Modal
open={open || false}
onClose={handleClose}
aria-labelledby="simple-modal-title"
aria-describedby="simple-modal-description"
>
{renderVideo}
</Modal>
</div>
)
}
Which is the proper way to set the dimensions on a react-player object?
Using the width and height props as such:
<ReactPlayer
width={“300px”}
url={`https://www.youtube.com/watch?v=${key}`} />
this code is from the react-player library. This made my video responsive
its from: https://github.com/cookpete/react-player
class ResponsivePlayer extends Component {
render () {
return (
<div className='player-wrapper'>
<ReactPlayer
className='react-player'
url='https://www.youtube.com/watch?v=ysz5S6PUM-U'
width='100%'
height='100%'
/>
</div>
)
}
}
----- CSS ------
.player-wrapper {
position: relative;
padding-top: 56.25% /* Player ratio: 100 / (1280 / 720) */
}
.react-player {
position: absolute;
top: 0;
left: 0;
}
I am using material-table (https://material-table.com/#/) which is built with React.
I have data coming in as a prop to material-table like shown in the code below.
I usually click a button in the parent component to change the prop in the Performancetbl component. But when i click on the button once, table is not rerendering with new data. When I click on it one more time, it rerenders though. Why is that happening?
I tried to save props into a state variable state in Performancetbl component, but that did not change the behavior at all.
I also tried console.log(props.datas) to see if correct data is appearing the first time I click on the button. And it is indeed the correct value! Can you guys figure out why this is happening?
function Performancetbl(props) {
const options = {
...
};
console.log(props.datas)
return(
<div style={{ maxWidth: "100%" }}>
<MaterialTable
title="Overall"
data={props.datas}
columns={props.columns}
options={options}
components={props.components}
/>
</div>
);
}
export default Performancetbl;
Thanks!
The reason this is most likely happening to you is because you are rendering the table before data has arrived.
Please see the following demo on how to grab data from an API and pass it via props.
You can view a live demo here
ParentComponent.js
import React, { useState } from "react";
import AppTable from "./AppTable";
export default function ParentComponent() {
const [tableData, setTableData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const columns = [
{
title: "Id",
field: "id"
},
{
title: "UserId",
field: "userId"
},
{
title: "Title",
field: "title"
},
{
title: "Completed",
field: "completed"
}
];
const tableDiv = {
marginTop: "30px"
};
const shadowStyle = {
boxShadow: "0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)"
};
const btnStyle = {
height: "40px",
width: "300px",
fontSize: "24px",
cursor: "pointer",
...shadowStyle
};
const headStyle = {
textAlign: "center",
padding: "20px",
backgroundColor: "lightcoral",
...shadowStyle
};
const sleep = time => {
return new Promise(resolve => setTimeout(resolve, time));
};
const fetchData = async () => {
setIsLoading(true);
// Add a timeout to give the appearance of long load times
await sleep(3000);
try {
const resp = await fetch("https://jsonplaceholder.typicode.com/todos");
const json = await resp.json();
setTableData(json);
} catch (err) {
console.trace(err);
alert(err.message + "\r\n\r\nSee console for more info.");
}
setIsLoading(false);
};
return (
<div>
<div style={headStyle}>
<h1>Click button to get data</h1>
<button style={btnStyle} onClick={fetchData}>
Click Me To Get API Data
</button>
</div>
<div style={tableDiv}>
<AppTable data={tableData} columns={columns} isLoading={isLoading} />
</div>
</div>
);
}
AppTable.js (uses material-table)
import React from "react";
import MaterialTable from "material-table";
import tableIcons from "./TableIcons.js";
export default function AppTable({ data, columns, ...rest }) {
return (
<MaterialTable
{...rest}
icons={tableIcons}
columns={columns}
data={data}
/>
);
}
I'm trying to do a loading bar with fixed timeout, says within 5 seconds, the bar should all filled up. I'm able to write the html and css but stuck in the js logic.
function App() {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setTick(tick => tick + 10); //some calculation is missing
}, 1000);
setTimeout(() => {
clearInterval(id);
}, 5000);
return () => clearInterval(id);
}, []);
return (
<div className="App">
<div
style={{
width: "100%",
background: "yellow",
border: "1px solid"
}}
>
<div
style={{
height: "10px",
background: "black",
width: tick + "%"
}}
/>
</div>
</div>
);
}
https://codesandbox.io/s/proud-architecture-fuwcw
I refactored your code a little.
I created 3 constants:
maxLoad: Is the percentage to cover, in your case a 100%.
fulfillInterval: It's the interval to fill a step in the bar.
step: It's the calculation of the width to fill in the present iteration.
Then I changed a while the code adding 1 milisecond to the clearTimeout to ensure that it's going to work and... it's working. :)
Hope this helps.
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [tick, setTick] = useState(0);
const maxLoad = 100; // total percentage to cover
const fulfillInterval = 5000; // clear interval timeout
const step = maxLoad/(fulfillInterval/1000); // % filled every step
useEffect(() => {
const id = setInterval(() => {
setTick(tick => tick + step); // No dependency anymore
}, 1000);
setTimeout(() => {
clearInterval(id);
}, fulfillInterval+1);
return () => clearInterval(id);
}, []);
return (
<div className="App">
<div
style={{
width: "100%",
background: "yellow",
border: "1px solid"
}}
>
<div
style={{
height: "10px",
background: "black",
width: tick + "%"
}}
/>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useEffect(() => {
const id = setInterval(() => {
if(tick !==100)
setTick(tick => tick + 10); // No dependency anymore
}, 1000);
setTimeout(() => {
clearInterval(id);
}, 5000);
return () => clearInterval(id);
}, [tick])
Replace your useEffect function like this.