React component doesn't render unless I refresh & duplicate render - reactjs

I've been working on React/Redux app with firestore database
In my app I have simple POST request sent when the user send a message in the input field
and the data the user enters supposed to render in the same page without the need to refresh but I do still need to refresh even without deps in my useEffect!
Here's my code :
Post component
{posts.length > 0 &&
[...posts].map(({ id, data: { message, name, job, avatarUrl } }) => (
<Post
key={id}
name={name}
job={job}
message={message}
avatarUrl={avatarUrl}
/>
))}
However I also encounter a weird behavior after I refresh which is the components are rendered twice!Although my database be containing only one unique data for each messageThe react app renders it twice ( The querySnapshot from the database being added to the state arrayposts twice
useEffect
useEffect(() => {
querySnapshot();
});
}, []);
Database query:
const q = query(
collection(db, "posts"),
where("type", "==", "post"),
orderBy("postDate", "desc")
);
Retrieving the data :
const [input, setInput] = useState("");
const [posts, setPosts] = useState([]);
const [nextId, setNextId] = useState("0");
const addPost = (e) => {
e.preventDefault();
const docData = {
name: "mo",
job: "zoo",
message: input,
avatarUrl: "https://",
postDate: firebase.firestore.FieldValue.serverTimestamp(),
type: "post",
};
setDoc(doc(db, "posts", nextId.toString()), docData);
setNextId(parseInt(nextId) + 1);
setInput("");
};
async function querySnapshot() {
const querySnapshot = await getDocs(q);
console.log(querySnapshot.docs[0].data().message);
setNextId(querySnapshot.docs.length)
querySnapshot.docs.forEach((doc) => {
let data = {
id: doc.id,
data: doc.data(),
};
if (data && !posts.includes(data.id)) {
setPosts((current) => [...current, data]);
console.log("psts now", posts);
}
});
}
I tried to use the JavaScript Set by creating
useState(new Set()) but the same problem occurred of duplicate elements
I also tried to change deps of useEffect to render when the posts state array changes still not rendering untill I refresh

The duplication reason would be caused by setPosts(), I have updated the code as below, try to avoid setting the value inside the loop.
async function querySnapshot() {
const querySnapshot = await getDocs(q);
setNextId(querySnapshot.docs.length)
const data = querySnapshot.docs.map((doc)=>{
return {id:doc.id, data: doc.data()}
})
setPosts((current) => [...current, data])
}

Related

How to re-run useEffect after a submit function?

Hello Guys!
So in my Project, I do a fetch data function in useeffect but when I add a new element to the firestore I want that the useEffect to run again so in the list will contain the added element, somebody can give me advice on how can I do it ?
useEffect(() => {
if (session) {
fetchTodos();
}
}, [session]);
const fetchTodos = async () => {
const fetchedtodos = [];
const querySnapshot = await getDocs(collection(db, session.user.uid));
querySnapshot.forEach((doc) => {
return fetchedtodos.push({ id: doc.id, data: doc.data() });
});
setTodos(fetchedtodos);
};
const submitHandler = async (todo) => {
const data = await addDoc(collection(db, session.user.uid), {
todo,
createdAt: serverTimestamp(),
type: "active",
});
}
I want that when I run the submitHandler the useeffect run again so the list will be the newest
The only way to get a useEffect hook to run again, is to change something in the dependency array, or to not provide an array at all, and get the component to re-render (by changing props or state). See useEffect Documentation
You could just call fetchTodos directly after you call addDoc:
const submitHandler = async (todo) => {
const data = await addDoc(collection(db, session.user.uid), {
todo,
createdAt: serverTimestamp(),
type: "active",
});
return fetchTodos();
}
In my experience, the best way to do what you are trying to do is to have any requests that modify your data in the backend return the difference and then modify your state accordingly:
const submitHandler = async (todo) => {
const data = await addDoc(collection(db, session.user.uid), {
todo,
createdAt: serverTimestamp(),
type: 'active',
});
setTodos((prev) => [...prev, data]);
};
This way you don't have to do any large requests for what is mostly the same data within the same session.
Of course, this method is not ideal if multiple clients/users can modify your backend's data, or if you do not control what the endpoint responds with.
Hope this helps.

how do I post data to my nextjs api endpoint

