Custom hook in React JS : arguments not updating after rerender - reactjs

I have a date range filter in which when submitted, it will update new values of dateStart and dateEnd using setDateStart and setDateEnd then it will pass down the new values to my useCollection custom hook. Why is it that when it rerenders, the useCollection custom hook arguments doesn't get the updated values?
*the custom useCollection hook is used for data Fetching on Firebase
*the dateStart and dateEnd is used for filtering the data displayed on the screen
useCollection custom hook:
import { useEffect, useState, useRef } from "react";
import { projectFirestore } from "../firebase/config";
export const useCollection = (
collection,
_query,
_query2,
_query3,
_orderBy
) => {
const [documents, setDocuments] = useState(null);
const [error, setError] = useState(null);
// if we don't use a ref --> infinite loop in useEffect
// _query is an array and is "different" on every function call
const query = useRef(_query).current;
const query2 = useRef(_query2).current;
const query3 = useRef(_query3).current;
const orderBy = useRef(_orderBy).current;
console.log("from query: " + query);
console.log("from query2: " + query2);
console.log("from query2: " + query3);
useEffect(() => {
let ref = projectFirestore.collection(collection);
if (query) {
ref = ref.where(...query);
}
if (query2) {
ref = ref.where(...query2);
}
if (query3) {
ref = ref.where(...query3);
}
if (orderBy) {
ref = ref.orderBy(...orderBy);
}
const unsubscribe = ref.onSnapshot(
(snapshot) => {
let results = [];
snapshot.docs.forEach((doc) => {
results.push({ ...doc.data(), id: doc.id });
});
// update state
setDocuments(results);
setError(null);
},
(error) => {
console.log(error);
setError("could not fetch the data");
}
);
// unsubscribe on unmount
return () => unsubscribe();
}, [collection, query, query2, query3, orderBy]);
return { documents, error };
};
TimeCard.js
import React, { useState, useEffect } from "react";
import moment from "moment";
import { useAuthContext } from "../../hooks/useAuthContext";
import { useCollection } from "../../hooks/useCollection";
import Table from "../../components/Table";
import DateRange from "../../components/DateRange";
import ErrorMessage from "../../components/ErrorMessage";
const TimeCard = () => {
const { uid } = useAuthContext().user;
let m1 = moment(new Date());
let m2 = moment();
m1.startOf("month").startOf("day");
m2.endOf("day");
const [dateStart, setDateStart] = useState(m1.toDate());
const [dateEnd, setDateEnd] = useState(m2.toDate());
console.log("Moment1 from State: ", dateStart);
console.log("Moment2: from State", dateEnd);
const [time1, setTime1] = useState("");
const [time2, setTime2] = useState("");
const [error, setError] = useState("");
console.log("RENDER");
const { documents } = useCollection(
"timeinout",
["uid", "==", uid],
["createdAt", ">=", dateStart],
["createdAt", "<=", dateEnd],
["createdAt", "asc"]
);
const handleSubmit = (e) => {
e.preventDefault();
var d1 = new Date(time1);
var d2 = new Date(time2);
if (!time1 || !time2) {
setError(
"Either ONE or BOTH of the date inputs below are empty, please select a date!"
);
return null;
}
if (d1.getTime() > d2.getTime()) {
setError(
"Invalid Date: Date 2 (To:) Input is greater than Date 1 (From:) Input"
);
return null;
}
const s1 = moment(time1).startOf("day").toDate();
const s2 = moment(time2).endOf("day").toDate();
setDateStart(s1);
setDateEnd(s2);
};
useEffect(() => {
const time = setTimeout(() => {
setError("");
}, [5000]);
return () => {
clearTimeout(time);
};
}, [error]);
return (
<>
<div className="flex flex-col">
{error && <ErrorMessage msg={error} />}
<DateRange
time1={time1}
time2={time2}
setTime1={setTime1}
setTime2={setTime2}
handleSubmit={handleSubmit}
/>
</div>
{documents && <Table dataFromDatabase={documents} />}
</>
);
};
export default TimeCard;

