Component re-renders self after API call whilst using useRef - javascript

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.

Related

Redux State Management with a Input field

Bit of a noob to redux but this appears to be quite a difficult question! I hope someone may be able to help me :)
I have build a page where you can input a search for different types of jobs. From this, it will make a get request to my DB and get all the info on this job. As this page is multi-levelled, I want to use redux to dispatch and pass the state throughout. This will help me pass my data on the job, e.g Data Analyst, through to each component so it can use the data and populate fields.
However, this was how my input field was originally setup:
export function SearchBarComp(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside ] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [jobPostings, setjobPostings] = useState([]);
const [noRoles, setNoRoles] = useState(false)
const isEmpty = !jobPostings || jobPostings.length === 0;
const changeHandler = (e) => {
//prevents defaulting, autocomplete
e.preventDefault();
if(e.target.value.trim() === '') setNoRoles(false);
setSearchQuery(e.target.value);
}
const expandedContainer = () => {
setExpanded(true);
}
//LINK THE BACKEND!
const prepareSearchQuery = (query) => {
//const url = `http://localhost:5000/api/role/title?title=${query}`;
const url = `http://localhost:5000/api/role/titlerole?title=${query}`;
//replaces bad query in the url
return encodeURI(url);
}
const searchRolePosition = async () => {
if(!searchQuery || searchQuery.trim() === "")
return;
setLoading(true);
setNoRoles(false);
const URL = prepareSearchQuery(searchQuery);
const response = await axios.get(URL).catch((err) => {
console.log(err);
});
if(response) {
console.log("Response", response.data);
if(response.data && response.data === 0)
setNoRoles(true);
setjobPostings(response.data);
}
setLoading(false);
}
useDebounce(searchQuery, 500, searchRolePosition)
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
// console.log("Value", searchQuery)
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={searchQuery}
onChange={changeHandler}
/>
<AnimatePresence>
{isExpanded && (<CloseIconSpan key="close-icon"
inital={{opacity:0, rotate: 0}}
animate={{opacity:1, rotate: 180}}
exit={{opacity:0, rotate: 0}}
transition={{duration: 0.2}}
onClick={collapseContainer}>
<CloseIcon/>
</CloseIconSpan>
)}
</AnimatePresence>
</SearchInputContainer>
{isExpanded && <LineSeperator/>}
{isExpanded && <SearchContent>
{!isLoading && isEmpty && !noRoles && (
<Typography color="gray" display="flex" flex="0.2" alignSelf="center" justifySelf="center">
Start typing to search
</Typography>
)}
{!isLoading && !isEmpty && <>
{jobPostings.map((searchRolePosition) => (
<JobSection
title={searchRolePosition.title}
//will need to do something like ----
//people = {searchRolePosition.title && searchRolePosition.title.average}
// future implementations
/>
))}
</>}
</SearchContent>}
</SearchBarContainer>
)
}
As you can see, the main thing is the 'query' this creates a backend request to my titlerole, such as getting the data on Data Analyst. This all works in my frontend right now, but I can't pass that data down to the next component etc
So i'm looking to use redux.
I've created the following slice:
import { createSlice } from "#reduxjs/toolkit";
const jobSearchSlice = createSlice({
name: "jobsearch",
initialState: {
currentRole: null,
isFetching: false,
error: false,
},
reducers: {
jobsearchStart: (state) => {
state.isFetching = true;
},
jobsearchSuccess: (state, action) => {
state.isFetching = false;
state.currentRole = action.payload;
},
jobsearchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { jobsearchStart, jobsearchSuccess, jobsearchFailure } = jobSearchSlice.actions;
export default jobSearchSlice.reducer;
With this, I'm also using the following apiCalls.js file:
import { jobsearchStart, jobsearchSuccess, jobsearchFailure } from "./jobsearchSlice";
import { publicRequest } from "../requestMethods";
export const roleSearchQuery = async (dispatch, jobsearch) => {
dispatch(jobsearchStart());
try{
const res = await publicRequest.get("`http://localhost:5000/api/role/titlerole?title=${query}`", jobsearch);
dispatch(jobsearchSuccess(res.data))
}catch(err){
dispatch(jobsearchFailure());
}
};
My question is as a Redux noob, how do I implement this query functionality into a redux API request? What's the way to do this properly as I begin to tranisition this to an app which uses standard state management!

Rendering only the specific component on Select option change. React

I have created a react app which will fetch the api using the values from the url params. which are modified using navigate prop without page refresh.
Here is the code.
const App = () => {
const [itemData, setItemData] = useState({});
const [itemError, setItemError] = useState({});
const [additionalData, setAdditionalData] = useState({});
const [additionalError, setAdditionalError] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [showTrailer, setShowTrailer] = useState(false);
const [trailer, setTrailer] = useState({});
const [trailerError, setTrailerError] = useState({});
const [group, setGroup] = useState([])
const backend_url = process.env.REACT_APP_BACKEND;
const handleCloseTrailer = () => setShowTrailer(false);
const handleShowTrailer = () => setShowTrailer(true);
const location = useLocation();
const id = location.pathname.split("/")[2];
const [searchParams, setSearchParams] = useSearchParams();
const [people, setPeople] = useState([]);
const [groupId, setGroupId] = useState(searchParams.get("group_id"));
const navigate = useNavigate();
function handleChange(value) {
navigate(`?group_id=${value}`);
}
useEffect(() => {
const fetchMainApi = () => {
setIsLoading(true)
axios.get(`${backend_url}/api/v1/metadata?id=${id}`)
.then(function(response) {
if(response.data.content.apiId !== 'undefined') {
axios.get("API_URL")
.then(function (response) {
setAdditionalData(response.data);
})
.catch(function (error) {
setAdditionalError(error);
})
}
if(itemData && (itemData.apiId !== 'null' || 'undefined')) {
axios.get("API_URL")
.then(function(response) {
setTrailer(response.data)
})
.catch(function(error) {
setTrailerError(error)
})
}
if(type === "cat" && itemData.children) {
setGroup(itemData.children)
}
if(type === "cat" && itemData.children)
axios.get("API_URL" + groupId)
.then(function (response) {
setPeople(response.data.content.children);
})
.catch(function (error) {
console.log(error);
});
setItemData(response.data.content)
})
.catch(function(error) {
setItemError(error)
})
setIsLoading(false)
}
fetchMainApi()
}, [backend_url,id,type,itemData.apiId,itemData.api])
return (
<>
<Form.Select onChange={event => handleChange(event.target.value)} aria-label="Default select example">
<option>Group All</option>
{cluster.map((person, index) => (
<option key={guid()} value={group.id}>{group.name}</option>
))}
</Form.Select>
<People people={people}/>
</>
);
};
export default App;
Here is the People component
const People = ({people}) => {
return (
<Row className="m-2 pt-2">
<h2 className="color-white">People</h2>
{people && people.length > 0 && (people.map((people, index) => (
<Col key={index} className="p-lg-4 p-sm-3" xs={12} sm={6} md={4} lg={3} xl={3}>
....
</Col>
)))}
{ (!people || people.length === 0) && (<h5 className="color-white">No Persons Found</h5>) }
</Row>
);
};
export default People;
Working
The select menu updates the query param and then the value of param is taken inside useEffect hook when then provides the data.
Every thing works well but the problem is to update the data inside the component i need to refresh the page when then works as expected.
Is there a way to change or update only the people component without a page refresh.

react js infinite scroll without third party library

Hi I'm new to react js and would like to implement infinite scroll without any help of third party/library. I achieved the infinite scroll, but there is problem. The problem is that for example my initial search is ant man movie, and then I try to search new movie let say the hulk. It doesn't re-render, but instead it continue/goes under ant man movies. What I want to achieve is to only shows the movie I search. Hopefully my question and problem is understandable.
Below is my code:
import React, { useEffect, useState, useRef, useCallback } from "react";
import axios from "axios";
const my_key = process.env.REACT_APP_MY_KEY;
export const InfiniteScroll = () => {
const [movies, setMovies] = useState([]);
const [movieName, setMovieName] = useState("ant man");
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
let cancel;
axios({
method: "GET",
url: "http://www.omdbapi.com/",
params: { apikey: `${my_key}`, s: movieName, page: page },
cancelToken: new axios.CancelToken((c) => (cancel = c)),
})
.then(({ data }) => {
if (data.Response === "True") {
setMovies((prev) => [...prev, ...data.Search]);
setHasMore(data.Search.length > 0);
} else {
setMovies((prev) => prev);
}
})
.catch((err) => err);
return () => cancel();
}, [movieName, page]);
const myObserver = useRef();
const myRef = useCallback(
(node) => {
if (myObserver.current) myObserver.current.disconnect();
myObserver.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPage((prev) => prev + 1);
}
});
if (node) myObserver.current.observe(node);
},
[hasMore]
);
const onSubmit = (e) => {
e.preventDefault();
setMovieName(search);
setSearch("");
setPage(1);
};
return (
<>
<h1>Searh Movie</h1>
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="title"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<input
type="submit"
value="submit"
disabled={search === "" ? true : false}
/>
</form>
{movies === "False" ? (
<div>
<h1>No Data</h1>
</div>
) : (
movies.map(({ imdbID, Title, Year, Poster }, i) => (
<div key={imdbID + i} ref={myRef}>
<img src={Poster} alt={imdbID} />
<h2>{Title}</h2>
<h3>{Year}</h3>
</div>
))
)}
</>
);
};
After the most bottom of ant man movie, comes the hulk movie. It should be re render and only shows hulk movie.
issue solved by seting setMovies([]) empty after search new movies

