React updates after two clicks instead of one - reactjs

I'm creating a sortable table, and my issue is that the sorting shows after TWO clicks on a column, instead of just after one click.
This is my sorting logic: (DATA is hardcoded)
export const useFetch = (order) => {
const [data, setData] = useState();
const orderBy = order?.by || 'offer_id';
const fromTo = order?.fromTo || SORTING_ORDER.ascending;
useEffect(() => {
if (!orderBy || !fromTo) return;
let orderedData = DATA.sort((a, b) => (a[orderBy] - b[orderBy]));
setData(orderedData);
}, [orderBy, fromTo]);
return { data, status };
};
And I'm using this hook like this, from the component that has that table.
export const AcceptedOffers = ({ setModalIsOpen }) => {
const [order, setOrder] = useState({ by: 'maturity', fromTo: SORTING_ORDER.ascending });
const { data, status } = useFetch(order);
function onHeaderClick(header) {
setOrder({ by: header, fromTo: SORTING_ORDER.descending });
}
return (
<WidgetContainer>
<Title>
Accepted Offers
</Title>
<Table>
<Header>
<tr>
{
Object.entries(HEADERS).map(([key, value]) =>
<th
key={key}
onClick={() => onHeaderClick(key)}
>
{value}
</th>)
}
</tr>
</Header>
<Body>
{
data?.map(row => (<tr key={row.offer_id}>
<td>{row.offer_id}</td>
etc...
Can anyone explain what's wrong with this. Thank you.

The side effect represented by your useEffectis executed after the render triggered by the click: the data are rendered first, then sorted. That's where your "delay" commes from.
Here is an other solution. It may suit you, or not: the purpose is to show an alternative implementation to trigger the sort when order is modified, but without useEffect. It works by "overloading" setOrder:
export const useFetch = (initialOrder) => {
// useReducer may be a better choice here,
// to store order and data with a single state
// (and update this state through a single call)
const [order, setOrder] = useState(initialOrder);
const [data, setData] = useState();
const publicSetOrder = (newOrder) => {
setOrder(newOrder);
const orderBy = newOrder?.by || 'offer_id';
const fromTo = newOrder?.fromTo || SORTING_ORDER.ascending;
if (!orderBy || !fromTo) return;
let orderedData = DATA.sort((a, b) => (a[orderBy] - b[orderBy]));
setData(orderedData);
};
return { order, setOrder: publicSetOrder, data, status };
};
export const AcceptedOffers = ({ setModalIsOpen }) => {
const { order, setOrder, data, status } = useFetch({ by: 'maturity', fromTo: SORTING_ORDER.ascending });
function onHeaderClick(header) {
setOrder({ by: header, fromTo: SORTING_ORDER.descending });
}
// ...
Feel free to adapt to your use case ;)

Related

Error message "Cannot read properties of null (reading 'filter')"

I'm new to learning react and have been having problems getting the array to filter using the .filter() method. I'm trying to create a grocery list and I keep getting the error message "Cannot read properties of null (reading 'filter')" Can someone please assist me on getting this work? Here is the code that I have.
import Header from './Header';
import SearchItem from './SearchItem';
import AddItem from './AddItem';
import Content from './Content';
import Footer from './Footer';
import { useState, useEffect } from 'react';
function App() {
const [items, setItems] = useState(JSON.parse(localStorage.getItem('shoppinglist')));
const [newItem, setNewItem] = useState('')
const [search, setSearch] = useState('')
console.log('before useEffect')
//useEffect looks to it's dependency and if the dependency changes then it will run the anonymous function
useEffect(() => {
console.log('inside useEffect')
},[items])
const setAndSaveItems = (newItems) => {
setItems(newItems);
localStorage.setItem('shoppinglist', JSON.stringify(newItems));
}
console.log('after useEffect')
const addItem = (item) => {
const id = items.length ? items[items.length - 1].id + 1 : 1;
const myNewItem = { id, checked: false, item };
const listItems = [...items, myNewItem];
setAndSaveItems(listItems);
}
const handleCheck = (id) => {
const listItems = items.map((item) => item.id === id ? { ...item, checked: !item.checked } : item);
setAndSaveItems(listItems);
}
const handleDelete = (id) => {
const listItems = items.filter((item) => item.id !== id);
setAndSaveItems(listItems);
}
const handleSubmit = (e) => {
e.preventDefault();
if (!newItem) return;
addItem(newItem);
setNewItem('');
}
return (
<div className="App">
<Header title="Grocery List" />
<AddItem
newItem={newItem}
setNewItem={setNewItem}
handleSubmit={handleSubmit}
/>
<SearchItem
search={search}
setSearch={setSearch}
/>
<Content
items={items.filter(item => ((item.item).toLowerCase()).includes(search.toLowerCase()))}
handleCheck={handleCheck}
handleDelete={handleDelete}
/>
<Footer length={items.length} />
</div>
);
}
export default App;
I feel that you're mentioning about this code excerpt:
items.filter((item) => item.id !== id);
can you please check if the items array is null or not. Only if items is null, filtering wouldn't be applicable and you will receive such error messages
can you log items before deletion?
Few pointers that could help
initilize the items in an useEffect as it could be null, it will make it easy to fetch data a api later
const [items, setItems] = useState([]);
useEffect(() => {
try {
const items = JSON.parse(localStorage.getItem('shoppinglist'))
setItems(items)
} catch(error) {
}
}, [])
// put ?. checks on items when calling filter, map
const handleDelete = (id) => {
const listItems = items?.filter((item) => item.id !== id);
if (listItems) {
setAndSaveItems(listItems);
}
}
Id generated will clash and cause bugs
const id = items.length ? items[items.length - 1].id + 1 : 1;
if the person deletes on item and adds another the new item will have the same id as the last one
item { id: 1}
item { id: 2}
item { id: 3}
after deleting id 2, when you add new items it will have id 3
and will cause bugs with select
either use a id that is a timestamp or check for unique ids
Save the items in local storage on submit, as calls get/set items to localstorage can lead to performace issues in the UI
Checkout the new docs on working with arrays
Hope it helps

React Child Component Api Calling Two Times (strict mode is off)

Here, are three components User Details and its two Childs are UserSpecificData1 and UserSpecificData2.
In User Details component im getting User Details with userId by api calling.
Now i declared Two childs by passing that user id.
Problem is: Two child api is calling two times! Why? React strict mode is off.
Note: I noticed that child components are rendering two times by console.log
`
export const UserDetails = () => {
const params = useParams(); // {userId: 223}
useEffect(() => {
if(params?.userId){
getCustomerDetails(params.userId) // 223
}
}, [params.userId]);
return (
<div>
<UserSpecificData1 userId={params.userId}/>
<UserSpecificData2 userId={params.userId}/>
</div>
);
};
// Component 1
const UserSpecificData1 = props => {
const [currentPage, setCurrentPage] = useState(0);
const [filteredBy, setFilteredBy] = useState({});
const [sortBy, setSortBy] = useState('ASC');
useEffect(() => {
getSpecificDataOne({
id: props.userId, //223
filteredBy: filteredBy,
page: currentPage,
size: 10,
sortBy: sortBy,
})
}, [sortBy, currentPage, filteredBy]);
return <div>
</div>
};
// Component 2
const UserSpecificData2 = props => {
const [currentPage, setCurrentPage] = useState(0);
const [filteredBy, setFilteredBy] = useState({});
const [sortBy, setSortBy] = useState('ASC');
useEffect(() => {
getSpecificDataTwo({
id: props.userId, //223
filteredBy: filteredBy,
page: currentPage,
size: 10,
sortBy: sortBy,
})
}, [sortBy, currentPage, filteredBy]);
return <div>
</div>
};
`
Hey i just reviewed your code and i came up with conclusion that you have to add a condition on both child useEffect where api is being called and check for prop.userId exist or not and don't forgot to passed it as dependency array.
useEffect(()=>{
if(props?.userId){
getSpecificDataTwo({
id: props.userId, //223
filteredBy: filteredBy,
page: currentPage,
size: 10,
sortBy: sortBy,
});
}
},[sortBy, currentPage, filteredBy,props.userId]);
let me know if this works for you otherwise we will go for another way.
My guess is that the code isn't quite complete?
So I'm assuming you also have a [content, setContent] somewhere in the first component UserDetails - and if so, it'll first render the child components, and then, if params.userId exists, after the content has loaded it'll re-render.
A couple of ways to stop this, probably the best being surrounding your child components with { content && <Child 1 />...}
So complete code would be:
export const UserDetails = () => {
const params = useParams(); // {userId: 223}
const [content, setContent] = useState(null)
useEffect(() => {
if(params?.userId){
getCustomerDetails(params.userId)
.then(result => {
setContent(result);
}) // 223
}
}, [params.userId]);
return (
<div>
{ content &&
<>
<UserSpecificData1 userId={params.userId}/>
<UserSpecificData2 userId={params.userId}/>
</>
}
</div>
);
};
Personally I'd probably also put the userId into a hook and use that as the check, up to you which works better.

Wait until onSnapshot data is fetched Firestore Firebase Next JS

On Click I want to fetch onSnapshot instance of data which startAfter current data. But When the request is sent for the 1st time it returns an empty array. I have to click twice to get new Data.
export default function Page() {
const [data, setData] = useState([])
const [newData, setNewData] = useState([])
const [loading, setLoading] = useState(false)
const [postsEnd, setPostsEnd] = useState(false)
const [postLimit, setPostLimit] = useState(25)
const [lastVisible, setLastVisible] = useState(null)
useEffect(() => {
const collectionRef = collection(firestore, "Page")
const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), limit(postLimit));
const unsubscribe = onSnapshot(queryResult, (querySnapshot) => {
setLastVisible(querySnapshot.docs[querySnapshot.docs.length-1])
setData(querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
CreatedOn : doc.data().CreatedOn?.toDate(),
UpdatedOn : doc.data().UpdatedOn?.toDate()
}))
)
});
return unsubscribe;
}, [postLimit])
const ths = (
<tr>
<th>Title</th>
<th>Created</th>
<th>Updated</th>
</tr>
);
const fetchMore = async () => {
setLoading(true)
const collectionRef = collection(firestore, "Page")
const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), startAfter(lastVisible), limit(postLimit));
onSnapshot(await queryResult, (querySnapshot) => {
setLastVisible(querySnapshot.docs[querySnapshot.docs.length-1])
setNewData(querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
CreatedOn : doc.data().CreatedOn?.toDate(),
UpdatedOn : doc.data().UpdatedOn?.toDate()
}))
)
});
//Commented If Statement cause data is not fetched completely
//if (newData < postLimit ) { setPostsEnd(true) }
setData(data.concat(newData))
setLoading(false)
console.log(newData)
}
return (
<>
<Table highlightOnHover>
<thead>{ths}</thead>
<tbody>{
data.length > 0
? data.map((element) => (
<tr key={element.id}>
<td>{element.id}</td>
<td>{element.CreatedOn.toString()}</td>
<td>{element.UpdatedOn.toString()}</td>
</tr>
))
: <tr><td>Loading... / No Data to Display</td></tr>
}</tbody>
</Table>
{!postsEnd && !loading ? <Button fullWidth variant="outline" onClick={fetchMore}>Load More</Button> : <></>}
</>
)
}
Current Result
Expected Result
The problem seems to be rooted in React updating the state asynchronously. I saw the same behavior using your code in my Next.js project. When looking at it closely, the onSnapshot() function and your query are working as expected.
You cannot await onSnapshot() (code reference) since it's not an asynchronous function, so the async/await in fetchMore() does not have any effect. Regardless, React state update is asynchronous, so setData(data.concat(newData)) might run before newData is updated. By the second time you load more documents, newData will have the document data.
It looks like fetchMore() can be done without relying on a "newData" state, since it was only used to update the actual data and to hide the "Load More" button:
const fetchMore = () => {
setLoading(true)
const collectionRef = collection(firestore, "page")
const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), startAfter(lastVisible), limit(postLimit));
onSnapshot(queryResult, (querySnapshot) => {
setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]);
//paginatedDocs simply holds the fetched documents
const paginatedDocs = querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
CreatedOn: doc.data().CreatedOn?.toDate(),
UpdatedOn: doc.data().UpdatedOn?.toDate()
}));
//Replaced usage of newData here with the amount of documents in paginatedDocs
if (paginatedDocs.length < postLimit ) { setPostsEnd(true) }
//Updates the data used in the table directly, rebuilding the component
setData(data.concat(paginatedDocs));
});
setLoading(false);
}
With these changes, there is no problem updating the documents at the first try when clicking the button.
Do you have persistence on? It could be firing once with the local result and again with the server data.