Implemented it without using a custom hook instead, basically all the codes from the useCollection custom hook were transferred to TimeCard.js and created a normal useEffect hook that process all the changes in the filter for the dates.
See code below:
import React, { useState, useEffect } from "react";
import { projectFirestore } from "../../firebase/config";
import moment from "moment";
import { useAuthContext } from "../../hooks/useAuthContext";
import Table from "../../components/Table";
import DateRange from "../../components/DateRange";
import ErrorMessage from "../../components/ErrorMessage";
const TimeCard = () => {
const { uid } = useAuthContext().user;
const [error, setError] = useState("");
const [documents, setDocuments] = useState(null);
const startOfMonth = moment().startOf("month").format("YYYY-MM-DD");
const endOfMonth = moment().endOf("month").format("YYYY-MM-DD");
console.log("startOfMonth: ", startOfMonth);
console.log("endOfMonth: ", endOfMonth);
const [time1, setTime1] = useState(startOfMonth);
const [time2, setTime2] = useState(endOfMonth);
const [query] = useState(["uid", "==", uid]);
const [query2, setQuery2] = useState([
"createdAt",
">=",
moment(time1).startOf("day").toDate(),
]);
const [query3, setQuery3] = useState([
"createdAt",
"<=",
moment(time2).endOf("day").toDate(),
]);
const [orderBy] = useState(["createdAt", "asc"]);
const handleSubmit = (e) => {
e.preventDefault();
var d1 = new Date(time1);
var d2 = new Date(time2);
if (!time1 || !time2) {
setError(
"Either ONE or BOTH of the date inputs below are empty, please select a date!"
);
return null;
}
// Loseless code below because of > comparison operator
if (d1.getTime() > d2.getTime()) {
setError(
"Invalid Date: Date 2 (To:) Input is greater than Date 1 (From:) Input"
);
return null;
}
setQuery2(["createdAt", ">=", moment(time1).startOf("day").toDate()]);
setQuery3(["createdAt", "<=", moment(time2).endOf("day").toDate()]);
};
useEffect(() => {
const time = setTimeout(() => {
setError("");
}, [5000]);
return () => {
clearTimeout(time);
};
}, [error]);
useEffect(() => {
let ref = projectFirestore.collection("timeinout");
if (query) {
ref = ref.where(...query);
}
if (query2) {
ref = ref.where(...query2);
}
if (query3) {
ref = ref.where(...query3);
}
if (orderBy) {
ref = ref.orderBy(...orderBy);
}
const unsubscribe = ref.onSnapshot(
(snapshot) => {
let results = [];
snapshot.docs.forEach((doc) => {
results.push({ ...doc.data(), id: doc.id });
});
// update state
setDocuments(results);
setError(null);
},
(error) => {
console.log(error);
setError("could not fetch the data");
}
);
// unsubscribe on unmount
return () => unsubscribe();
}, [query, query2, query3, orderBy]);
console.log("Updated documents OUTSIDE: ", documents);
return (
<>
<div className="flex flex-col">
{error && <ErrorMessage msg={error} />}
<DateRange
time1={time1}
time2={time2}
setTime1={setTime1}
setTime2={setTime2}
handleSubmit={handleSubmit}
/>
</div>
{documents && <Table documents={documents} />}
</>
);
};
export default TimeCard;

Related

React Hook error in array handing in useEffect() and setState does not set the state

