React useState: unexpected behavior when partitioning infinite feed by time - reactjs

Context
I have a component that displays an infinite feed of posts. I want to detect the first element that is older than a day so I can display a "posted yesterday" banner above just that element (eventually doing for week and month).
Buggy Solution
I do this by checking if the post is older than a day when rendering my <li> items, but there is some weird behavior when I try to use a boolean state flag to prevent "posted yesterday" being attached to every post older than a day.
Code
Infinite Scroll Component:
export function InfiniteFeed(props: InfiniteFeedProps) {
const [page, setPage] = useState(0);
const [shouldDisplay, setShouldDisplay] = useState(true)
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [posts, setPosts] = useState<Post[]>([])
const [hasMore, setHasMore] = useState(false);
const nowMillis = DateTime.local({zone: 'utc'}).toMillis();
// have omitted dataFetching logic + ref observer logic
const timeCheck = (eventTimeMillis: number) => {
if (!shouldDisplay) {
return false
}
if (!olderThanDay(eventTimeMillis, nowMillis)) {
return false
}
setShouldDisplay(false) // setting this renders every time-check(post) to false
return true
}
return (
<div>
<FeedWrapper>
<Banner title="Infinite Feed"/>
<ul style={{padding: 0}}>
{posts.map((post, i) => (
<FeedElement
key={post.id}
ref={(post.length === i + 1) ? lastEventElementRef : () => null}
id={post.id}
display={timeCheck(post.eventTimestamp)} // if true, element prepends "posted yesterday"
// other props
/>
))}
</ul>
</FeedWrapper>
<div></div>
</div>
);
}
Behavior
If I don't do anything with "shouldDisplay" state, the feed correctly prepends every post older than a day with "Posted Yesterday". However, if I set shouldDisplay to false as seen in the code, timeCheck() is always false and "posted yesterday is not displayed".
Ask
Are there any pointers to what is going wrong here?

I had overlooked that setState() causes a re-render, thus making timeCheck() return false every time. To get my desired behavior of a time partitioned feed, I calculated which indices should prepend a time string when I fetched my posts, I then set shouldDisplay() = true if the elements index were in this array. Here is my code
Solution
export function InfiniteFeed(props: InfiniteFeedProps) {
const [page, setPage] = useState(0);
const [shouldDisplay, setShouldDisplay] = useState(true)
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [posts, setPosts] = useState<Post[]>([])
const [hasMore, setHasMore] = useState(false);
const [timePartitions, setTimePartitions] = useState<number[]>([]) // indices of time buckets
const now = DateTime.local({zone: 'utc'});
// using luxon library for handling time and using
// Duration api to account for the variable month lengths
const timeBuckets = [
now.minus(Duration.fromObject({month: 1})).toMillis(),
now.minus(Duration.fromObject({week: 1})).toMillis(),
now.minus(Duration.fromObject({day: 1})).toMillis()
];
// have omitted useRef dom observer logic
useEffect(() => {
// ...
CoreApi().post<GetEventsResponse>('posts', request)
.then((res) => {
const allPosts = [...posts, ...res.data.posts]
setPosts(allPosts);
calculateTimePartitions(allEvents)
// ...
})
}, [page])
const shouldDisplay = (index: number): boolean => {
return timePartitions.find(num => num == index) != undefined
}
// calculate the indexes where we want to prepend time banner
const calculateTimePartitions = (eventList: NewsEvent[]) => {
let indexes = [];
let intervals = timeBuckets
for (let i = 0; i < posts.length; i++) {
if (eventList[i].eventTimestamp < intervals[intervals.length - 1] ) {
intervals.pop()
indexes.push(i);
}
}
setTimePartitions(indexes)
}
return (
<div>
<FeedWrapper>
<Banner title="Infinite Feed"/>
<ul style={{padding: 0}}>
{posts.map((post, i) => (
<FeedElement
key={post.id}
ref={(post.length === i + 1) ? lastEventElementRef : () => null}
id={post.id}
display={shouldDisplay(i)} // if true, element prepends "posted yesterday"
// other props
/>
))}
</ul>
</FeedWrapper>
<div></div>
</div>
);
}
Feed element logic
export const FeedElement = forwardRef<any, NewsEvent & FeedElementProps>((props: Post & FeedElementProps, ref) => {
// can be more expensive
const getTimeBucket = () => {
const nowMillis = DateTime.local({zone: 'utc'})
const eventTime = DateTime.fromMillis(props.eventTimestamp, {zone: 'utc'})
const interval = Interval.fromDateTimes(eventTime, nowMillis)
if (interval.length('day') > 1 && interval.length('day') < 2) {
return 'Posted yesterday'
}
if (interval.length('day') > 2 && interval.length('week') < 1) {
return 'Posted this week'
}
if (interval.length('week') > 1 && interval.length('month') < 1) {
return 'Posted this month'
}
else if (interval.length('month') > 1) {
return 'Posted over a month ago'
}
else {
return '';
}
}
return (
<div>
{props.display ? (
<Banner title={getTimeBucket()} />
) : (<></>)}
<div>
<li>
{/*other stuff*/}
</li>
</div>
</div>
)},
);

