I'm using the react-infinite-scroll-component to implement the infinite scroll component. The configuration of the component is:
<div id="scrollableDiv" style={{ height: 300, overflow: "auto" }}>
<InfiniteScroll
dataLength={texts.length}
next={getText}
hasMore={true}
loader={<h5>Loading...</h5>}
endMessage={<p style={{textAlign:'center'}}><b>Yay! You've seen it all!</b></p>}
scrollableTarget="scrollableDiv"
>
{<Texts texts = {texts}/>}
</InfiniteScroll>
</div>
where texts is simply a state array of some text objects; const [texts, setTexts] = useState([])
This is the getText method to be called in next :
const getText = async ()=>{
const res = await axios.get("http://localhost:3001/api/sth",{
params: {
ids: followings.map(i => i.id),
str_ids: followings.map(i => i.str_id)
},
paramsSerializer: params => {
return qs.stringify(params)
}
});
let str_ids_res = res.data.map(i => i.string_id)
let texts_res = res.data.map(i => i.texts)
console.log(texts_res)
const filtered_res = //some processing here on texts_res
/* eslint eqeqeq: 0 */
if(!arrayIsEqual(filtered_res,texts)){
console.log("Not equal")
// append arrays to a array state
setTexts((prevtexts) => prevtexts.concat([...filtered_res]))
}
}
await axios.get("http://localhost:3001/api/sth")
always return two different texts from DB so setTexts should always get called.
component is a card component from semantic UI tho.
const Texts = ({texts}) =>{
return (
<>
{texts.map(text =>(
<div key={text.id}>
<Card>
<Card.Content>
<Card.Description>
{text.full_text}
</Card.Description>
</Card.Content>
</Card>
</div>
))}
</>
);
}
InfiniteScroll component only fires Next once even though my datalength variable is setting correctly.
PS. the texts is empty initially so when I start the app, only 'Loading....' is called
After some experiments, passing height = {some height} as props to InfiniteScroll does the trick..
<div id="scrollableDiv" style={{ height: 300, overflow: "auto" }}>
<InfiniteScroll
dataLength={tweets.length}
next={handleScrolling}
hasMore={!isEnd}
loader={<h5>Loading...</h5>}
scrollThreshold={1}
height={300}
endMessage={
<p style={{textAlign:'center'}}><b>Yay! You've seen it all!</b></p>
}
scrollableTarget="scrollableDiv"
>
{<Tweets tweets = {tweets}/>}
</InfiniteScroll>
</div>
Please do not initialize 'texts' with an empty array, you must initialize the 'texts' array with some array of objects (for example 10 or 20).
In the initial render of your code (at the first time). you can just fire an API in
useEffect(()=>{
// make API call which your firing in the next call function
},[])
So your component gets initialization then after it will start behaving as expected.
For your understanding, Please refer to the sample example in "code sandbox"
https://codesandbox.io/s/elated-danny-k4hylp?file=/src/index.js
Related
I have a component which take a children as props and render it inside a div which has a callback ref.
const ShowMoreText = (props: ShowLessOrMoreProps) => {
const { children } = props;
const [showMore, setShowMore] = useState<boolean>(false);
const [showButton, setShowButton] = useState<boolean>(false);
const [childrenHeight, setChildrenHeight] = useState<number>(9.5);
const measureRef = useCallback((node: HTMLDivElement) => {
if (node) {
setChildrenHeight(node.getBoundingClientRect().height);
}
}, []);
useEffect(() => {
console.log(childrenHeight)// when use measureRef function it gives me 0 when using inline version this gives me correct height and I don't know why
if (childrenHeight > 45) {
setShowButton(true);
}
}, [childrenHeight]);
return (
<Stack
sx={{
alignItems: "flex-start",
}}
>
<Collapse in={showMore} collapsedSize={45}>
<div
// ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
ref={() => measureRef} // if I comment this and use the above commmented version everything works fine
>
{children}
</div>
</Collapse>
{showButton && ( //when using measureRef function this button won't display
<Button onClick={() => setShowMore((prev) => !prev)}>
{showMore ? "Show less" : "Show more"}
</Button>
)}
</Stack>
);
};
the problem is that when I use stable measureRef function the console log inside useeffect prints 0 but in inline ref version everythings works fine. can anyone explain me why ?
ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
You return a function but ref shoulb be an Object (of certain type)
Hence You probably return void function.
if You want to pass (node: HTMLDivElement) => { // setChildrenHeight(node?.getBoundingClientRect().height); // } use unreserved prop then.
props.ref is reserved for useRef() hook .
Finally :
If for some reason Yuu want to save such contruction, do thi that way:
(node: HTMLDivElement) => {
return setChildrenHeight(node?.getBoundingClientRect().height);
}}
Where setChildrenHeight() returns correct object type proper po ref.
I found the solution and share it here in case anyone is curious or have a same problem.
The problem was that the children that I passed to this component was something like this:
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
and because of the delay of request the div height was actually 0 at the beginning. so the actual code for callback ref worked correct and the problem raised because of the api call delay. the solution for me and the mistake that i had been made was not to render the above code conditionally like this:
{dataIsLoading &&
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
}
hopefully this can help someone else.
I have a mapped component which iterates through API data. It passes props to each one and therefore each card looks different. See example below.
https://gyazo.com/39b8bdc4842e5b45a8ccc3f7ef3490b0
With the following, I would like to achieve two goals:
When the component is selected, it uses state to STAY SELECTED, and changes the colour as such to lets say blue for that selected component.
I hope this makes sense. How do I index a list as such and ensure the colour and state remains active based on this selection?
See below.
The level above, I map the following cards using these props.
{
jobs.length > 0 &&
jobs.map(
(job) =>
<JobCard key={job.id} job={job}
/>)
}
I am then using the following code for my components:
const JobCard = ({ job }) => {
const responseAdjusted = job.category.label
const responseArray = responseAdjusted.split(" ")[0]
return (
<CardContainer>
<CardPrimary>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1">
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" fontSize="inherit">
<FavoriteIcon fontSize="inherit"
onClick={()=> setOpen(prevOpen => !prevOpen)}/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color="text.secondary">
{job.company.display_name}
</Typography>
</CardHeader>
<CardSecondary>
</CardSecondary>
</CardPrimary>
</CardContainer>
)
}
You can attach a handler on the <CardPrimary> component by passing a function to the onClick event. That way whenever you click anywhere on the card div, the function will be triggered.
const [isSelected, setIsSelected] = useState(false);
<CardPrimary onClick={() => setIsSelected(true)} className={isSelected ? "css-class-to-highlight-div" : undefined>
....
</CardPrimary>
If I'm understanding what you're asking for, which I believe is to have your component be highlighted when it is clicked, then you need to modify the 'CardContainer' component to render with an 'onClick' parameter.
Example:
function CardContainer(props) {
const cssClass = 'highlighted';
const my_id = props.id || 'need_an_id';
var clearExistingHighlight = () => [...document.getElementByClassName(cssClass)].forEach((elem)=>elem.classList.remove(cssClass));
var isHighlighted = () => document.getElementById(my_id).classList.contains(cssClass);
var setHighlighted = (e) => {
clearExistingHighlight();
e.target.classList.add(cssClass);
}
return (
<div id={my_id} onClick={setHighlighted}>Cheeseburger fry</div>
)
}
If you don't want the highlight to disappear, you can get rid of the clearExistingHighlight function. Or if you want it to toggle, I recommend a modification of #sid's answer:
const {useState} = React;
function CardContainer(props) {
const [isSelected, setIsSelected] = useState(false);
<div onClick={() => setIsSelected(!isSelected)} className={isSelected ? "highlighted" : undefined>
}
style.css:
.highlighted {
background-color: 'orange';
}
You can do all of this without any react hook and rely instead on CSS classes. You can use the 'isHighlighted' method to determine if a given component is highlighted or not.
I am using Material-UI within my ReactJS app to create a table that, when clicked, expands to show more detailed info (a new row just beneath the clicked row). As example, here is a minimal toy example:
https://codesandbox.io/s/material-collapse-table-forked-t6thz
The code relevant to the problem is:
<Collapse
in={open}
timeout="auto"
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
}}
mountOnEnter
unmountOnExit
>
<div>
{/* actual function calls here; produces JSX output */}
{console.log("This should not execute before expanding!")}
Hello
</div>
</Collapse>;
Do note that the console.log() statement is just a simple replacement for my actualy functionality, which involves some API calls that are made when a row is clicked, and the corresponding info is displayed. So instead of console.log() I would actually call some other function.
I find that the console.log() statement executed on initial page render itself, even though in=false initially. How can I prevent this? Such that the function calls take place only when the Collapse is expanded. I initially thought this would be automatically handled by using mountOnEnter and unmountOnExit, but that does not seem to be the case. Any help would be appreciated, that could fix this problem in the sample example above.
I am working on an existing open source project, and therefore do not have the flexibility to restructure the existing codebase a lot. I would ideally have loved to implement this differently, but don't have that option. So posting here to know what options I might have given the above scenario. Thanks.
Problem
The children are rendered on initial load because they're defined within the Row component.
Solution
Move the Collapse children to its own React component. This won't render the children until the Collapse is opened. However, it'll re-render the child component when Collapse is closed. So depending on how you're making the API call and how other state interacts with this component, you may want to pass open to this component and use it as an useEffect dependency.
For example:
const Example = ({ open }) => {
React.useEffect(() => {
const fetchData = async () => {...};
if(open) fetchData();
}, [open]);
return (...);
}
Demo
Code
A separate React component:
const Example = ({ todoId }) => {
const [state, setState] = React.useState({
error: "",
data: {},
isLoading: true
});
const { data, error, isLoading } = state;
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`
);
if (res.status !== 200) throw String("Unable to locate todo item");
const data = await res.json();
setState({ error: "", data, isLoading: false });
} catch (error) {
setState({ error: error.toString(), data: {}, isLoading: false });
}
};
fetchData();
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
return (
<div
style={{
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : isLoading ? (
<p>Loading...</p>
) : (
<>
<div>
<strong>Id</strong>: {data.id}
</div>
<div>
<strong>Title</strong>: {data.title}
</div>
<div>
<strong>Completed</strong>: {data.completed.toString()}
</div>
</>
)}
</div>
);
};
The Example component being used as children to Collapse (also see supported Collapse props):
<Collapse
in={open}
timeout="auto"
// mountOnEnter <=== not a supported prop
// unmountOnExit <=== not a supported prop
>
<Example todoId={todoId + 1} />
</Collapse>
Other Thoughts
If the API data is static and/or doesn't change too often, I'd recommend using data as a dependency to useEffect (similar to the open example above). This will limit the need to constantly query the API for the same data every time the same row is expanded/collapsed.
Firstly, huge thanks to Matt for his detailed explanation. I worked through his example, and expanded on it to work for me as required. The main takeaway for me was: "Move the Collapse children to its own React component."
The solution posted by Matt above, I felt, didn't completely solve the problem for me. E.g. if I add a console.log() statement to the render() of the new child component (<Example>), I still see it being executed before it is mounted.
Adding mountOnEnter and unmountOnExit solved this problem:
But as Matt mentioned, the number of times the children were getting rendered was still a problem. So I slightly changed some bits (also simplified the code a bit):
Essentially, I do this now:
My child component is:
function Example(props) {
return (
<div
style={{
fontSize: 100,
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{props.flag && console.log("This should not execute before expanding!")}
{props.value}
</div>
);
}
and I call it from the parent component as:
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" mountOnEnter unmountOnExit>
<Example value={row.name} flag={open} />
</Collapse>
</TableCell>
</TableRow>
Note that the parameter flag is essential to avoid the function execution during closing of the <Collapse>.
I'm working with react-infinite-scroll-component library and randomuser.me api. I can fetch the data and the infinite scroll works just fine but the problem I have is that since the component makes a request at every scroll, I'm getting repeatable results.
I've found a workaround that removes arrays that are the same but it is not working properly with the infinite scroll package.
This is my code:
function App() {
const [people, setPeople] = useState([]);
const fetchPeopleImages = () => {
axios.get(`https://randomuser.me/api/?results=30&nat=br`).then((res) => {
const result = res.data.results;
setPeople([...people, ...result]);
});
// if (people.length > 30) {
// const removedDuplicatedPeople = people.filter(
// (ele, ind) =>
// ind ===
// people.findIndex(
// (elem) => elem.picture.medium === ele.picture.medium,
// ),
// );
// setPeople([...people, removedDuplicatedPeople]);
// }
};
useEffect(() => {
fetchPeopleImages();
// commented below because ESLINT was asking me to use useCallback
// inside fetchPeopleImage func. idk why
// eslint-disable-next-line
}, []);
return (
<div className="App">
<InfiniteScroll
dataLength={people.length}
next={() => fetchPeopleImages()}
hasMore={true}
loader={<h4>Carregando...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
{people.length > 1 &&
people.map((people, i) => (
<div>
<img src={people.picture.medium} alt="Imagem de uma pessoa" />
</div>
))}
</InfiniteScroll>
</div>
);
}
export default App;
What is commented was the workaround I've found to remove arrays that have the same image link.
Codesandbox: https://codesandbox.io/s/gracious-wildflower-opjx5?file=/src/App.js
Ideally don't do the filtering business specially with frequent fetches (like scroll). It is better if you maintain a state say page and pass it to your api and the api should return the data.
The problem with the inconsistency you are facing is due to the limitations of randomuser.me api. The api has only limited images to serve, so it will mix up the names, ages, images etc and tries its best to serve unique records. Hence you will often see duplicate images. You can check by rendering the name along with image and you will see 2 same images will have different names.
Some suggestions to solve your issue:
- provide a low value to results query param say 10
- use seed option to the api
- on every scroll increment the page and pass it to the api query param
See updated demo here - it works to some extent
Note - you are still not guaranteed to see unique images rendered. However, you can use the updated code and it will work when you use it with real api.
updated code snippet
function App() {
const [people, setPeople] = useState([]);
const [page, setPage] = useState(0);
const fetchPeopleImages = () => {
axios
.get(`https://randomuser.me/api/?results=10&nat=br&page=${page}&seed=abc`)
.then(res => {
const result = res.data.results;
setPeople([...people, ...result]);
setPage(prev => prev + 1);
});
console.log("page", page);
};
useEffect(() => {
fetchPeopleImages();
// commented below because ESLINT was asking me to use useCallback inside
// fetchPeopleImage func. idk why
// eslint-disable-next-line
}, []);
return (
<div className="App">
<InfiniteScroll
dataLength={people.length}
next={() => fetchPeopleImages()}
hasMore={true}
loader={<h4>Loading.....</h4>}
endMessage={
<p style={{ textAlign: "center" }}>
<b>Yay! You have seen it all</b>
</p>
}
>
{people.length > 1 &&
people.map((people, i) => (
<div key={i}>
<img src={people.picture.medium} alt="Person" />
<p>{people.name.first}</p>
</div>
))}
</InfiniteScroll>
</div>
);
}
I am new to React Hooks. I write a small code that displays a list of courses to the user. It contains 2 main components CourseList and Course and a custom hook useCourseList. Here is the code of the custom hook:
function useCourseList(searchString) {
const [courses, setCourses] = useState([]);
useEffect(() => {
function handleCourseListUpdating(nextCourses) {
setCourses(nextCourses);
}
getCourses(searchString, handleCourseListUpdating)
return () => {
setCourses([]);
}
}, [searchString]);
return courses;
}
The above code is fetching api data by a search string via getCourses function. Then it updates the retrieved data to courses state which is rendered by CourseList component. The useEffect function is triggered with searchString as its dependencies to keep updating courses' data once the search string changes.
The web browser refreshes many times corresponding with every single character change of the search string although the data results on those keyword changes are the same.
For example, we are looking for a game course with keyword "Game". Then it calls 4 times on useEffect one by one with "G", "Ga", "Gam", and "Game". Those keywords give the same results with my current api data but they are constantly updated React DOM via web interface. That leads to browser twinkling 4 times which is unnecessary. It also gives a bad user experience.
Is there any solution to prevent updating UI as data are still consistent? Is there any problem with the data flow?
For demonstration, you can see the full sample code here:
CodeSandbox
There are 2 changes you can make for a better ui experience
Debounce the API call for search
Do not set the state to empty in useEffect cleanup
You can choose to write your own custom debounce function or use it from a library such as lodash or underscore. Once you do that you can use useMemo to create a debounced instance of the getCourses function and call it inside useEffect
const debounce = (fn, delay) => {
let timer = null;
return function(...args) {
if (timer){
clearInterval(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
// A custom Hook
function useCourseList(searchString) {
const [courses, setCourses] = useState([]);
const getDebouncedCourses = useMemo(() => debounce(getCourses, 300), []);
useEffect(() => {
function handleCourseListUpdating(courses) {
setCourses(courses);
}
getDebouncedCourses(searchString, handleCourseListUpdating);
}, [searchString, getDebouncedCourses]);
return courses;
}
// CourseList component
function CourseList() {
const [searchString, setSearchString] = useState('');
const courses = useCourseList(searchString)
return (
<div>
{courses ? (
<div>
<TextField style={{ padding: 24 }}
id="searchInput"
placeholder="Search for Courses"
margin="normal"
onChange={event => setSearchString(event.target.value)}
/>
<Grid container spacing={4} style={{ padding: 24 }}>
{courses.map((course, index) => (
<Grid key={index} item xs={12} sm={6} lg={4} xl={3}>
<Course course={course} />
</Grid>
))}
</Grid>
</div>
) : "No courses found"}
</div>
);
}