I'm having two problems in this code. The first is in the second useEffect. For the reason that I don't understand the useEffect stops working every now and then and causes an error "Cannot read property 'toLowerCase'". Removing the toLowerCase does not solve the problem, but the whole array handling seems to be impossible at that time.
The other problem is in the function addName. setNewName does not set newName. That one I've tried in various kinds of forms, such as setNewName(...newName, {name: '', number: ''}), setNewName('') inside .then and else as well as outside else.
...
import React, {useState, useEffect} from 'react'
import Filter from './components/Filter'
import PersonForm from './components/PersonForm'
import Persons from './components/Persons'
import personService from './services/person'
const App = () => {
const [person, setPerson] = useState([])
const [newName, setNewName] = useState({name: '', number: ''})
const [filteredPerson, setFilteredPerson] = useState([''])
const [searchTerm, setSearchTerm] = useState('')
useEffect(() => {
personService
.getAll()
.then(initialPersons => {
setPerson(initialPersons)
})
}, [])
useEffect( () => {
const results = person.filter( p =>
p.name.toLowerCase().includes(searchTerm) )
setFilteredPerson(results)
},[person,filteredPerson] )
const addName = (event) => {
event.preventDefault()
const nameObject = {
name: newName.name,
number: newName.number
}
if (person.some(p => p.name === newName.name)
) {
window.alert(`${newName.name} is already added to phonebook`)
}
else {
personService
.create(nameObject)
.then(returnedPerson => {
setPerson(person.concat(returnedPerson))
setNewName({name: '', number: ''})
})
console.log('newName', newName.name )
}
}
const handleAddPerson = (event) => {
console.log('event.target.name ', event.target.name)
console.log('event.target.value ', event.target.value)
setNewName({...newName,
[event.target.name]: event.target.value
})
}
const handleSearchTerm = (event) => {
setSearchTerm(event.target.value)
}
return (
<div >
<h2>Phonebook</h2>
<Filter searchTerm={searchTerm} onChange={handleSearchTerm} />
<h3>Add a new</h3>
<PersonForm onSubmit={addName} onChange={handleAddPerson} />
<h2>Numbers</h2>
<Persons list={filteredPerson} />
</div>
);
}
export default App;
...
import axios from 'axios'
const baseUrl = 'http://localhost:3001/persons'
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
const create = newObject => {
const request = axios.post(baseUrl, newObject)
return request.then(response => response.data)
}
const update = (id, newObject) => {
const request = axios.put(`${baseUrl}/${id}`, newObject)
return request.then(response => response.data)
}
/*const updater = {
getAll,
create,
update
}*/
export default {
getAll,
create,
update
}
EDIT
Use async await in your personService so you can return response instead of return request.then(...) something like:
const getAll = async () => {
const response = await axios.get(baseUrl);
return response;
}
After that you can do as follows in your useEffect
useEffect(() => {
(async () => {
const response = await personService.getAll();
if (response.status === 200) {
setPerson(response.data);
const filtered = response.data.filter(item =>
item.name.toLowerCase().includes(searchTerm)
);
setFilteredPerson([...filtered]);
}
})();
}, []);

React useState hook is not working as expected inside useEffect hook

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;

TypeError: Cannot read property 'map' of undefined React Hooks

I need some help understanding why I'm getting the error from the title: 'TypeError: Cannot read property 'map' of undefined'. I need to render on the page (e.g state & country here) some data from the API, but for some reason is not working.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const APIFetch = () => {
const [user, setUser] = useState('');
const [info, setInfo] = useState([]);
const fetchData = async () => {
const data = await axios.get('https://randomuser.me/api');
return JSON.stringify(data);
}
useEffect(() => {
fetchData().then((res) => {
setUser(res)
setInfo(res.results);
})
}, [])
const getName = user => {
const { state, country } = user;
return `${state} ${country}`
}
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>
})}
</div>
)
}
Can you guys provide me some help? Thanks.
Try this approach,
const APIFetch = () => {
const [user, setUser] = useState("");
const [info, setInfo] = useState([]);
const fetchData = async () => {
const data = await axios.get("https://randomuser.me/api");
return data; <--- Heres is the first mistake
};
useEffect(() => {
fetchData().then((res) => {
setUser(res);
setInfo(res.data.results);
});
}, []);
const getName = (user) => {
const { state, country } = user.location; <--- Access location from the user
return `${state} ${country}`;
};
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>;
})}
</div>
);
};
Return data without stringify inside the fetchData.
Access user.location inside getName.
Code base - https://codesandbox.io/s/sharp-hawking-6v858?file=/src/App.js
You do not need to JSON.stringify(data);
const fetchData = async () => {
const data = await axios.get('https://randomuser.me/api');
return data.data
}
Do it like that
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const APIFetch = () => {
const [user, setUser] = useState('');
const [info, setInfo] = useState([]);
useEffect(() => {
const fetchData = async () => {
const res = await axios.get('https://randomuser.me/api');
setUser(res.data);
setInfo(res.data.results);
}
featchData();
}, [])
const getName = user => {
const { state, country } = user;
return `${state} ${country}`
}
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>
})}
</div>
)
}
Codesandbox: https://codesandbox.io/s/vigorous-lake-w52vj?file=/src/App.js