Related

How to swipe card programatticaly in react using react-tinder card

I am fetching data from the Backend and loading them in the card using react-tinder-card
Swiping works properly but unable to swipe using the buttons
I follow the documentation but still did not work
Here is the sample
Swiping gestures are working fine.
But when implement by checking documentation things did not work
Things are not working and tomorrow is my project day
import React, { useEffect, useState, useContext, useRef } from "react";
function Wink() {
const [people, setPeople] = useState([]);
const [loading, setLoading] = useState(false);
const [currentIndex, setCurrentIndex] = useState(people.length - 1)
const [lastDirection, setLastDirection] = useState()
// used for outOfFrame closure
const currentIndexRef = useRef(currentIndex)
const childRefs = useMemo(
() =>
Array(people.length)
.fill(0)
.map((i) => React.createRef()),
[]
)
const updateCurrentIndex = (val) => {
setCurrentIndex(val)
currentIndexRef.current = val
}
const canGoBack = currentIndex < people.length - 1
const canSwipe = currentIndex >= 0
// set last direction and decrease current index
const swiped = (direction, nameToDelete, index) => {
setLastDirection(direction)
updateCurrentIndex(index - 1)
}
const outOfFrame = (name, idx) => {
console.log(`${name} (${idx}) left the screen!`, currentIndexRef.current)
// handle the case in which go back is pressed before card goes outOfFrame
currentIndexRef.current >= idx && childRefs[idx].current.restoreCard()
// TODO: when quickly swipe and restore multiple times the same card,
// it happens multiple outOfFrame events are queued and the card disappear
// during latest swipes. Only the last outOfFrame event should be considered valid
}
const swipe = async (dir) => {
if (canSwipe && currentIndex < db.length) {
await childRefs[currentIndex].current.swipe(dir) // Swipe the card!
}
}
// increase current index and show card
const goBack = async () => {
if (!canGoBack) return
const newIndex = currentIndex + 1
updateCurrentIndex(newIndex)
await childRefs[newIndex].current.restoreCard()
}
useEffect(() => {
setLoading(true);
axios
.post("http://localhost:4000/api/all-profile", { email })
.then(function (response) {
setPeople(response.data);
setCurrentIndex(response.data.length);
}
}, []);
return (
<div className="DateMainDiv">
<Header />
<div className="ProfieCards">
{people.map((person) => (
<TinderCard
className="swipe"
key={person.email}
ref={childRefs[index]}
preventSwipe={swipe}
onSwipe={(dir) => swiped(dir, person.name, person.email)}
onCardLeftScreen={onCardLeftScreen}
onCardUpScreen={onCardUpScreen}
>
<div
style={{ backgroundImage: `url(${person.image})` }}
className="Winkcard"
>
<img
onLoad={handleLoad}
src={person.image}
alt="Image"
className="TinderImage"
/>
<h3>
{person.name}{" "}
<IconButton
style={{ color: "#fbab7e" }}
onClick={() => handleOpen(person.email)}
>
<PersonPinSharpIcon fontSize="large" />
{parseInt(person.dist/1000)+"KM Away"}
</IconButton>
</h3>
</div>
</TinderCard>
))}
<SwipeButtonsLeft onClick={()=>{swipe("left")}} />
<SwipeButtonsLeft onClick={()=>{goback()}} />
<SwipeButtonsLeft onClick={()=>{swipe("right")}} />
</div>
</div>
);
}
export default Wink;

component doesn't render after mergin two APIS

After I request two endpoints and store it in a new state variable I'm not being able to render the component after the state changes. When i assign the state variable to the dependency array of useEffect it renders infinitely.
I tried a few things but the only way that i've being able to do to render the component after it loads has been just adding the merge state to the dependency array.
import { ChangeEvent, FC, useEffect, useState } from "react";
import spacex from "../api/spacex";
import CardGrid from "../components/CardGrid";
import Header from "../components/Header";
import Pagination from "../components/Pagination";
import SkeletonGrid from "../components/SkeletonGrid";
type Launch = {
mission_name: string;
};
const LaunchesMain: FC = () => {
const [launches, setLaunches] = useState<any>([]);
const [rockets, setRockets] = useState<any>([]);
const [merged, setMerged] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [postsPerPage, setPostsPerPage] = useState(9);
const [searchTerm, setSearchTerm] = useState("");
const [filteredResult, setFilteredResult] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchRockets = async () => {
const responseRocket = await spacex.get("/rockets");
const responseLaunches = await spacex.get("/launches");
setRockets(responseRocket.data);
setLaunches(responseLaunches.data);
};
fetchRockets().then(() => {
const mergedApis = () => {
const launchesCopy: any = [...launches];
for (let i = 0; i < launches.length; i++) {
for (let j = 0; j < rockets.length; j++) {
if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
launchesCopy[i].rocket = rockets[j];
}
}
}
setMerged(launchesCopy);
setIsLoading(false);
};
mergedApis();
});
}, []);
console.log(merged);
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
if (searchTerm.length === 0) {
setFilteredResult(merged);
} else if (searchTerm.length > 0) {
const filteredData = merged.filter((launch: Launch) => {
return `${launch.mission_name}`
.toLowerCase()
.includes(searchTerm.toLowerCase());
});
setFilteredResult(filteredData);
}
};
const lastPostIndex = currentPage * postsPerPage;
const firstPostIndex = lastPostIndex - postsPerPage;
const currentPosts = merged.slice(firstPostIndex, lastPostIndex);
return (
<>
<Header />
<div className="text-white">
<div>
<input
style={{
background:
"linear-gradient(0deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05)), #121212",
}}
onChange={(event) => handleSearchChange(event)}
placeholder="Search all launches..."
value={searchTerm}
className="md:w-[26rem] w-[16rem] h-[3rem] rounded-lg mt-10 mx-5 md:mx-24 rounded-3"
/>
</div>
<div className="mx-5 md:ml-24 mt-5 opacity-40">
Total({currentPosts.length})
</div>
{isLoading ? (
<SkeletonGrid cards={postsPerPage} />
) : (
<CardGrid
postsData={currentPosts}
filteredResult={filteredResult}
searchTerm={searchTerm}
/>
)}
<Pagination
totalPosts={merged.length}
postsPerPage={postsPerPage}
setCurrentPage={setCurrentPage}
currentPage={currentPage}
/>
</div>
</>
);
};
export default LaunchesMain;
This is the code of the component. How can i solve this issue?
Since you need rockets and launches as a dependency of the useEffect, whenever they change, the useEffect is called, which calls the api, which changes, etc... However, you don't use rockets and launches states beyond merging them, and then you use the merged state.
So you don't have to store rockets and launches in the state. Use Promise.all() to get both data arrays in to .then() block, merge them, and store only the merged state:
useEffect(() => {
const fetchRockets = () => Promise.all(
spacex.get("/rockets"),
spacex.get("/launches")
])
fetchRockets()
.then(([responseRocket, responseLaunches]) => {
const rockets = responseRocket.data;
const launches = responseLaunches.data;
for (let i = 0; i < launches.length; i++) {
for (let j = 0; j < rockets.length; j++) {
if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
launches[i].rocket = rockets[j];
}
}
}
setMerged(launches);
setIsLoading(false);
});
}, []);