updating data in useState constant | React

I'm trying to make "load more" pagination in react app fetching data from google books api via axios.
Firstly I'm grabbing data by submitting the form and setting it into the bookResponse const and then putting the value of bookResponse const into the booksArray const by setBooksArray(bookReponse).
Secondly I need to show more book's by clicking the 'Load More' button. By click I make a new request to the api and receive the response. I'm updating the bookResponse const with new data and trying to update the booksArray const by setBooksArray(booksArray + bookResponce) but it returns errror "books.map is not a function". How can i solve the problem?
{books && books.map(book => (
<div key={book.id}>
<img alt="book's thumbnail" src={book.volumeInfo.imageLinks.thumbnail} />
<div>
<h5>{book.volumeInfo.title}</h5>
<p>{book.volumeInfo.authors}</p>
<p>{book.volumeInfo.categories}</p>
</div>
</div>
))
}
function App() {
const [bookName, setBookName] = useState('')
const [bookCategory, setBookCategory] = useState('all')
const [bookFilter, setBookFilter] = useState('relevance')
const [bookResponse, setBookResponse] = useState([])
const [booksArray, setBooksArray] = useState([])
const apiKey = ('MY_KEY')
const [showLoadMoreButton, setShowLoadMoreButton] = useState(false)
const handleSubmit = (e) => {
e.preventDefault()
console.log(bookName, bookCategory, bookFilter)
axios.get(endpoint)
.then(res => {
console.log(res.data.items)
setBookResponse(res.data.items)
setBooksArray(bookResponse)
console.log(bookResponse ,booksArray)
})
setShowLoadMoreButton(true)
}
const handleLoadMore = (e) => {
e.preventDefault()
axios.get(endpoint)
.then(res => {
console.log(res.data.items)
console.log(bookResponse)
setBookResponse(bookResponse)
setBooksArray(booksArray + bookResponse)
})
}
You don't + to add 2 arrays, you can do something like this.
setBooksArray( prev => [ ...prev, ...bookResponse ] )
....
return (
....
{ booksArray.map(book => ( .....) }
.....
)
if booksArray and bookResponse types are array you can use this:
setBooksArray([...booksArray,...bookResponse])
When you do booksArray + bookResponse you are creating a string,
example would be
const arr1 = [1,2,3]
const arr2 = [4,5,6]
console.log(arr1 + arr2)
"1,2,34,5,6"
The solution would be to spread both of the arrays like this:
setArray([...arr1, ...arr2])

React useEffect not running after state change

I am querying Firebase real time database, saving the result into state and then rendering the results.
My data is not displaying because the page is rendering before the data is had. What I don't understand is why
useEffect(() => {
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
}, [projects]);
My use effect is not running once the projects state change is triggered, populating the array and triggering the conditional render line.
What am I missing here?
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const result = [];
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
useEffect(() => {
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
}, [projects]);
return (
<>
{result.length > 0 && result}
</>
);
};
result should also be a state!
Right now at every rerender result is being set to []. So when the useEffect does kick in, the subsequent rerender would set result to [] again.
This should not be a useEffect. Effects run after rendering, but you're trying to put <Project> elements on the page, which must happen during rendering. Simply do it in the body of your component:
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
const result = [];
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
return (
<>
{result.length > 0 && result}
</>
);
result.push does not mutate result in place. It instead creates a copy of the array with the new value.
As a solution, you could get your current code working by hoisting result into a state variable like so:
const [result, setResult] useState([])
...
useEffect(() => {
for (var key in projects) {
...
setResult([...result, <Project props={projectData} />])
}
}, [result, projects]);
however, this solution would result in an infinite loop...
My suggestion would be to rework some of the logic and use projects to render your Project components, instead of creating a variable to encapsulate your render Components. Something like this:
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
return (
<>
{projects.length > 0 && projects.map(project=>{
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
return <Project props={projectData} />
})}
</>
);
};
You're component is not re-rendering since react doesn't care about your result variable being filled.
Set it up as a state like this: const [result, setResult] = useState([]);
Then use map to return each item of the array as the desire component:
{result.length > 0 && result.map((data, index) => <Project key={index} props={data} />)}

Resources