React cache google places api search results

So i have the following functional component that works fine. What i want to do is cache the results so i am not hitting the api over and over again for same search term.
import React, { useState, useEffect, useRef } from "react";
import currentSession from "../currentSession";
let autoComplete;
const loadScript = (url, callback) => {
let script = document.createElement("script");
script.type = "text/javascript";
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState === "loaded" || script.readyState === "complete") {
script.onreadystatechange = null;
callback();
}
};
} else {
script.onload = () => callback();
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
};
function handleScriptLoad(updateQuery, autoCompleteRef) {
autoComplete = new window.google.maps.places.Autocomplete(
autoCompleteRef.current,
{ types: ["(cities)"] }
);
autoComplete.setFields(["address_components", "formatted_address"]);
autoComplete.addListener("place_changed", () =>
handlePlaceSelect(updateQuery)
);
}
async function handlePlaceSelect(updateQuery) {
const addressObject = autoComplete.getPlace();
const query = addressObject.formatted_address;
updateQuery(query);
}
function SearchLocationInput(props) {
const [query, setQuery] = useState("");
const autoCompleteRef = useRef(null);
useEffect(() => {
loadScript(
"https://maps.googleapis.com/maps/api/js?key=" +
currentSession.getGoogleApiKey() +
"&libraries=places",
() => handleScriptLoad(setQuery, autoCompleteRef)
);
}, []);
return (
<div className="search-location-input">
<input
ref={autoCompleteRef}
onChange={(event) => {
props.onCityChange(event.target.value);
setQuery(event.target.value);
}}
placeholder="Enter a City"
value={query}
/>
</div>
);
}
export default SearchLocationInput;
I am assuming the request is occurring in the loaded script so i am not sure how i can accomplish this?

React Hooks state not updating variable in view

I'm trying to set a state, which I fetch from an API in the form of an array.
Tried this in every way possible, doesn't work.
Any ideas on how to fix this?
instance is an axios.create that creates the instance to a localhost django server which has CORS-ALLOW-CROSS-ORIGIN True
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
fetchText();
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Do it like this,
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
useEffect(() => {
fetchText();
},[ any variable you want it to fetch that again ]);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
This looks to be a good use case for a useEffect hook. Also, you need to await async functions within useEffect statement. The useEffect hook cannot be an async function in itself. Also, the original implementation would result in an infinite loop. The setState function would trigger a re-render which would then trigger the fetch function to fire off again. Consider the following:
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const request= await instance.get(`/one/list/`);
request.then( r => setOne(r.data))
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Based on you guys' advice, I came up with the below code which works fine so far.
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const [el, setEl] = useState(null);
const fetchText = async () => {
let res = await instance.get("/one/list/");
setOne(res.data);
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
useEffect(() => {
const handleClick = e => {
let newEl = document.createElement("input");
newEl.value = e.target.innerHTML;
newEl.id = e.target.id;
newEl.addEventListener("keypress", e => handleInput(e));
newEl.addEventListener("focusout", e => handleInput(e));
e.target.parentNode.replaceChild(newEl, e.target);
newEl.focus();
};
const handleInput = async e => {
console.log(e.type);
if (
e.target.value !== "" &&
(e.key === "Enter" || e.type === "focusout")
) {
let payload = { text: e.target.value };
try {
e.preventDefault();
let res = await instance.put(`/one/update/${e.target.id}`, payload);
let text = res.data.text;
e.target.value = text;
let newEl = document.createElement("span");
newEl.innerHTML = text;
newEl.addEventListener("click", e => handleClick(e));
newEl.id = e.target.id;
e.target.parentNode.replaceChild(newEl, e.target);
} catch (e) {
console.error(e);
}
}
};
setEl(
one.map(o => (
<p>
<span id={o.id} onClick={e => handleClick(e)}>
{o.text}
</span>
</p>
))
);
}, [one]);
return <>{el}</>;
};
export default OneList;

Resources