React hooks context component doesn't re-render when context state changes - javascript

I am working on a react app that should allow a user to browsers records from a database one page at a time.
I am using functional components and react context to manage state. I have a useEffect that will pulls a function from my Context to fetch records from the database and puts them into the Context State. The component pulls the array of records from the Context State and if they is at least 1 (array length > 0) it will map the array using each record to populate a record list item component.
The data is pulled and the React Dev Tools shows that the data is in the Context. I can console log the records array (with records) to the console and I can even log the list of elements that are the Record List Item Components to the console, but the page always show the "No Records Found" message that you should get if the array is empty (length of 0).
The main component is:
<ul className="list-group">
{
records.length > 0
? (records.map((rec) => <RecordListItem record={rec}/>))
: (<li className="list-group-item">No Records Found</li>)
}
</ul>
the useEffect in the RecordsList is:
const {
filter,
getRecords,
page,
perPage,
records,
} = useContext(RecordsContext);
useEffect(() => {
const start = (page - 1) * perPage;
getRecords(start, perPage, filter); // filter, page, and perPage come from the Context
}, []); // only want this useEffect to run once when the component loads.
getRecords is a function that queries the database for records starting from one record and going up to the number of records per page.
const getRecords = async (start, limit, filter) => {
if (filter) {
const items = await db("contacts").select()
.where({status: filter})
.offset(start).limit(limit);
} else {
const items = await db("contacts").select()
.offset(start).limit(limit);
}
dispatch({type: SET_RECORDS, payload: items);
The records are showing up in the dev tools and in the console logs. I am not seeing a problem with the database connection or queries.
The only problem I am seeing is that the component doesn't update when the state inside the context changes. This is only happening with this one element. Other state values are changing and the components that use them are updating.
For example. When the filter is picked the number of records updates to reflect the total number of records with that filter. Adding, updating, and deleting records in the database also cause these numbers to update when the components are refreshed.
I have another useEffect that will trigger when the page or perPage changes and another that resets the page to 1 and getsRecords again when the filter changes. Both of these useEffects are currently not in the code to avoid them causing issues.
The Parent Component is the RecordsViewer. This is simply just a shell to hold the other elements in. It creates a Navbar at the top that gives filter options as well as paging and items per page options and a section with a container
The RecordsList is a card that contains the List-Group element and gets the data from the Context. It then maps over the fetched records and passes each record to a display component.
The RecordsContextProvider wraps the RecordsViewer and I can see the data in the Context within the React Dev Tools. Also doing a console.log(records) does show an array of the correct number of items. I have tried capturing the map of records in a value and then showing that, but no change. I can console.log that variable and see it is an array of react elements of type "LI".
I am completely lost as to what the hang up is here.

I've found the issue. Misspelled length as lenght in the condition to check if the array was empty or not.
What I get for trying to work after less than 5 hours of sleep.

Related

React Query // How to properly use client-state and server-state (when user can drag/sort items on the UI)?

I have an React App using React Query to load a dataset of folders containing items. The app allows the user to drag/sort items (within a folder and drag to another folder). Before implementing drag/sort it was simple, RQ fetched the dataset folderData and then the parent supplied data to child components: parent > folder > item .
In order to implement drag/sort, I am now having to copy the entire dataset folderData into a client-state variable foldersLocal. This is the only way I could figure out how to change the UI when the user moves an item.
However, I feel like this approach essentially removes most of the benefits of using React Query because as soon as the data is fetched, I copy the entire dataset into a "client-side" state variable (foldersLocal) and only work with it. This also makes the QueryClientProvider functionality effectively useless.
Is there a better way to approach this? Or am I missing something?
// Fetch data using React Query
const {isLoading, isError, foldersData, error, status} = useQuery(['folders', id], () => fetchFoldersWithItems(id));
// Place to hold client-state so I can modify the UI when user starts to drag/sort items
const [foldersLocal, setFoldersLocal] = useState(null);
// Store React Query data in client-state everytime it's fetched
useEffect(() => {
if (status === 'success') {
setFoldersLocal(foldersData);
}
}, [foldersData]);
// Callback to change "foldersLocal" when user is dragging an item
const moveItem = useCallback((itemId, dragIndex, hoverIndex, targetFolderId) => {
setFoldersLocal((prevData) => {
// change data, etc. so the UI changes
}
}
// Drastically simplified render method
return (
<QueryClientProvider client={queryClient}>
<FolderContainer folders={foldersLocal} moveItem={moveItem} moveItemCompleted={moveItemCompleted}/>
</QueryClientProvider>
)

Sorting a JSON file in React, component not rerendering onClick

I am working on a E-Commerce site in React that displays a list of items that are derived from a JSON file. The JSON file includes info such as the price, model, image, etc.
I'm trying to add "Sort by: Price" functionality to the site. The function I have right now works correctly in sorting the JSON data:
import products from "../data/products.json";
function Shop(){
const [items, setItems] = useState();
function sortProducts() {
setItems(products.sort(sortByProperty("price")));
}
function sortProductsReverse(){
setItems(products.sort(sortByProperty("price")).reverse);
}
return (
{products.map((product) => (
<React.Fragment>
<Product {...product} />
</React.Fragment> )
}
}
sortByProperty does the sorting logic from lowest to highest, sortProducts sorts from lowest to highest, sortProductsReverse sorts from highest to lowest. Product is the component that takes in the JSON data and displays everything.
When the page is first loaded, all the products are displayed correctly without being sorted. When a button is onClicked to sortProducts or sortProductsReverse, it works. However, it works only once. The items cannot be sorted/re-rendered again.
I think I'm missing something in items useState, but I'm not sure what I should do.
For your component to rerender, you need to set the state with a new object reference, React then compares previous and current state, and if they are not equal, it rerenders the component.
In your case, items is initially undefined, and when you set it as products, the component rerenders.
After that, everytime you call products.sort, it sets items the same object from before, because sort method in JavaScript mutates the original object, but doesn't return a new one.
To solve the problem, everytime call setItems with a new object:
setItems([...products].sort(sortByProperty("price")));

Conditional Rendering of Arrays in React: For vs Map

I'm new to React and building a calendar application. While playing around with state to try understand it better, I noticed that my 'remove booking' function required a state update for it to work, while my 'add booking' function worked perfectly without state.
Remove bookings: requires state to work
const [timeslots, setTimeslots] = useState(slots);
const removeBookings = (bookingid) => {
let newSlots = [...timeslots];
delete newSlots[bookingid].bookedWith;
setTimeslots(newSlots);
}
Add bookings: does not require state to work
const addBookings = (slotid, tutorName) => {
timeslots[slotid].bookedWith = tutorName;
}
I think that this is because of how my timeslot components are rendered. Each slot is rendered from an item of an array through .map(), as most tutorials online suggest is the best way to render components from an array.
timeslots.map(slot => {
if (!slot.bookedWith) {
return <EmptyTimeslot [...props / logic] />
} else {
return <BookedTimeslot [...props / logic]/>
}
})
So, with each EmptyTimeslot, the data for a BookedTimeslot is available as well. That's why state is not required for my add bookings function (emptyTimeslot -> bookedTimeslot). However, removing a booking (bookedTimeslot -> emptyTimeslot) requires a rerender of the slots, since the code cannot 'flow upwards'.
There are a lot of slots that have to be rendered each time. My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots? This I assume would require state to be used for both the add booking and remove booking function. Like this:
for (let i=0;i<timeslots.length;i++) {
if (!timeslot[i].bookedWith) {
return <EmptyTimeslot />
} else {
return <BookedTimeslot />
}
}
Hope that makes sense. Thank you for any help.
Your addBooking function is bad. Even if it seems to "work", you should not be mutating your state values. You should be using a state setter function to update them, which is what you are doing in removeBookings.
My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots?
Your map approach is not rendering both. For each slot, it uses an if statement to return one component or the other depending on whether the slot is booked. I'm not sure how the for loop you're proposing would even work here. It would just return before the first iteration completed.
This I assume would require state to be used for both the add booking and remove booking function.
You should be using setTimeslots for all timeslot state updates and you should not be mutating your state values. That is true no matter how you render them.

How to change local state without changing global state

I am making a blog and I want to show a list of articles under each post so the user do not need to go back to the front page. But I do not want the article they are currently reading to be in the list.
I am using filter method and it's working, but only for a split second. As far as i understand it is happening because of the useContext. I do not want to change global state, only local.
const ArticleDetails = (props) => {
const {data} = useContext(ArticleContext); //data with all fetched articles from db
const article = props.location.state.article; //data for individual article
return (
<div>
//showing here data for individual post that i passed in props
{data.filter(item => {return item !== article})
.map(item => {return <ArticleList articleTitle={item.title} key={item.id} />})}
</div>
)
}
It can be that the useContext hooh has been completed some modification to the data you use in return after the render.
By filter or map you actually are not changing any global state or local state at all.
After that above mentioned update, your data items might be changing. Also your filter function needs to filter with a never changing id or something like that so your filter results stays consistent across renders or state updates.
data.filter(item => {return item.id !== article.id})
Also using equality for the objects like that will not work for your filter criteria. You use not equal criteria and yes after a render your item will not be equal to article in a subsequent render even they are same reference in one render. That kind of equality is checked by Object.is that would be a specific need. But I think you just need an id check.

Reactjs render component only when ALL its child done thier render

I have a "slow" loading issue in one of my pages.
That page contains a "root" component - let's call it Container.
My Container makes 2 fetch calls upon a value selection from a dropdown list.
The fetched data (A and B) are passed to a child component (Board).
Board is built from 2 unrelated components - Player and Item and they use A and B respectively as initial data.
It so happens that the Players render process is much more heavy than Item's.
The result is that Item is rendered on the page and after some few seconds Player is rendered also.
My wish is to see both rendering at the same time - how can I achieve that?
I guess it is something should be done by Board but how?
It is also important that this "waiting" will take place every time the value from the droplist is changed (it always there for the users to choose).
Thanks!
There are a couple of ways to do this.
1) Use graphql to orchestrate the loading of data, so that the page renders when all of the data is available
2) Set state values for playerDataReady and itemDataReady. If either of these is false, display a loading spinner. When they are both true (as a result of ajax calls being resolved), then the render will do both at the same time
In the constructor...
state = {
playerDataReady: false,
itemDataReady: false,
}
:
:
componentDidMount() {
// Request player data, using axios or fetch
axios.get(...)
.then(data) {
this.data = data
this.setState({playerDataReady:true})
}
// Do the same for your item data (and update itemDataReady)
}
In your render function:
render() {
if (!playerDataReady || !itemDataReady) {
return <Loader/>
// The data has arrived, so render it as normal...
return ....
You can fill in the details

Categories

Resources