I am fetching data in React, I am fetching users, so I have a state (I am using redux) allUsers, in which I am storing fetched users and adding more users with infinite scroll once scrollbar reaches the bottom. The problem is that when new data is being added to the allUsers state, React is re-rendering all of the users because I am using .map to render them. Could you please tell me how to fix that? Here is the code:
const allUsers = useSelector(state => state.setAllUsersReducer);
const page = useSelector(state => state.setPageReducer);
const loading = useSelector(state => state.setLoadingReducer);
const dispatch = useDispatch();
const fetchAllUsers = () => {
console.log(page)
fetch(`${url}/${page}/15`)
.then(res => res.json())
.then(data => {
dispatch(setAllUsers(data.list))
})
.catch(err => console.log('Error message: ', err))
}
console.log(allUsers)
useEffect(() => {
fetchAllUsers();
}, [page])
const handleScroll = () => {
dispatch(setPage());
}
window.onscroll = function () {
if(window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
handleScroll();
}
}
return (
<div className="allUsersList">
{
allUsers.length > 0 ? (
allUsers.map((user, index) => (
<User key={index + 1} name={user.name} lastName={user.lastName} prefix={user.prefix} title={user.title} img={user.imageUrl}/>
))
) : (
<div> Loading... </div>
)
}
</div>
)
}
Related
I'm working on a project and wanted to try and implement an infinitely scrollable page to list users. Filtering and such works fine and all but every time the scrolling component reaches the ref element the component makes an API call to append to the list and then the parent component re-renders self completely.
const UsersList = () => {
const [searchString, setSearchString] = useState('')
const [next, setNext] = useState('')
const { userList, error, nextPage, loading, hasMore } = useFetch(next)
const [usersList, setUsersList] = useState([])
const observer = useRef()
const lastElemRef = useCallback(
(node) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setNext((prev) => (prev = nextPage))
setUsersList((prev) => new Set([...prev, ...userList]))
}
})
if (node) {
observer.current.observe(node)
}
},
[loading, nextPage, hasMore],
)
useEffect(() => {
setUsersList((prev) => new Set([...prev, ...userList]))
console.log(error)
}, [])
return (
<>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<CContainer
className="w-100 justify-content-center"
style={{ maxWidth: 'inherit', overflowY: 'auto', height: 600 }}
>
<CFormInput
className="mt-2 "
id="userSearchInput"
value={searchString}
onChange={(e) => {
setSearchString(e.target.value)
}}
/>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<>
{Array.from(usersList)
.filter((f) => f.username.includes(searchString) || searchString === '')
.map((user) => {
if (Array.from(usersList)[usersList.size - 1] === user) {
return (
<UserCard
key={user.id}
user={user}
parentRef={searchString ? null : lastElemRef}
/>
)
} else {
return <UserCard key={user.id} user={user} />
}
})}
</>
)}
</CContainer>
)}
</>
)
}
export default UsersList
This is the component entirely.
Here's my useFetch hook;
export const useFetch = (next) => {
const [userList, setUserList] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [nextPage, setNextPage] = useState(next)
const [hasMore, setHasMore] = useState(true)
useEffect(() => {
setLoading(true)
setError('')
axios
.get(next !== '' ? `${next}` : 'http://localhost:8000/api/getUsers/', {
headers: {
Authorization: 'Bearer ' + localStorage.getItem('access_token'),
},
})
.then((res) => {
setUserList((userList) => new Set([...userList, ...res.data.results]))
setNextPage((prev) => (prev = res.data.next))
if (res.data.next === null) setHasMore(false)
setLoading(false)
})
.catch((err) => {
setError(err)
})
}, [next])
return { userList, error, nextPage, loading, hasMore }
}
export default useFetch
I'm using Limit Offset Pagination provided by Django Rest Framework, next object just points to the next set of objects to fetch parameters include ?limit and ?offset added at the end of base API url. What is it that I'm doing wrong here ? I've tried many different solutions and nothing seems to work.
Solved
Apparently it was just my back-end not cooperating with my front-end so I've changed up the pagination type and now it seems to behave it self.
I am trying to fetch data from another fetched data by id, then display them together in a card using react hooks. I am getting an empty array error in the console.
Any help would be greatly appreciated. Also not sure if I am storing the data correclty in state.
const CardContainer = () => {
const [work, setWork] = useState([]);
const [work2, setWork2] = useState([]);
useEffect(() => {
fetch("https://www.hatchways.io/api/assessment/work_orders")
.then((response) => response.json())
.then((data) => {
console.log(data.orders);
data.orders.map((order) => {
console.log(order.workerId);
fetch(
`https://www.hatchways.io/api/assessment/workers/${order.workerId}`
)
.then((res) => res)
.then((data) => setWork2(data));
});
});
}, []);
console.log(work);
return (
<div>
<h2>Cards</h2>
{work.map((items, index) => (
<CardUI key={index} props={items} />
))}
</div>
);
};
Try
fetch('https://www.hatchways.io/api/assessment/work_orders')
.then((response) => response.json())
.then((data) => {
const respones = data.orders.map((order) =>
fetch(`https://www.hatchways.io/api/assessment/workers/${order.workerId}`).then((res) => res.json()),
);
Promise.all(respones).then((fetchedOrders) => {
setWork2(fetchedOrders);
});
});
If you have more specific question please ask in the comment
When you use map to create a new array of promises you should use Promise.all to wait until they've all either been resolved or been rejected. Then you can map over that JSON, parse it, and add it to state.
fetch('https://www.hatchways.io/api/assessment/work_orders')
.then(response => response.json())
.then(data => {
const orders = data.orders.map(order => {
fetch(`https://www.hatchways.io/api/assessment/workers/${order.workerId}`)
});
Promise.all(orders).then(data => {
setWork2(data.map(data => JSON.parse(el));
});
});
const CardContainer = () => {
const [work, setWork] = useState([]);
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetch('https://www.hatchways.io/api/assessment/work_orders')
.then((response) => response.json())
.then((data) => {
const respones = data.orders.map((order) =>
fetch(`https://www.hatchways.io/api/assessment/workers/${order.workerId}`).then((res) => res.json()),
);
Promise.all(respones).then((fetchedOrders) => {
setWork(fetchedOrders);
setIsLoading(false)
});
});
}, []);
return (
isLoading ? <div>Loading</div> : <div>
<h2>Cards</h2>
{work.map((item, index) => (
<div key={index} props={item}>{item.worker.companyName} </div>
))}
</div>
)
}
i have added loading and showing company name in card
output
I'm getting data from Django Rest API and React for Frontend, and I need to create the pagination with posts. I did it all in pagination component. I created the state with current page and I'm changing it by clicking on the page button in component like this:
const Paginator = () => {
const [count, setCount] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(null);
const [nextPage, setNextPage] = useState(null);
const [previousPage, setPreviousPage] = useState(null);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setCount(data.count);
setTotalPages(data.total_pages)
setNextPage(data.links.next);
setPreviousPage(data.links.previous);
setValid(true);
})
}, [currentPage]);
...
return (
<>
{
...
<PbStart style={style} totalPages={range(1, totalPages+1)} setCurrentPage={setCurrentPage} />
...
}
</>
);
};
const PagItem = ({key, handleClick, className, title, name }) => {
return (
<li key={key} onClick={handleClick}>
<Link to='/' className={className} title={`Go to page ${title}`}>
{name}
</Link>
</li>
);
};
const PbStart = ({ style, totalPages, setCurrentPage }) => {
return (
...
{totalPages.map(p => (
<PagItem key={p} handleClick={() => setCurrentPage(p)} title={p} name={p} />
))}
...
);
};
And in posts component I don't know how to change current page, or getting it from the paginaton component. I've written that like this:
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}, [currentPage]);
return (
<>
{
...
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
...
}
</>
);
};
So, how to do that(get the current page from pagination component or another way)?
I think a Paginator's job is only moving between pages and updating current page state. It should not be fetching data by itself, you can provide functionality to do extra work with props.
I haven't tested this, but this might be a good starting point.
With the example below you'll have a list of articles and then below it next and previous buttons.
In Softwares, as you can see I am passing the same function for handling next and previous pages, you can refactor it to have one function like onPageMove and call this function handleNext and handlePrev.
I added two separate functions if you have want to handle something different in either.
const Paginator = ({
total, // Required: Total records
startPage = 1, // Start from page / initialize current page to
limit = 30, // items per page
onMoveNext = null, // function to call next page,
onMovePrev = null, // function to call previous page
}) => {
const [currentPage, setCurrentPage] = useState(startPage);
const canGoNext = total >= limit;
const canGoPrev = currentPage > 1;
function handleNext(e) {
if (canGoNext) {
setCurrentPage((prevState) => prevState+1);
onMoveNext && onMoveNext({ currentPage });
}
}
function handlePrev(e) {
if (canGoPrev) {
setCurrentPage((prevState) => prevState-1);
onMovePrev && onMovePrev({ currentPage });
}
}
return (
<div>
<button onClick={handlePrev} disabled={!canGoPrev}>Prev</button>
<button onClick={handleNext} disabled={!canGoNext}>Next</button>
</div>
);
};
Here is how you can use Paginator in other components.
const PER_PAGE = 30; // get max # of records per page
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [valid, setValid] = useState(false);
const onFetchData = ({ currentPage }) => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}&per_page=${PER_PAGE}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}
useEffect(() => {
onFetchData({ currentPage: 1 })
}, []);
return (
<>
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator total={softwares.length} limit={PER_PAGE} onMoveNext={onFetchData} onMovePrev={onFetchData} />
</>
);
};
I have a component in which I am trying to add pagination functionality. I have made good progress in the structure so far. I have category buttons, where when a category button is selected, the element below shows a list of posts within that category.
For the pagination functionality, I using the WordPress REST API to pull in 5 posts at a time for each category. I am trying to create a "load more" button after the 5th post, where onClick it will load the next 5 posts:
const Posts = ({ state, actions }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
const [morePosts, setMorePosts] = useState([]);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5&page=" + 2)
.then((response) => response.json())
.then((data) => {
setMorePosts(data);
});
}
}, [categoryId]);
return (
<>
{categories.length > 0 ? (
categories.map((category, i) => {
return (
<button key={i} onClick={() => setCategoryId(category.id)}>{category.name}</button>
)
})
) : (
<p>Loading...</p>
)
}
<div>
{posts.length === 0 ? (
<p>No posts...</p>
) : (
<>
<ol>
{posts.map((post, i) => {
return (
<li key={i}>{post.title.rendered}</li>
)
})}
</ol>
<button onClick={() => setMorePosts(category.id)}>Load More</button>
{console.log(morePosts.map((post) => post.title.rendered))}
</>
)}
</div>
</>
)
}
As you can see, under the last button I am consoling the array, and it does return the next 5 posts for the selected category. I am stuck on how to turn that array from the console into actually being shown on the page.
I would do it this way:
Load more button changes the page number.
When page number is changed: useEffect fires and posts are refetched/rerendered based on page number param.
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]);
useEffect(() => {
if (!categoryId) {
return;
}
let url = `${state.source.api}/wp/v2/postscategories=${categoryId}&per_page=5"`;
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, ...data]);
});
}, [categoryId, page]);
<button onClick={() => { setPage(page + 1); }}>Load more</button>
I have a React component that renders a list of items that are being mapped over, and shows the id for each item. When the component first loads, the list items appear, but then a second or two later disappears and in the console returns undefined.
The component being rendered is:
const Posts = ({ state }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
useEffect(() => {
if (!categoryId) {
return;
}
let url = state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, data]);
});
}, [categoryId, page]);
return (
<>
{categories.length > 0 ? (
categories.map((category, i) => {
return (
<button key={i} onClick={() => setCategoryId(category.id)}>{category.name}</button>
)
})
) : (
<p>Loading...</p>
)
}
<div>
{posts.length === 0 ? (
<p>No posts...</p>
) : (
<>
<ol>
{posts.map((post, i) => {
console.log(post.id);
return (
<li key={i}>{post.id}</li>
)
})}
</ol>
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
)}
</div>
</>
)
}
And the console shows:
I have a lot of code commented out above this component, so the line in the console is referring to console.log(post.id);.
Does anyone have an idea of what could be causing this?
This useEffect is not needed. Both useEffects on initial load trying to hit same endpoint with same url params.
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
And I think arrays are not merged correctly here:
try to replace:
setPosts([...posts, data]);
to:
setPosts([...posts, ...data]);