I'm new to React. I'm trying to make my socket io listener work. When I it out of useEffect it works but it is called several times. In useEffect it is called only once (which is good obviously) but this time users are not updated - initial value.
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
getUsers();
if(users.length > 0) {
socket.on("statusChange", (data) => {
console.log(users); // this returns initial state of users
let tempUsers = [...users];
let ndx = tempUsers.findIndex(obj => obj.id === data.id);
if(ndx === -1)
return;
tempUsers[ndx].status = data.status;
setUsers(tempUsers);
});
}
}, []);
function getUsers() {
fetch(/* stuff */)
.then(res => res.json())
.then(res => setUsers(res.data));
}
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
Some parts where I'm making assumptions:
getUsers() should be called only once: when the component mounts
We want to listen to socket.on("statusChange") and get updates about users we got from getUsers().
// This is pulled outside the component.
function getUsers() {
fetch(/* stuff */)
.then(res => res.json());
}
function Users() {
const [users, setUsers] = useState([]);
// Fetch users only once, when the component mounts.
useEffect(() => {
getUsers().then(res => {
setUsers(res.data);
});
}, []);
// Listen to changes
useEffect(() => {
if (!users.length) return;
const listener = (data) => {
// Functional update
// See: https://reactjs.org/docs/hooks-reference.html#functional-updates
setUsers(prevUsers => {
let nextUsers = [...prevUsers];
let ndx = nextUsers.findIndex(obj => obj.id === data.id);
// What's up here? See below.
if(ndx === -1) return nextUsers;
nextUsers[ndx].status = data.status;
return nextUsers;
});
};
socket.on("statusChange", listener);
// Unsubscribe when this component unmounts
// See: https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
return () => socket.off("details", listener);
}, [users]);
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
About if(ndx === -1) return nextUsers;
This means that users will never change in size, i.e. you won't handle data about a new user.
Alternatively, you could do if(ndx === -1) return [ ...nextUsers, data ];
Instead of simply executing getUsers, use it with a callback. And then in the callBack execute setUsers:
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(/* stuff */)
.then(res => res.json())
.then(res => {
if(res.data.length > 0) {
socket.on("statusChange", (data) => {
console.log(res.data); // this returns initial state of users
let tempUsers = [...res.data];
let ndx = tempUsers.findIndex(obj => obj.id === data.id);
if(ndx === -1)
return;
tempUsers[ndx].status = data.status;
setUsers(tempUsers);
});
}
});
}, []);
return (
<div className={styles.container}>
<div className={styles.usersContainer}>
{ users.map((user, i) => <UserCard key={i} user={user} />) }
</div>
</div>
);
}
export default Users;
Related
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 am trying to query data using useEffect add those data to state and render them but nothing comes up unless a state in the app changes. this is what i have done so far Please help, Thanks in Advance.
useEffect
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
whole component
import { useDispatch, useSelector } from "react-redux";
import _ from "lodash";
import styles from "../../../styles/CreateLeads.module.css";
import {
getAgentCampaigns,
getCampaignLeads,
} from "../../../utils/requests/campaign";
import {
hideTopLoader,
showTopLoader,
} from "../../../store/actions/TopLoader/topLoaderActions";
import CampaignSection from "./CampaignSection";
import Empty from "../Empty/Empty";
import SectionHeader from "../SectionHeader/SectionHeader";
import SearchBar from "../SearchBar/SearchBar";
import { RootState } from "../../../store/store";
const CreateLeadsCardsWrapper: React.FC = () => {
const authToken = useSelector(
(store: any) => store.authenticationReducer.authToken
);
const [stateCampaigns, setStateCampaigns] = React.useState<any[]>([]);
const [showCampaigns, setShowCampaigns] = React.useState<boolean>(false);
const [campaigns, setCampaigns] = React.useState<any[]>(stateCampaigns);
const [filter, setFilter] = React.useState<string>("");
const dispatch = useDispatch();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Reset filter
setFilter("");
let campaignSearch = e.target.value.trim();
if (campaignSearch.length === 0) {
return;
}
let campaignSearchLower = campaignSearch.toLowerCase();
let campaignSearchUpper = campaignSearch.toUpperCase();
let campaignSearchSentence =
campaignSearch.charAt(0).toUpperCase() + campaignSearch.slice(1);
let results = stateCampaigns.filter(
({ leads }: { leads: any[] }, i) =>
leads &&
leads?.some(
(lead: any) =>
lead.firstName.includes(campaignSearch) ||
lead.firstName.includes(campaignSearchLower) ||
lead.firstName.includes(campaignSearchUpper) ||
lead.firstName.includes(campaignSearchSentence) ||
lead.lastName.includes(campaignSearch) ||
lead.lastName.includes(campaignSearchLower) ||
lead.lastName.includes(campaignSearchUpper) ||
lead.lastName.includes(campaignSearchSentence) ||
lead.email.includes(campaignSearch) ||
lead.email.includes(campaignSearchLower) ||
lead.email.includes(campaignSearchUpper) ||
lead.email.includes(campaignSearchSentence) ||
lead.phoneNo.includes(campaignSearch) ||
lead.phoneNo.includes(campaignSearchLower) ||
lead.phoneNo.includes(campaignSearchUpper) ||
lead.phoneNo.includes(campaignSearchSentence)
)
);
setCampaigns(results);
};
React.useEffect(() => {
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
React.useEffect(() => {
setCampaigns(stateCampaigns);
campaigns.length > 0 && setShowCampaigns(true);
console.log(showCampaigns);
dispatch(hideTopLoader());
}, []);
return (
<div className={styles.wrappers}>
{/* Multi step form select a campaign first then fill info on the next step */}
<SectionHeader text="Campaign Leads" />
{showCampaigns && stateCampaigns.length === 0 && (
<>
<Empty description="No agents yet" />
<p>Join a campaign.</p>
</>
)}
{showCampaigns && stateCampaigns.length > 0 && (
<>
<p className="text-grey-500">Create Leads for Campaigns.</p>
<section className={styles.container}>
<SearchBar
placeholder="Find Campaign Leads"
onChange={handleChange}
/>
{campaigns.map((item: any) => (
<CampaignSection
key={item?.Campaign?.id}
id={item?.Campaign?.id}
name={item?.Campaign?.name}
imageUrl={item?.Campaign?.iconUrl}
campaignType={item?.Campaign?.type}
productType={item?.Campaign?.Products[0]?.type}
/>
))}
</section>
</>
)}
</div>
);
};
export default CreateLeadsCardsWrapper;
there are two things wrong in yoour code :
1- you should not have two useeffects with the same dependencies in your case: [] you have to merge those useeffects into one or change the second one's dependencies
2- doing async code in useeffect can be problematic sometimes. it is better to create an async function which does the query to the backend and sets the state and the call the function in your useeffect like below :
const getData = async()=>{
// do some queries and set the state
}
React.useeffect(()=>{
getData()
},[])
Get All values of checkboxes from dynamic values
import * as React from "react";
import Checkbox from "#mui/material/Checkbox";
import FormControlLabel from "#mui/material/FormControlLabel";
import axios from "axios";
export default function Checkboxes() {
const [users, setUsers] = React.useState([]);
const [isChecked, setIsChecked] = React.useState(() =>
users.map((i) => false)
);
React.useEffect(() => {
getUsers();
}, []);
const getUsers = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
setUsers(response.data);
} catch (error) {
console.error(error);
}
};
const isCheckboxChecked = (index, checked) => {
setIsChecked((isChecked) => {
return isChecked.map((c, i) => {
if (i === index) return checked;
return c;
});
});
};
console.log(isChecked);
return (
<div>
{users.map((checkbox, index) => {
return (
<FormControlLabel
key={index}
control={
<Checkbox
checked={isChecked[index]}
onChange={(e) => isCheckboxChecked(index, e.target.checked)}
/>
}
label={checkbox.name}
/>
);
})}
<pre>{JSON.stringify(isChecked, null, 4)}</pre>
</div>
);
}
i'm trying to do like this.
https://codesandbox.io/s/69640376-material-ui-react-multiple-checkbox-using-tabs-8jogw?file=/demo.js
but this has static data.
this is what i have done so far
https://codesandbox.io/s/festive-euclid-w3dzpq?file=/src/App.js
I've checked your code and found that you should issue in your "isCheckboxChecked" method.
const isCheckboxChecked = (index, checked) => {
isChecked[index] = checked;
setIsChecked([...isChecked]);
};
Another issue was you should wait till you get your response from API, so we are only setting checkboxes when we have users.
React.useEffect(() => {
if (users.length) setIsChecked(users.map((i) => false));
}, [users]);
Hope this helps😁
I've already updated your sandbox.
https://codesandbox.io/s/festive-euclid-w3dzpq?file=/src/App.js
If you need the same functionality as the first sandbox, then you need to update somehow your "checked" array after you fetch the users.
Currently this array is not updated, so you never see true/false values
Try this
const getUsers = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
await setUsers(response.data);
setIsChecked(() => response.data.map((i) => false));
} catch (error) {
console.error(error);
}
};
sandbox
If you don't want to use a function, you can instead use an array
const [isChecked, setIsChecked] = React.useState([]);
React.useEffect(() => {
getUsers();
}, []);
const getUsers = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
await setUsers(response.data);
setIsChecked(Array.from({length: response.data.length}, i => i = false))
} catch (error) {
console.error(error);
}
};
In order to clear the "uncontrolled state" warnings/errors, change this line
<Checkbox
checked={isChecked[index] == null ? false : isChecked[index]}
onChange={(e) => isCheckboxChecked(index, e.target.checked)}
/>
Checkbox must have an initial controlled state of true or false and not null/undefined.
I am trying to add pagination in React and am sending a query for it, based on the page number of the URL. The data should be fetched and update the same component.
But the problem occurs when I go from page 2 to page 3. The data from page 2 data and page 3 data are shown alternately for a while.
After some time, the newly fetched data is updated.
I have setPost([]) after page 3 is clicked, but I don't know from where page 2 is coming.
I want to setLoading to true till the newly fetched data is completely fetched.
const Daily = ({match}) => {
const [posts, setPosts] = useState([]);
const [numberOfPages, setNumberOfPages] = useState([]);
const [loading, setLoading] = useState(true);
const perPageItems = 8;
useEffect(() => {
const totalPost = () => {
getPostsCount()
.then(data => {
setNumberOfPages(Math.ceil(data/perPageItems))
})
}
const loadIndexPosts = (a, b) => {
getPostsByIndex(a, b)
.then(data => {
if (data.error) {
console.log(data.error);
} else {
setPosts(data);
}
setLoading(false);
})
};
totalPost()
if(match.params.page==undefined)
match.params.page=1;
var n = parseInt(match.params.page)-1;
var startIdx = n*perPageItems;
loadIndexPosts(startIdx, perPageItems)
},)
console.log(posts);
return (
<div>
<div className="mt-5 daily-card-feed">
{loading && <ThreeDotsWave/>}
{posts.map((post, index) => {
return (
<div key={index}>
<DailyCard post={post}/>
</div>
)
})}
</div>
<div className="h4 pb-5 pull-right mr-5">
{match.params.page==1 &&
<Link to={`/daily/page/2`} onClick={() => {setLoading(true); setPosts([])}}>Older Post -></Link>}
{match.params.page!=1 &&
<Link to={`${parseInt(match.params.page)+1}`} onClick={() => {setLoading(true); setPosts([])}}>Older Post -></Link>}
</div>
</div>
)
}
First of all, let's figure out the logic of loading new data. When a new page arrives at path, you want to call the api. I think the problem is here:
useEffect(() => {
const totalPost = () => {
getPostsCount()
.then(data => {
setNumberOfPages(Math.ceil(data/perPageItems))
})
}
const loadIndexPosts = (a, b) => {
getPostsByIndex(a, b)
.then(data => {
if (data.error) {
console.log(data.error);
} else {
setPosts(data);
}
setLoading(false);
})
};
totalPost()
if(match.params.page==undefined)
match.params.page=1;
var n = parseInt(match.params.page)-1;
var startIdx = n*perPageItems;
loadIndexPosts(startIdx, perPageItems)
},)
We can break this like:
const totalPost = () => {
getPostsCount()
.then(data => {
setNumberOfPages(Math.ceil(data/perPageItems))
})
}
const loadIndexPosts = (a, b) => {
getPostsByIndex(a, b)
.then(data => {
if (data.error) {
console.log(data.error);
} else {
setPosts(data);
}
setLoading(false);
})
};
useEffect(() => {
if(match.params.page==undefined)
match.params.page=1;
var n = parseInt(match.params.page)-1;
var startIdx = n*perPageItems;
loadIndexPosts(startIdx, perPageItems)
},[numberOfPages])
This will add the dependency of the call when page number changes. And call totalPost() only from click event.
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 1 year ago.
I fetched an array of products from firebase with the normal way :
export const getUsersProducts = async uid => {
const UsersProducts = []
await db.collection('products').where("userID", "==", uid).get().then(snapshot => {
snapshot.forEach(doc => UsersProducts.push(doc.data()))
})
return UsersProducts
}
and the fetched array shows up in the dom normally, but when I tried to fetch it with onSnapshot method it didnt show up on the DOM even though in appeared in my redux store and when I console log it, it shows up normally.
export const getUsersProducts = uid => {
let UsersProducts = []
db.collection('products').where("userID", "==", uid).onSnapshot(querySnapshot => {
querySnapshot.docChanges().forEach(change => {
if (change.type === "added") {
UsersProducts.push(change.doc.data())
}
})
})
return UsersProducts
}
here is the code used to show it in the DOM
const MyProducts = () => {
const CurrentUserInfos = useSelector(state => state.userReducer.currentUserInfos)
const searchQuery = useSelector(state => state.productsReducer.searchQuery)
const myProducts = useSelector(state => state.productsReducer.usersProducts)
const dispatch = useDispatch()
const settingUsersProductList = async () => {
try {
const usersProducts = getUsersProducts(CurrentUserInfos.userID)
dispatch(setUsersProducts(usersProducts))
console.log(myProducts)
} catch (err) {
console.log(err)
}
}
useEffect(() => {
settingUsersProductList()
}, [CurrentUserInfos])
return (
<div className="my-products">
<div className="my-products__search-bar">
<SearchBar />
</div>
<div className="my-products__list">
{
Object.keys(myProducts).length===0 ? (<Loading />) : (myProducts.filter(product => {
if(searchQuery==="")
return product
else if(product.title && product.title.toLowerCase().includes(searchQuery.toLowerCase()))
return product
}).map(product => {
return(
<ProductItem
key={product.id}
product={product}
/>
)
}))
}
</div>
</div>
)
}
export default MyProducts
You are returning the array before promise is resolved hence its empty. Try this:
export const getUsersProducts = async uid => {
const snapshot = await db.collection('products').where("userID", "==", uid).get()
const UsersProducts = snapshot.docs.map(doc => doc.data())
return UsersProducts
}
For onSnapshot, add the return statement inside of onSnapshot,
export const getUsersProducts = uid => {
let UsersProducts = []
return db.collection('products').where("userID", "==", uid).onSnapshot(querySnapshot => {
querySnapshot.docChanges().forEach(change => {
if (change.type === "added") {
UsersProducts.push(change.doc.data())
}
})
return UsersProducts
})
}