I have this component:
export const Widget: FC<IWidgetProps> = ({ appid, flavor, showWidgetField, updateSpeed }) => {
const { state: geocodingState } = useContext(GeocodingContext);
const { dispatch: weatherDispatch } = useContext(WeatherContext);
const fetchCb = useCallback(() => {
if (geocodingState.coords) getWeatherInfo(weatherDispatch)(appid, geocodingState.coords.lon, geocodingState.coords.lat);
}, [geocodingState.coords]);
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (!interval) {
interval = setInterval(fetchCb, updateSpeed);
} else {
if (!geocodingState.coords) clearInterval(interval);
}
return () => {
if (interval) clearInterval(interval);
}
}, [geocodingState.coords]);
return (
<>
</>
);
};
Unfortunately, the interval does not get cleared when the geocodingState.coords is nullified nor in the useEffect return.
Because everytime you change geocodingState.coords, you creating new interval.... and the previous one is still running. Best solution for this is to save interval in useState, because othervise, your code doesnt know ID of interval, that needs to be cleared.
export const Widget: FC<IWidgetProps> = ({ appid, flavor, showWidgetField, updateSpeed }) => {
const { state: geocodingState } = useContext(GeocodingContext);
const { dispatch: weatherDispatch } = useContext(WeatherContext);
const [intervalId, setIntervalId] = useState();
const fetchCb = useCallback(() => {
if (geocodingState.coords) getWeatherInfo(weatherDispatch)(appid, geocodingState.coords.lon, geocodingState.coords.lat);
}, [geocodingState.coords]);
useEffect(() => {
if (!intervalId) {
interval = setInterval(fetchCb, updateSpeed);
setIntervalId(interval)
} else {
if (!geocodingState.coords) clearInterval(intervalId);
}
return () => {
if (intervalId){
clearInterval(interval);
setIntervalId(undefined);
}
}, [geocodingState.coords]);
return (
<>
</>
);
};
Related
I want to write a test case to cover the test useEffect, Which is depends on Appcontect value. Can someone please help on this please. Thanks.
export const ShareEmail = () => {
const {
nextData,
} = useAppContext();
const [topic, setTopic] = useState([]);
const generateTopics = () => {
const topics= nextData.filter(
(topic: { flag: boolean }) => !topic.flag
);
setTopic(topics)
}
useEffect(() => {
generateTopics();
}, [nextData]);
const generateEmail =() => {
const emailcontent = topics[0]
}
return (
< a
download="meeting.eml"
onClick={generateEmail}
>
send
</a>
);
}
Disclaimer: Please don't mark this as duplicate. I've seen similar questions with answers. But none of them is working for me. I'm just learning React.
What I'm trying to achieve is basically infinite scrolling. So that when a user scrolls to the end of the page, more data will load.
I've used scroll eventListener to achieve this. And it is working.
But I'm facing problems with the state of the variables.
First, I've changed the loading state to true. Then fetch data and set the state to false.
Second, when scrolling to the end of the page occurs, I again change the loading state to true. Add 1 with pageNo. Then again fetch data and set the loading state to false.
The problems are:
loading state somehow remains true.
Changing the pageNo state is not working. pageNo always remains to 1.
And actually none of the states are working as expected.
My goal: (Sequential)
Set loading to true.
Fetch 10 posts from API after component initialization.
Set loading to false.
After the user scrolls end of the page, add 1 with pageNo.
Repeat Step 1 to Step 3 until all posts loaded.
After getting an empty response from API set allPostsLoaded to true.
What I've tried:
I've tried adding all the states into dependencyList array of useEffect hook. But then an infinite loop occurs.
I've also tried adding only pageNo and loading state to the array, but same infinite loop occurs.
Source:
import React, { lazy, useState } from 'react';
import { PostSection } from './Home.styles';
import { BlogPost } from '../../models/BlogPost';
import { PostService } from '../../services/PostService';
const defaultPosts: BlogPost[] = [{
Id: 'asdfg',
Content: 'Hi, this is demo content',
Title: 'Demo title',
sections: [],
subTitle: '',
ReadTime: 1,
CreatedDate: new Date()
}];
const defaultPageNo = 1;
const PostCardComponent = lazy(() => import('./../PostCard/PostCard'));
const postService = new PostService();
const Home = (props: any) => {
const [posts, setPosts]: [BlogPost[], (posts: BlogPost[]) => void] = useState(defaultPosts);
const [pageNo, setPageNo] = useState(defaultPageNo);
const [pageSize, setPageSize] = useState(10);
const [loading, setLoading] = useState(false);
const [allPostsLoaded, setAllPostsLoaded] = useState(false);
const [featuredPost, setFeaturedPost]: [BlogPost, (featuredPost: BlogPost) => void] = useState(defaultPosts[0]);
async function getPosts() {
return await postService.getPosts(pageSize, pageNo);
}
async function getFeaturedPost() {
return await postService.getFeaturedPost();
}
function handleScroll(event: any) {
console.log('loading ' + loading);
console.log('allPostsLoaded ' + allPostsLoaded);
var target = event.target.scrollingElement;
if (!loading && !allPostsLoaded && target.scrollTop + target.clientHeight === target.scrollHeight) {
setLoading(true);
setPageNo(pageNo => pageNo + 1);
setTimeout(()=>{
getPosts()
.then(response => {
const newPosts = response.data.data;
setLoading(false);
if (newPosts.length) {
const temp = [ ...posts ];
newPosts.forEach(post => !temp.map(m => m.Id).includes(post.Id) ? temp.push(post) : null);
setPosts(temp);
} else {
setAllPostsLoaded(true);
}
})
}, 1000);
}
}
function init() {
setLoading(true);
Promise.all([getFeaturedPost(), getPosts()])
.then(
responses => {
setLoading(false);
setFeaturedPost(responses[0].data.data);
setPosts(responses[1].data.data);
}
);
}
React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
init();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []
);
return (
<PostSection className="px-3 py-5 p-md-5">
<div className="container">
<div className="item mb-5">
{posts.map(post => (
<PostCardComponent
key={post.Id}
Title={post.Title}
intro={post.Content}
Id={post.Id}
ReadTime={post.ReadTime}
CreatedDate={post.CreatedDate}
/>
))}
</div>
</div>
</PostSection>
);
};
export default Home;
Used more effects to handle the change of pageNo, loader and allPostsLoaded state worked for me.
Updated Source:
import React, { lazy, useState } from 'react';
import { Guid } from "guid-typescript";
import { PostSection } from './Home.styles';
import { BlogPost } from '../../models/BlogPost';
import { PostService } from '../../services/PostService';
import { Skeleton } from 'antd';
const defaultPosts: BlogPost[] = [{
Id: '456858568568568',
Content: 'Hi, this is demo content. There could have been much more content.',
Title: 'This is a demo title',
sections: [],
subTitle: '',
ReadTime: 1,
CreatedDate: new Date()
}];
const defaultPageNo = 1;
const defaultPageSize = 10;
const PostCardComponent = lazy(() => import('./../PostCard/PostCard'));
const postService = new PostService();
const Home: React.FC<any> = props => {
const [posts, setPosts]: [BlogPost[], (posts: BlogPost[]) => void] = useState(defaultPosts);
const [pageNo, setPageNo] = useState(defaultPageNo);
const [pageSize, setPageSize] = useState(defaultPageSize);
const [loading, setLoading] = useState(false);
const [allPostsLoaded, setAllPostsLoaded] = useState(false);
const [featuredPost, setFeaturedPost]: [BlogPost, (featuredPost: BlogPost) => void] = useState(defaultPosts[0]);
function getNewGuid() {
return Guid.create().toString();
}
async function getPosts() {
return await postService.getPosts(pageSize, pageNo);
}
async function getFeaturedPost() {
return await postService.getFeaturedPost();
}
function init() {
setLoading(true);
Promise.all([getFeaturedPost(), getPosts()])
.then(
responses => {
setLoading(false);
setFeaturedPost(responses[0].data.data);
setPosts(responses[1].data.data);
}
);
}
React.useEffect(() => {
init();
return;
}, []);
React.useEffect(() => {
if (allPostsLoaded || loading) return;
function handleScroll(event: any) {
var target = event.target.scrollingElement;
if (!loading && !allPostsLoaded && target.scrollTop + target.clientHeight === target.scrollHeight) {
setPageNo(pageNo => pageNo+1);
}
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [loading, allPostsLoaded]
);
React.useEffect(() => {
if (pageNo > 1) {
setLoading(true);
setTimeout(()=>{
getPosts()
.then(response => {
const newPosts = response.data.data;
setTimeout(()=>{
setLoading(false);
if (newPosts.length) {
const temp = [ ...posts ];
newPosts.forEach(post => !temp.map(m => m.Id).includes(post.Id) ? temp.push(post) : null);
setPosts(temp);
} else {
setAllPostsLoaded(true);
}
}, 1000);
})
}, 1000);
}
}, [pageNo]
);
return (
<PostSection className="px-3 py-5 p-md-5">
<div className="container">
<div className="item mb-5">
{posts.map(post => (
<PostCardComponent
key={post.Id}
Title={post.Title}
intro={post.Content}
Id={post.Id}
ReadTime={post.ReadTime}
CreatedDate={post.CreatedDate}
/>
))}
</div>
</div>
</PostSection>
);
};
export default Home;
I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};
I build a player using React.js and Howler.js and I use Redux/Redux-Thunk to dispatch the player state and controls to the entire app.
I want to get the seek position in "realtime" to know where the seek is in the song and maybe store the value in the future. Now I'm using setInterval to dispatch an other action that get the current position every 200ms. Because it dispatch an action it updates every components connected to the store every 200ms.
Is it a good practice to use setInterval in Action Creators ?
If I have to do in the UI, so in my Player component I have to catch events like Play Pause Prev and Next track to set or kill the interval.
How do you recommand me to do this ?
My action creator :
export const PLAYER_INITIALIZE = 'PLAYER_INITIALIZE'
export const initialize = (trackId) => {
return {
type: PLAYER_INITIALIZE,
}
}
export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
export const setTrack = (track) => {
return (dispatch, getState) => {
const { player } = getState();
let animationId = null;
if (player.audioObj && player.audioObj.state() != 'unloaded') {
dispatch({
type: PLAYER_UNLOAD,
});
}
dispatch({
type: PLAYER_SET_TRACK,
audioObj: new Howl({
src: API.API_STREAM_TRACK + track.id,
html5: true,
preload: true,
autoplay: true,
format: track.format,
volume: player.volume,
onplay: () => {
dispatch({
type: PLAYER_STARTED_PLAYBACK,
});
animationId = setInterval(() => {
dispatch({
type: PLAYER_GET_SEEK_POS,
});
}, 200);
dispatch({
type: PLAYER_STARTED_SEEK_TRACKING,
animationId: animationId,
});
},
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS,
});
},
onloaderror: (id, error) => {
dispatch({
type: PLAYER_LOAD_ERROR,
error: error,
});
},
onend: () => {
dispatch(nextTrack());
},
}),
trackMetadata: track,
duration: track.duration,
});
}
}
export const PLAYER_LOAD_SUCCESS = 'PLAYER_LOAD_SUCCESS'
export const loadSuccess = () => {
return {
type: PLAYER_LOAD_SUCCESS,
}
}
export const PLAYER_LOAD_ERROR = 'PLAYER_LOAD_ERROR'
export const loadError = () => {
return {
type: PLAYER_LOAD_ERROR,
}
}
export const PLAYER_PLAY_TRACK = 'PLAYER_PLAY_TRACK'
export const playTrack = () => {
return {
type: PLAYER_PLAY_TRACK,
}
}
export const PLAYER_STARTED_PLAYBACK = 'PLAYER_STARTED_PLAYBACK'
export const startedPlayback = () => {
return {
type: PLAYER_STARTED_PLAYBACK,
}
}
export const PLAYER_PAUSE_TRACK = 'PLAYER_PAUSE_TRACK'
export const pauseTrack = () => {
return {
type: PLAYER_PAUSE_TRACK,
}
}
export const PLAYER_NEXT_TRACK = 'PLAYER_NEXT_TRACK'
export const nextTrack = () => {
return (dispatch, getState) => {
const { playQueue, queuePos } = getState().player;
if (queuePos < playQueue.length - 1) {
dispatch (setTrack(playQueue[queuePos + 1]));
dispatch (setQueuePos(queuePos + 1));
} else {
dispatch (unload());
}
}
}
export const PLAYER_PREV_TRACK = 'PLAYER_PREV_TRACK'
export const prevTrack = () => {
return (dispatch, getState) => {
const { playQueue, queuePos } = getState().player;
if (queuePos > 0) {
dispatch (setTrack(playQueue[queuePos - 1]));
dispatch (setQueuePos(queuePos - 1));
} else {
dispatch (unload());
}
}
}
export const PLAYER_SET_SEEK_POS = 'PLAYER_SET_SEEK_POS'
export const setSeek = (pos) => {
return {
type: PLAYER_SET_SEEK_POS,
seekPos: pos
}
}
export const PLAYER_GET_SEEK_POS = 'PLAYER_GET_SEEK_POS'
export const getSeekPos = () => {
return {
type: PLAYER_GET_SEEK_POS,
}
}
export const PLAYER_STARTED_SEEK_TRACKING = 'PLAYER_STARTED_SEEK_TRACKING'
export const startedSeekTracking = (animationId) => {
return {
type: PLAYER_SET_VOLUME,
animationId: animationId,
}
}
export const PLAYER_SET_VOLUME = 'PLAYER_SET_VOLUME'
export const setVolume = (value) => {
return {
type: PLAYER_SET_VOLUME,
volume: value
}
}
export const PLAYER_SET_PLAY_QUEUE = 'PLAYER_SET_PLAY_QUEUE'
export const setPlayQueue = (queue) => {
return {
type: PLAYER_SET_PLAY_QUEUE,
playQueue: queue || {},
}
}
export const PLAYER_SET_QUEUE_POSITION = 'PLAYER_SET_QUEUE_POSITION'
export const setQueuePos = (pos) => {
return {
type: PLAYER_SET_QUEUE_POSITION,
queuePos: pos,
}
}
export const PLAYER_UNLOAD = 'PLAYER_UNLOAD'
export const unload = () => {
return {
type: PLAYER_UNLOAD,
}
}
Is it a good practice to use setInterval in Action Creators ?
No. Redux has large feedback loop and overhead. So this technique is unefficient since it triggers a lot if unnecesary operations and is not responsive enough.
In general Redux is good for state management based on "static" states. And audio playback on the other hand is a stream-like process. So my advice is to put "static info" (like song name) inside Redux. And stream-like operations should be executed directly from Howler's api.
I'm learning to React Hooks.
And I'm struggling initialize data that I fetched from a server using a custom hook.
I think I'm using hooks wrong.
My code is below.
const useFetchLocation = () => {
const [currentLocation, setCurrentLocation] = useState([]);
const getCurrentLocation = (ignore) => {
...
};
useEffect(() => {
let ignore = false;
getCurrentLocation(ignore);
return () => { ignore = true; }
}, []);
return {currentLocation};
};
const useFetch = (coords) => {
console.log(coords);
const [stores, setStores] = useState([]);
const fetchData = (coords, ignore) => {
axios.get(`${URL}`)
.then(res => {
if (!ignore) {
setStores(res.data.results);
}
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
let ignore = false;
fetchData(ignore);
return () => {
ignore = true;
};
}, [coords]);
return {stores};
}
const App = () => {
const {currentLocation} = useFetchLocation();
const {stores} = useFetch(currentLocation); // it doesn't know what currentLocation is.
...
Obviously, it doesn't work synchronously.
However, I believe there's the correct way to do so.
In this case, what should I do?
I would appreciate if you give me any ideas.
Thank you.
Not sure what all the ignore variables are about, but you can just check in your effect if coords is set. Only when coords is set you should make the axios request.
const useFetchLocation = () => {
// Start out with null instead of an empty array, this makes is easier to check later on
const [currentLocation, setCurrentLocation] = useState(null);
const getCurrentLocation = () => {
// Somehow figure out the current location and store it in the state
setTimeout(() => {
setCurrentLocation({ lat: 1, lng: 2 });
}, 500);
};
useEffect(() => {
getCurrentLocation();
}, []);
return { currentLocation };
};
const useFetch = coords => {
const [stores, setStores] = useState([]);
const fetchData = coords => {
console.log("make some HTTP request using coords:", coords);
setTimeout(() => {
console.log("pretending to receive data");
setStores([{ id: 1, name: "Store 1" }]);
}, 500);
};
useEffect(() => {
/*
* When the location is set from useFetchLocation the useFetch code is
* also triggered again. The first time coords is null so the fetchData code
* will not be executed. Then, when the coords is set to an actual object
* containing coordinates, the fetchData code will execute.
*/
if (coords) {
fetchData(coords);
}
}, [coords]);
return { stores };
};
function App() {
const { currentLocation } = useFetchLocation();
const { stores } = useFetch(currentLocation);
return (
<div className="App">
<ul>
{stores.map(store => (
<li key={store.id}>{store.name}</li>
))}
</ul>
</div>
);
}
Working sandbox (without the comments) https://codesandbox.io/embed/eager-elion-0ki0v