[solution]Why "Cards" still doesn't receive the passed value?

Why "Cards" still doesn't receive the passed value from selectedCountryInfo
I just tried passing await to the variable, still doesn't work. "Cards" still don't receive value.
<----solution: when there are have 2 setStates, should use 2 variables, not use 1 variable.(I guess if there are 3 setStates use 3 variables and so on)
I've been thinking about it for over 12 hours and can't think of a solution.
Because the default value of useState cannot put async/await.
(fetchedCountries is array,selectedCountryInfo is object)
const App = () => {
const [fetchedCountries, setFetchedCountries] = useState([]);
const [selectedCountryInfo, SetSelectedCountryInfo] = useState();
useEffect(() => {
const myCountries = async () => {
const countries = await worldWideCountries();
setFetchedCountries(countries);
SetSelectedCountryInfo(fetchedCountries[0]);
};
myCountries();
}, []);
return (
<div>
<Cards selectedCountryInfo={selectedCountryInfo} />
</div>
);
Solution:(from the 3 lines)
const countries = await worldWideCountries();
setFetchedCountries(countries);
const ww = countries[0];
SetSelectedCountryInfo(ww);
You probably want to use conditional rendering
const App = () => {
const [fetchedCountries, setFetchedCountries] = useState([]);
const [selectedCountryInfo, SetSelectedCountryInfo] = useState();
useEffect(() => {
const myCountries = async () => {
setFetchedCountries(await worldWideCountries());
SetSelectedCountryInfo(fetchedCountries[0]);
};
myCountries();
}, []);
return (
<div>
{ selectedCountryInfo && <Cards selectedCountryInfo={selectedCountryInfo} /> }
</div>
);
}

react - text input give me null

(in REACT)
i have app function :
function App() {
const [welcomeMenu, setWelcomeMenu] = useState(true);
const [gameMenu, setGameMenu] = useState(false);
const [username, setUsername] = useState('');
const welcomeMenuShow = () => {
setWelcomeMenu(false);
}
const getUserName = (value) => {
setUsername(value);
console.log(username);
};
return (
<div className="App">
{
welcomeMenu ? <WelcomeMenu gameStarter={welcomeMenuShow} getUserName={getUserName}/> : null
}
</div>
);
}
in welcomemenu component i pass getUserName function to get username which user input
next in Welcome menu i have :
const WelcomeMenu = ({ gameStarter, getUserName }) => {
return (
<div className="welcome-menu">
<WelcomeText />
<WelcomeBoard gameStarter={gameStarter} getUserName={getUserName}/>
</div>
)
};
i pass get User Name in second time
in WelcomeBoard i have:
const WelcomeBoard = ({ gameStarter, getUserName }) => {
const [text, setText] = useState('');
const [warning, setWarning] = useState(false);
const checkBtn = (event) => {
if(text) {
gameStarter();
} else {
setWarning(true);
setTimeout(() => {
setWarning(false);
}, 3000);
}
};
const handleChange = (event) => {
setText(event.target.value);
};
return (
<div className="welcome-board">
<div className="username">Please enter the name</div>
<input type="text" value={text} onChange={handleChange} className="username-input" />
<button className="username-btn" onClick={() => {
getUserName(text);
checkBtn();
}}>start</button>
{warning ? <Warning /> : null}
</div>
)
};
in input onchange i make state and pass the input value on text state
next on button i have on click which active 2 function:
getUserName(text) // text is a state text with input value
checkBtn()
and after a click button in app i activate getUserName(text), this function pass the text in username state and here is a problem
when i try to see this text console.log(username) - it's give me null
but it if i try to see value console.log(value) - i see my input text
i don't understand how to fix that
react setState is async, which means those state variables are updated in the NEXT RENDER CYCLE(think of it as a thread or buffer).
try running this code if you want to understand what is happening BEHIND THE SCENES.
let renderCount = 0;
function TestApp() {
renderCount++;
const [state, setState] = useState(0);
const someRef = useRef(0);
someRef.current = state;
const someCallback = () => {
const someValue = new Date().getTime();
setState(someValue);
console.log(someRef.current, renderCount);
setTimeout(() => {
console.log(someRef.current, renderCount);
},100)
}
return <button onClick={someCallback}>clickme<button>;
}

How to avoid NaN being rendered in audio player duration?

I built a cool little audio player and am having issues with data fetching. The page renders before the audio file in the return statement src, here:
<audio ref={audio} src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg" alt="oops, something went wrong..."></audio>
The NaN shows up in the duration time represented by this line:
{/* duration */}
<div className={styles.duration}>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>
This above line of code isn't preventing the NaN, so I tried my hand at fetching in the useEffect, shown below but that has made this issue worse.
const [data, setData] = useState([])
--------------------
useEffect(() => {
fetch("https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg").then(
res => setData(res.loadedmetadata)
)
})
--------------------
<audio ref={audio} src={data} alt="oops, something went wrong..."></audio>
If anyone could give it a look and point me in the right direction, id be very grateful. Below I will provide all the code for my component.
import React, { useState, useRef, useEffect } from 'react';
import styles from '../styles/AudioPlayer.module.css';
import {BsArrowClockwise} from 'react-icons/bs';
import {BsArrowCounterclockwise} from 'react-icons/bs';
import {BsPlayCircleFill} from 'react-icons/bs';
import {BsPauseCircleFill} from 'react-icons/bs';
const AudioPlayer = () => {
//state
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [data, setData] = useState([])
//refs
const audio = useRef();
const progressBar = useRef();
const progressBarAnimation = useRef();
//effects
useEffect(() => {
const seconds = Math.floor(audio.current.duration);
setDuration(seconds);
progressBar.current.max = seconds;
}, [ audio?.current?.loadedmetadata, audio?.current?.readyState ]);
useEffect(() => {
fetch("https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg").then(
res => setData(res.loadedmetadata)
)
})
//functions & Handlers
const calculateTime = (secs) => {
const minutes = Math.floor(secs / 60);
const returnedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(secs % 60);
const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${returnedMinutes}:${returnedSeconds}`;
}
const isPlayingHandler = () => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
if (!prevValue) {
audio.current.play();
progressBarAnimation.current = requestAnimationFrame(whilePlaying);
} else {
audio.current.pause();
cancelAnimationFrame(progressBarAnimation.current);
};
};
const whilePlaying = () => {
progressBar.current.value = audio.current.currentTime;
progressBarValueTicker();
progressBarAnimation.current = requestAnimationFrame(whilePlaying);
};
const progressHandler = () => {
audio.current.currentTime = progressBar.current.value;
progressBarValueTicker();
};
const progressBarValueTicker = () => {
progressBar.current.style.setProperty('--seek-before-width', `${progressBar.current.value / duration * 100}%`);
setCurrentTime(progressBar.current.value);
}
const backwardFifteen = () => {
console.log(progressBar.current.value)
progressBar.current.value = Number(progressBar.current.value) - 15;
console.log(progressBar.current.value)
progressHandler();
};
const forwardFifteen = () => {
console.log(progressBar.current.value)
progressBar.current.value = Number(progressBar.current.value) + 15;
console.log(progressBar.current.value)
progressHandler();
};
return(
<>
<div>
{/* eventually, a loop component tag will replace the below line to loop all audio file title and descriptions*/}
</div>
<div className={styles.audioWrapper}>
{/* eventually, a loop component tag will replace the below line to loop all audio files*/}
<audio ref={audio} src={data} alt="oops, something went wrong..."></audio>
{/* <audio ref={audio} src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg" alt="oops, something went wrong..."></audio> */}
<button className={styles.sideButtons} onClick={backwardFifteen}><BsArrowCounterclockwise />15</button>
<button className={styles.playPauseButton} onClick={isPlayingHandler}>
{ isPlaying ? <BsPauseCircleFill /> : <BsPlayCircleFill /> }</button>
<button className={styles.sideButtons} onClick={forwardFifteen}>15<BsArrowClockwise /></button>
{/* current time */}
<div className={styles.currentTime}>{calculateTime(currentTime)}</div>
{/* progress bar */}
<div>
<input type="range" ref={progressBar} className={styles.progressBar} onChange={progressHandler} defaultValue='0'/>
</div>
{/* duration */}
<div className={styles.duration}>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>
</div>
</>
);
};
export default AudioPlayer;
const onLoadedMetadata = ()=>{
const seconds = Math.floor(audioPlayer.current.duration);
setDuration(seconds);
progressBar.current.max = seconds;
}
<audio ref={audioPlayer} src={audio_file} preload="metadata" onLoadedMetadata={onLoadedMetadata}></audio>
The reason the NaN is rendered is because NaN is a falsy value and will return the value immediately in the expression below.
(duration && !isNaN(duration)) && calculateTime(duration)
// `NaN && !isNaN(NaN)` returns `NaN` because it is falsy
Simply removing the first condition will avoid NaN being rendered.
!isNaN(duration) && calculateTime(duration)
However, the actual duration value will still be NaN and nothing will get rendered. This is because when you check for the audio.current.duration value inside the useEffect the duration hasn't actually updated yet.
To solve this issue, you can listen to the onDurationChange event in the audio element and update the duration state variable when it gets triggered.
// Convert the `useEffect` code into a function instead
const onDurationChangeHandler = (e) => {
const seconds = Math.floor(e.target.duration);
setDuration(seconds);
progressBar.current.max = seconds;
};
<audio
ref={audio}
src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg"
alt="oops, something went wrong..."
onDurationChange={onDurationChangeHandler}
></audio>

Resources