How to show search result not found message

import React, { useState, useEffect } from 'react'
import axios from 'axios'
function DataApi({ searchTerm }) {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState()
useEffect(() => {
axios
.get('https://jsonplaceholder.typicode.com/posts')
.then(res => {
setUsers(res.data);
console.log(res.data);
setLoading(true);
})
.catch(error => {
console.log(error);
setError('Error retrieving data');
});
}, []);
return (
<div>
<div>
{
!loading ?
<h1>...Loading</h1>
:
users.length > 0 && users.filter((item) =>
(searchTerm === '') ? item :
(item.title.toLowerCase().includes(searchTerm.toLocaleLowerCase())) ? item :
// <h1>search result not found</h1>
null
).map((item) => {
return (
<ul key={item.id}>
<li>Name: {item.id}</li>
<li>Title: {item.title}</li>
<li>Body: {item.body}</li>
</ul>
)}
)
}
{
error ? <h1>{error}</h1> : null
}
</div>
</div>
)
}
export default DataApi;
I have made a search field in which user can search the name of the person. If user does not get the searched name then there should be a message come that search result not found. I tried to implement it using if-else (ternary operator) & put the message into else part but it is not working. When I put null instead of search result not found then it works perfectly but I am not able to show the message then. But if I put search result not found instead of null then nothing works, not even filter functionality. Can you guys please help me? Thank you in advancve.
You can simply check the length of user and move the filter method to the useEffect and show a message
import React, { useState, useEffect } from "react";
import axios from "axios";
function DataApi({ searchTerm }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [searchTermTest, setsearchTermTest] = useState();
function handleChange(event) {
setsearchTermTest(event.target.value);
}
useEffect(() => {
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((res) => {
const data = res.data;
const filteredData = data.filter((dat) =>
dat.title.includes(searchTermTest === undefined ? "" : searchTermTest)
);
setUsers(filteredData);
setLoading(true);
})
.catch((error) => {
console.log("errr", error);
setError("Error retrieving data");
});
}, [searchTermTest]);
return (
<div>
<input type="text" onChange={handleChange} />
<div>
{!loading ? (
<h1>...Loading</h1>
) : (
users.length > 0 &&
users.map((item) => {
return (
<ul key={item.id}>
<li>Name: {item.id}</li>
<li>Title: {item.title}</li>
<li>Body: {item.body}</li>
</ul>
);
})
)}
{users.length === 0 && loading ? <h1>search result not found</h1> : ""}
{error ? <h1>{error}</h1> : null}
</div>
</div>
);
}
export default DataApi;
{users.length === 0 && loading ? <h1>search result not found</h1> : ""}
I have made it in codesandbox
Codesandbox link here
In Array.filter() method you need to return true/false value, that's how it works.
Modified the code and added the renderUser function to take care of user data filter.
DataApi function
function DataApi({ searchTerm }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [searchTermTest, setsearchTermTest] = useState();
function handleChange(event) {
setsearchTermTest(event.target.value);
}
useEffect(() => {
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((res) => {
const data = res.data;
const filteredData = data.filter((dat) =>
dat.title.includes(searchTermTest === undefined ? "" : searchTermTest)
);
setUsers(filteredData);
setLoading(true);
})
.catch((error) => {
console.log("errr", error);
setError("Error retrieving data");
});
}, [searchTermTest]);
return (
<div>
<input type="text" onChange={handleChange} />
<div>
{!loading ? (
<h1>...Loading</h1>
) : (
users.length > 0 && renderUsers(users, searchTerm) // updated here...
)}
{error ? <h1>{error}</h1> : null}
</div>
</div>
);
}
renderUsers function
const renderUsers = (users, searchTerm) => {
const filteredUsers = users.filter((item) => {
console.log(item.title);
return searchTerm === ""
? true
: item.title.toLowerCase().includes(searchTerm.toLocaleLowerCase())
? true
: false;
});
return filteredUsers.length > 0 ? (
filteredUsers.map((item) => {
return (
<ul key={item.id}>
<li>Name: {item.id}</li>
<li>Title: {item.title}</li>
<li>Body: {item.body}</li>
</ul>
);
})
) : (
<h1>search result not found</h1>
);
};
export default DataApi;

How to display posts on the current page from the API

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} />
</>
);
};

Categories