I am making a test e-commerce sight to learn nextjs. I am trying to implement the checkout through stripe and I have it working if all of the information is static. when I make any of the information set to a variable it stops working and tells me that I am not passing any values into my variables. to test this I am making all of my data that needs to be passed, static data except for one which is when I get the error that I am not passing in information properly
obviously I am not sending the data to the api correctly. I think that my code looks just like the guides and docs so I am stuck. any help would be greatly appreciated.
here is the error message that I get:
"Missing required param: line_items[0][price_data][product_data][name]."
even if I change the state variable 'title' to a single value instead of an array, and in the updateState function settitle("title") I still get the same error
here is the front end code where I try to send the data to the api endpoint:
basket is an array of objects containing all of the products that the user has chosen.
const [id, setId] = useState([]);
const [price, setprice] = useState([]);
const [description, setdescription] = useState([]);
const [title, settitle] = useState([]);
const updateState = () => {
basket.forEach(element => {
setId([...id, element.id]);
setdescription([...description, element.description]);
setprice([...price, element.price]);
settitle([...title, basket.title]);
});
console.log(id);
console.log(description);
console.log(price);
console.log(title);
}
//send data to the api
const postData = async () => {
const response = await fetch("/api/checkout_sessions", {
method: "POST",
body: JSON.stringify(
id,
price,
description,
title,
),
});
return response.json();
};
return (
<form action="/api/checkout_sessions" method="POST">
<button
type="submit"
role="link"
className="button"
onClick={() => {
updateState;
postData;
}}
>
proceed to checkout
</button>
</form>
)}
here is the api code where I try to get that data and use it which is not working how I expect:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
export default async function handler(req, res) {
// var priceVariable ='price_1MB8P4FqoomU2P4qrVmtxCvp';
if (req.method === 'POST') {
const items = req.body.id
const amount = req.body.price
const description = req.body.description
const title = req.body.title
try {
// Create Checkout Sessions from body params.
const session = await stripe.checkout.sessions.create({
// shipping_options: ["shr_1MBn0HFqoomU2P4qZk4vqOQ3"],
shipping_address_collection: {
allowed_countries: ["US", "CA", "GB"],
},
line_items:[{
price_data: {
unit_amount: 1000,
currency: 'usd',
product_data: {
name: title,
description: "description",
},
},
quantity: 1,
}],
mode: 'payment',
success_url: `${req.headers.origin}/?success=true`,
cancel_url: `${req.headers.origin}/?canceled=true`,
});
res.redirect(303, session.url);
} catch (err) {
res.status(err.statusCode || 500).json(err.message);
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
}
you can see in the line_items that everything is static except for the one variable that I am testing.
JSON.stringify expects an object (https://www.w3schools.com/js/js_json_stringify.asp)
const postData = async () => {
const response = await fetch("/api/checkout_sessions", {
method: "POST",
body: JSON.stringify({
id,
price,
description,
title,
}),
});
return response.json();
};
And on the api side you may have to parse the body before accessing any properties like so
const body = JSON.parse(req.body)
const title = body.title
(https://www.w3schools.com/Js/js_json_parse.asp)
It's unclear if the array/string mismatch is due to your testing changes, but you'll need to ensure a single string is supplied for name.
Your actual issue is likely here:
onClick={() => {
updateState;
postData;
}}
I'm surprised this is invoking the functions without (), but even if it were your postData() would start before the react state change happened.
I suspect if you initialized title with a value your endpoint would receive that.
const [title, setTitle] = useState("some default title");
You'll need to review how your application is tracking state here, and perhaps calculate that title and pass it through to the request alongside updating the state.

Batch hook requests and wait for them all to finish in React

I am having troubles figuring out how to wait for a series of requests to finish sent through an API hook in React.
My fetch hook:
export default function useFetch<T>(endpoint: string, config: RequestInit) {
const [data, setData] = useState<T | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
// Set headers, body, auth etc.
const fetchData = async ({ body, onSuccess }: RequestConfig = {}) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}${endpoint}`, config);
if (response.ok) {
const responseData = await response.json().catch((_) => _);
setError("");
setData(responseData);
// Edit: added count of requests
if (onSuccess) {
onSuccess(responseData);
}
setIsLoading(false);
} else {
setIsLoading(false);
// Handle errors
}
} catch (_) {
setError("Could not contact the server. Please try again later.");
setIsLoading(false);
}
};
const state: ResponseState<T> = { data, isLoading, error };
return [state, fetchData] as const;
}
In the app I have a list of items and the backend only takes single requests at a time, so if I update, say five, items I will send five requests. I want to wait for all the requests to finish, so I can tell the user that the updates are successful (e.g. in the form of a toast).
Before I made my fetch hook, I used promises and then it worked great with Promise.all().
Edit:
Now I have the project container that sends the API requests:
function ProjectContainer() {
const { projectId } = useParams();
const [ state, getProject] = useFetch<IProject>();
const [{ data: hasUpdated, isLoading, error }, attachMaterial] =
useFetch<IProject>();
const [{ data: hasUpdated }, detachMaterial] = useFetch<IProject>();
const [totalNumberOfRequests, setTotalNumberOfRequests] = useState(0); // Edit: added count of requests
const [openToast, setOpenToast] = useState(false);
useEffect(() => {
getProject(`projects/${projectId}`);
}, []);
// Edit: added count of requests:
useEffect(() => {
if (hasUpdated && totalNumberOfRequests === 0) {
setOpenToast(true);
}
}, [totalNumberOfRequests]);
// The backend only has attach and detach, so if I want to make an update to a material for a project, I have to delete (detach) the material and attach a new one
const attachNewMaterialToProject = (materialToAttach: IProjectMaterial) => {
setTotalNumberOfRequests(totalNumberOfRequests + 1); // Edit: added count of requests
attachMaterial(`projects/${projectId}/${materialToAttach.materialId!}`, {
body: materialToAttach,
onSuccess: () => setTotalNumberOfRequests(totalNumberOfRequests - 1) // Edit: added count of requests
});
};
const detachMaterialFromProject = (projectMaterialId: number) =>
detachMaterial(`projects/${projectId}/${projectMaterialId}`);
return (
<>
<ProjectMaterials
attachMaterial={attachNewMaterialToProject}
detachMaterial={detachMaterialFromProject}
/>
<Toast
open={(hasUpdated && !isLoading) || openToast}
onClose={() => setOpenToast(false)}
type={error ? "error" : "success"}
text={error || "Save successful"}
/>
</>
);
}
export default ProjectContainer;
The ProjectMaterials component finds the updated fields and calls attach/detach in the parent:
function ProjectMaterials({
// Other props
attachMaterial,
detachMaterial,
}: ProjectMaterialsProps) {
const { control, handleSubmit, formState } =
useForm<ProjectMaterialFormValues>({
defaultValues: { materialBars: initialMaterialBars },
});
const { fields, append } = useFieldArray({
control,
name: "materialBars",
});
const dirtyFields = formState.dirtyFields.materialBars;
const saveAllMaterials = async (dirtyFields) => {
dirtyFields?.forEach((dirtyProjectMaterial) => {
// Check for dirty fields and then call attachMaterial in the parent for each dirty item in the list
if (dirtyProjectMaterial.id) {
detachMaterial(dirtyProjectMaterial.id);
}
attachMaterial(dirtyProjectMaterial);
});
};
const addMaterial = (): void => {
append(defaultMaterialBar);
};
return (
<>
{fields.map(({ id }, index) => (
<MaterialBar key={id} index={index} control={control} />
))}
<Button onClick={addMaterial}>ADD MATERIAL</Button>
<Button onClick={handleSubmit(saveAllMaterials)}>SAVE ALL</Button>
</>
);
}
export default ProjectMaterials;
In the parent I have tried hasUpdated && !isLoading. This is always true after all the requests have finished and thus the toast will never disappear.
So, using a hook API service, is there a way to wait for several requests of the same type to finish, so I can run some code (here show a toast) when I am sure all requests finished successfully (or show an error if not)?
You could add some state to track the number of active requests.
const [totalNumberOfRequests, setTotalNumberOfRequests] = useState(0);
Use functional state updates to correctly update from the previous state's value. This avoids stale closures over the totalNumberOfRequests state in the attachNewMaterialToProject callback scope.
const attachNewMaterialToProject = (materialToAttach: IProjectMaterial) => {
setTotalNumberOfRequests(count => count + 1);
attachMaterial(`projects/${projectId}/${materialToAttach.materialId!}`, {
body: materialToAttach,
onSuccess: () => setTotalNumberOfRequests(count => count - 1);
});
};

React app backend firestore useEffect not working correctly

I am trying to keep track of the number of visitors my website gets using firestore as a backend.
I have two functions in my useEffect hook getVistorCount, and updateVistorCount. getVistorCount gets the doc containing the vistor_count variable from firestore and saves that array of data to state.
My updateVistorCount function gets the id and data of the document I am trying to update from state. The problem is that my state is initialized to an empty array so when I try to get the id and vistor_count from state I get the error "Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'id')" because the array from state is empty.
I am also getting the following warning in VSCode: "React Hook useEffect has missing dependencies: 'numberOfVistors' and 'portfolioStatesRef'. Either include them or remove the dependency array"
Here is the current code:
const portfolioStatesRef = collection(db, "portfolio-stats")
useEffect (() => {
let sessionKey = sessionStorage.getItem("sessionKey")
const getVistorCount = async () => {
const dataFromPortfolioStatsCollection = await getDocs(portfolioStatesRef)
setnumberOfVistors(dataFromPortfolioStatsCollection.docs.map((doc) => {
return ({ ...doc.data(), id: doc.id })
}))
}
const updateVistorCount = async () => {
const portfolioStatsDoc = doc(db, "portfolio-stats", numberOfVistors[0].id)
const updatedFields = {vistor_count: numberOfVistors[0].vistor_count + 1}
await updateDoc(portfolioStatsDoc, updatedFields)
}
getVistorCount()
if (sessionKey === null)
{
sessionStorage.setItem("sessionKey", "randomString")
updateVistorCount()
}
}, [])
Maybe using two effects can achieve what you want. First to get visitors:
useEffect (() => {
let sessionKey = sessionStorage.getItem("sessionKey")
const getVistorCount = async () => {
const dataFromPortfolioStatsCollection = await getDocs(portfolioStatesRef)
setnumberOfVistors(dataFromPortfolioStatsCollection.docs.map((doc) => {
return ({ ...doc.data(), id: doc.id })
}))
}
getVistorCount()
}, [])
Second to update count
useEffect (() => {
if(!numberOfVistors.length) return;
const updateVistorCount = async () => {
const portfolioStatsDoc = doc(db, "portfolio-stats", numberOfVistors[0].id)
const updatedFields = {vistor_count: numberOfVistors[0].vistor_count + 1}
await updateDoc(portfolioStatsDoc, updatedFields)
}
updateVistorCount()
}, [numberOfVistors])

React hooks dependencies, including it creates an infinite loop, not including it doesn't give me the latest value

Using React hooks.
I'm trying to do a simple API fetch call with some data, but I can't seem to make this work.
Here is the sandbox link
In this example, the objective is that every 5secs, it fetches to the server to get any updates to the username since the latest latestUpdate.
But for convenience, I will include the code here as well:
const SmallComponent = () => {
const { id, username, latestUpdate } = useItemState();
const dispatch = useItemDispatch();
console.log("Render id", id, "Latest", latestUpdate);
const fetchUsername = useCallback(async () => {
console.log("Getting Id", id, "Latest", latestUpdate);
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/" + id
);
const user = await response.json();
dispatch({ type: "setUsername", usernameUpdated: user.name });
}, [dispatch, id]);
const updateId = useCallback(() => {
dispatch({ type: "setId", id: id + 1 });
}, [dispatch, id]);
useEffect(() => {
fetchUsername();
const refresh = setInterval(() => {
updateId();
}, 5000);
return () => clearInterval(refresh);
}, [fetchUsername, updateId]);
return (
<div>
<h4>Username from fetch:</h4>
<p>{username || "not set"}</p>
</div>
);
};
As you'll notice, my fetchUsername is missing a dependency for latestUpdate (which is used on my server to only send udpates since that date). I update latestUpdate when the fetchUsername is finished in my reducer.
What I need:
on mount: fetch username => updates state for username and latestUpdate
interval: every 5secs => fetch updates to username and update latestUpdate to new Date()
The problem is:
If I add the dependency to the useCallback for fetchUsername, I get an infinite refresh loop.
If I don't add it, my latestUpdate value is wrong (ie initial value)
What am I doing wrong?
As you're not using the fetch method anywhere else, it makes sense to put it inside the useEffect directly. No need for useCallback:
useEffect(() => {
const fetchUsername = async () => {
console.log("FETCH", latestUpdate);
const url =
"https://jsonplaceholder.typicode.com/users/" + id + "#" + latestUpdate;
const response = await fetch(url);
const user = await response.json();
dispatch({ type: "setUsername", usernameUpdated: user.name });
};
const refresh = setInterval(() => {
fetchUsername();
}, 5000);
return () => clearInterval(refresh);
}, [dispatch, id, latestUpdate]);
Here is the full CodeSandBox:
https://codesandbox.io/s/trusting-framework-hvw06?file=/src/App.js
You can find more in the official docs (look for "...to move that function inside of your effect"):
https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies
And I also recommend Robin Wieruch's hook-fetching tutorial: https://www.robinwieruch.de/react-hooks-fetch-data
In general, I would highly recommend using something like react-query, as it will also take care of caching. It is a better way to consume your server data (instead of fetching and putting the response in your context): https://github.com/tannerlinsley/react-query

Resources