I'm trying to implement intersection observer in react functional component.
import React, { useEffect, useRef, useState } from "react";
import { getData } from "./InfiniteClient";
export default function InfiniteScroll() {
const [data, setData] = useState([]);
const [pageCount, setPageCount] = useState(1);
const sentinal = useRef();
useEffect(() => {
const observer = new IntersectionObserver(intersectionCallback);
observer.observe(sentinal.current, { threshold: 1 });
getData(setData, data, pageCount, setPageCount);
}, []);
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => pageCount + 1);
getData(setData, data, pageCount, setPageCount);
}
};
return (
<section>
{data &&
data.map((photos, index) => {
return <img alt="" src={photos.url} key={index} />;
})}
<div className="sentinal" ref={sentinal}>
Hello
</div>
</section>
);
}
When I'm consoling prevCount above or prevData in the below function is coming as 1 and [] which is the default state.
function getData(setData, prevData, pageCount, setPageCount) {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${pageCount}&limit=10`
)
.then((val) => val.json())
.then((val) => {
console.log("prevD", prevData,val,pageCount);
if (!prevData.length) setData([...val]);
else {
console.log("Here", pageCount, prevData, "ddd", val);
setData([...prevData, ...val]);
}
}).catch((e)=>{
console.log("Error",e);
});
}
export { getData };
The code is not entering the catch block. I have also tried setPageCount(pageCount=> pageCount+ 1); and setPageCount(pageCount+ 1); gives same result. What am I doing wrong?
Code Sandbox
Edit: I converted the above code to class based component and it is working fine. I'm more curious on how hooks based approach is resets the states.
import React, { Component } from "react";
export default class InfiniteClass extends Component {
constructor() {
super();
this.state = {
pageCount: 1,
photos: []
};
}
getData = () => {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${this.state.pageCount}&limit=3`
)
.then((val) => val.json())
.then((val) => {
this.setState({
photos: [...this.state.photos, ...val],
pageCount: this.state.pageCount + 1
});
})
.catch((e) => {
console.log("Error", e);
});
};
componentDidMount() {
console.log(this.sentinal);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.getData();
}
});
observer.observe(this.sentinal, { threshold: 1 });
}
render() {
return (
<section>
{this.state &&
this.state.photos.length &&
this.state.photos.map((photo, index) => {
return <img alt="" src={photo.url} key={index} />;
})}
<div
className="sentinal"
ref={(sentinal) => (this.sentinal = sentinal)}
>
Hello
</div>
</section>
);
}
}
Edit 2 : I tried consoling pageCount at two places one above IntersectionCallback and one inside. The value inside is not changing meaning it is storing its own variables.
useState in react takes either argument or function. So, I did something hackish. It is working but I'm looking for a better aproach.
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => {
setData(d=>{
getData(setData, d, pageCount);
return d;
})
return pageCount + 1;
});
}
};
Related
I'm new to React, and I would like to know if someone can help me?
I'm trying to use useEffect and State to manipulate the API.
But the cards are not rendering.
Sometimes all the cards are rendering, other times not.. and they always come on a different order even after sorting them :( Can you help me?
App.js
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from "react";
import PlayerList from "./PlayerList";
import axios from "axios";
function App() {
const Team = [
...
];
const Team2 = [
...
];
const Team3 = [
...
];
const teamForLoop = [Team, Team2, Team3];
const [allPlayers, setAllPlayers] = useState([]);
const [team, setTeam] = useState([]);
const [allTeams] = useState(teamForLoop);
const [loading, setLoading] = useState(true);
useEffect(() => {
const playerInfo = async () => {
setLoading(true);
allTeams.map(async (teamArray) => {
setTeam([]);
teamArray.map(async (player) => {
let playerName = player.split(" ");
const result = await axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${playerName[0]}%20${playerName[1]}`
);
if (result.data.player === null) {
setTeam((state) => {
return [...state];
});
} else {
setTeam((state) => {
return [...state, result.data.player[0]];
});
}
});
setAllPlayers(team);
});
setLoading(false);
};
playerInfo();
}, [allTeams]);
if (loading) return "...Loading...";
return (
<>
<PlayerList allPlayers={allPlayers} />
</>
);
}
export default App;
PlayerList.js
import React from "react";
export default function PlayerList({ allPlayers }) {
const myData = []
.concat(allPlayers)
.sort((a, b) => (a.strNumber > b.strNumber ? 1 : -1))
.sort((a, b) => (a.idTeam !== b.idTeam ? 1 : -1));
return (
<div>
{myData.map((player, index) => (
<div key={index}>
<div className="playerCard">
<img
className="playerImage"
src={player.strCutout}
alt={`${player.strPlayer}`}
/>
<h1 className="playerName">{player.strPlayer}</h1>
<h2 className="playerNumber">{player.strNumber}</h2>
</div>
</div>
))}
</div>
);
}
Codesandbox link:
"https://codesandbox.io/s/busy-orla-v872kt?file=/src/App.js"
I can make a successful call to getApiSuggestions with data returned. However I'm unable to assign this to my state.
As you can see my console output shows that the value for response has an array. However, when attempting to assign it to wikiResults:response the array remains empty.
note that this is a modification of react-search-autocomplete
Am I attempting to pass the variables incorrectly?
NarrativeSearch.js
import React, {useContext, useState, useEffect} from "react";
import './search.css'
import { ReactSearchAutocomplete } from 'react-search-autocomplete'
import { getApiSuggestions } from '../../requests/requests';
import {TextSearchContext} from "../../contexts/TextSearchContext"
import {SearchContext} from "../../contexts/SearchContext"
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const [wikiTitleResults, setWikiTitleResults] = useState({wikiResults:[]});
var cnJson = wikiTitleResults;
const items = wikiTitleResults.wikiResults;
const handleOnSearch = (string, results) => {
console.log("STRING: ", string)
getApiSuggestions(string).then(response => {
console.log("RESPONSE: ", response);
setWikiTitleResults({wikiResults:response}); //<---- This doesn't update the state
console.log("WikiTitle: ", wikiTitleResults.wikiResults);
console.log("Items: ", items);
})
}
const handleOnHover = (result) => {
// the item hovered
console.log(result)
}
const handleOnSelect = (item) => {
// the item selected
setTextFilterState({textFilter:item.name});
console.log(item)
}
const handleOnFocus = () => {
console.log('Focused')
}
const handleOnClear = () => {
setTextFilterState({textFilter:""});
}
const formatResult = (item) => {
return (
<>
<span style={{ display: 'block', textAlign: 'left' }}>id: {item.title}</span>
</>
)
}
return (
<div >
<div className="searchbar">
<ReactSearchAutocomplete
items={items}
onSearch={handleOnSearch}
onHover={handleOnHover}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onClear={handleOnClear}
styling={{ zIndex: 4 }} // To display it on top of the search box below
autoFocus
/>
</div>
</div>
)
}
export default Search
getApiSuggesetions
const getApiSuggestions = (title) => {
//console.log("URL Being called"+ urlSingleResult);
//console.log(title);
let result = urlMultiResult
.get(`${title}`)
.then((response) => {
console.log(Object.values(response.data.query.pages))
return Object.values(response.data.query.pages);
})
.catch((error) => {
return error;
console.log(error);
});
console.log(result);
return result;
};
I fixed this by including a useEffect and a context from the parent component.
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const {wikiTitleResults, setWikiTitleResults} = useContext(SearchContext);
var items = wikiTitleResults.wikiTitles;
useEffect(() => {
const fetchData = async () => {
const data = await getApiSuggestions(textFilterState.textFilter)
setWikiTitleResults({wikiTitles:data})
}
fetchData();
},
[textFilterState])
const handleOnSearch = (string, results) => {
setTextFilterState({textFilter:string});
}
I'm trying to simulate the 'see more' functionality to a blog.
It works as expected on the browser but when I simulate the behavior on react testing library it doesn't.
describe('when 12 blogs', () => {
describe('fetch more blogs', () => {
beforeEach(() => {
const twelveBlogs = generateBlogs(12);
const twoBlogs = generateBlogs(10);
Api.query.mockReturnValueOnce(twelveBlogs);
Api.query.mockReturnValueOnce(twoBlogs);
});
test('should fetch more blog posts when clicking on "See More" button', async () => {
render(
<MemoryRouter>
<Blog />
</MemoryRouter>
);
const seeMoreButton = await screen.findByRole('button', {
name: /See More/i,
});
fireEvent.click(seeMoreButton);
await waitFor(() => expect(Api.query).toHaveBeenCalledTimes(2));
await waitFor(
() =>
expect(screen.getAllByText(/NaN de undefined de NaN/)).toHaveLength(
15
)
);
});
});
});
And the implementation
import React from 'react';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import Api from '../../api/api';
import BlogPreview from '../../components/BlogPreview/BlogPreview';
import './Blog.css';
function Blog() {
const [blogPosts, setBlogPosts] = useState([]);
const pageSize = 12;
const category = ['document.type', 'blog'];
const orderings = '[my.blog.data desc]';
const [apiPage, setApiPage] = useState(1);
const [shouldFetchMoreBlogs, setShouldFetchMoreBlogs] = useState(true);
useEffect(() => {
async function fetchApi(options) {
return Api.query(category, options);
}
const options = { pageSize, page: apiPage, orderings };
fetchApi(options).then((response) => {
if (response?.length > 0) {
if (blogPosts.length !== 0) {
setBlogPosts([...blogPosts, response]);
} else {
setBlogPosts(response);
}
} else {
setShouldFetchMoreBlogs(false);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiPage]);
async function handleSeeMoreClick() {
setApiPage(apiPage + 1);
}
function renderBlogPosts() {
if (blogPosts.length > 0) {
return blogPosts.map((blog, index) => (
<Link to={{ pathname: `/blog/${blog.uid}`, ...blog }} key={index}>
<BlogPreview key={index} {...blog} />
</Link>
));
}
}
function renderSeeMoreButton() {
debugger;
if (blogPosts.length > 0) {
if (blogPosts?.length % 12 === 0 && shouldFetchMoreBlogs) {
return (
<div className="see-more-container">
<button className="see-more-button" onClick={handleSeeMoreClick}>
Veja Mais
</button>
</div>
);
}
}
}
return (
<section className="content blog">
<h1>BLOG</h1>
<div className="blog-posts">{renderBlogPosts()}</div>
{renderSeeMoreButton()}
</section>
);
}
export default Blog;
It fails 'cause it only finds the initial 12 blog posts, even though it shows that the api was called twice.
There's obviously some async issue here.
I've tried switching from fireEvent to userEvent, from waitFor to find*, but it still doesn't work.
Thanks
I have a form that takes its state from a react useState hook, that hooks default value I would like to come from a useTracker call, I am using pub sub in Meteor to do this. I get a error Cannot access '' before initialization I know it has something to do with the lead not being ready yet and returning undefined and the hook not being able to use that, at least I think so. But I am not sure how to solve that.
Here is my code thus far
import React, { useState } from "react";
import Dasboard from "./Dashboard";
import { Container } from "../styles/Main";
import { LeadsCollection } from "../../api/LeadsCollection";
import { LeadWalkin } from "../leads/LeadWalkin";
import { useTracker } from "meteor/react-meteor-data";
const Walkin = ({ params }) => {
const [email, setEmail] = useState(leads.email);
const handleSubmit = (e) => {
e.preventDefault();
if (!email) return;
Meteor.call("leads.update", email, function (error, result) {
console.log(result);
console.log(error);
});
setEmail("");
};
const { leads, isLoading } = useTracker(() => {
const noDataAvailable = { leads: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leads");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leads = LeadsCollection.findOne({ _id: params._id });
return { leads };
});
console.log(leads);
//console.log(params._id);
const deleteLead = ({ _id }) => {
Meteor.call("leads.remove", _id);
window.location.pathname = `/walkin`;
};
return (
<Container>
<Dasboard />
<main className="split">
<div>
<h1>Edit a lead below</h1>
</div>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<>
<LeadWalkin
key={params._id}
lead={leads}
onDeleteClick={deleteLead}
/>
<form className="lead-form" onSubmit={handleSubmit}>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Type to edit lead"
/>
<button type="submit">Edit Lead</button>
</form>
</>
)}
</main>
</Container>
);
};
export default Walkin;
It should work if you change the order of these two hooks, but it's probably better to break this into two components so that you can wait until your subscription is ready before you try to use leads.email as default value. It's not possible to branch out ('return loading`) in between hooks, because React doesn't like it when the number of hooks it finds in a component change in-between re-renderings.
const Walkin = ({ params }) => {
const { leads, isLoading } = useTracker(() => {
const noDataAvailable = { leads: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leads");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leads = LeadsCollection.findOne({ _id: params._id });
return { leads };
});
if (isLoading || !leads) {
return <div>loading..</div>;
} else {
return <SubWalkin params=params leads=leads />;
}
};
const SubWalkin = ({ params, leads }) => {
const [email, setEmail] = useState(leads.email);
...
};
I cannot figure out how to handle my function components calling my api repeatedly. I have two components which retrieve data, one of them calls the api twice. Once before the second component once after.
I am using a custom react hook and axios get method to retrieve the data. My two components are are nested. The first component when loads and fetches data. Inside this component is a child component which when renders it fetches data right before passing the first set of data as props to another child component. When it completes loading it reloads the first child component which again calls the api for data. I understand the function components reload on state change. I would be happy for it to not call the api a second time. Is there a way to check if it already has data and bypass the api call?
Custom hook to retrieve data
import React, { useState, useEffect, useReducer } from "react";
import axios from "axios";
const dataFetchReducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return { ...state, isLoading: true, hasErrored: false };
case "FETCH_SUCCESS":
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: action.payload
};
case "FETCH_FAILURE":
return {
...state,
isLoading: false,
hasErrored: true,
errorMessage: "Data Retrieve Failure"
};
case "REPLACE_DATA":
// The record passed (state.data) must have the attribute "id"
const newData = state.data.map(rec => {
return rec.id === action.replacerecord.id ? action.replacerecord : rec;
});
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: newData
};
default:
throw new Error();
}
};
const useAxiosFetch = (initialUrl, initialData) => {
const [url] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
hasErrored: false,
errorMessage: "",
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
let result = await axios.get(url);
if (!didCancel) {
dispatch({ type: "FETCH_SUCCESS", payload: result.data });
}
} catch (err) {
if (!didCancel) {
dispatch({ type: "FETCH_FAILURE" });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const updateDataRecord = record => {
dispatch({
type: "REPLACE_DATA",
replacerecord: record
});
};
return { ...state, updateDataRecord };
};
export default useAxiosFetch;
Main component which renders the "CompaniesDropdown" twice inside
CompaniesDropdown is one of three dropdowns within the ListFilterContainer component but the only one which calls the api more than once. The other two dropdowns load by selection of the CompaniesDropdown.
import React, { useMemo, useEffect, useContext } from "react";
import InvoiceList from "../src/Components/Lists/InvoiceList";
import useAxiosFetch from "../src/useAxiosFetch";
import { ConfigContext } from "./_app";
import ListFilterContainer from "../src/Components/Filters/InvoiceFilters";
// import "../css/ListView.css";
const Invoices = props => {
const context = useContext(ConfigContext);
useEffect(() => {
document.title = "Captive Billing :: Invoices";
});
const {
data,
isLoading,
hasErrored,
errorMessage,
updateDataRecord
} = useAxiosFetch("https://localhost:44394/Invoice/GetInvoices/false", []);
const newInvoicesList = useMemo(
() => data
// .filter(
// ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
// )
// .sort(function(a, b) {
// if (a.firstName < b.firstName) {
// return -1;
// }
// if (a.firstName > b.firstName) {
// return 1;
// }
// return 0;
// }),
// [speakingSaturday, speakingSunday, data]
);
const invoices = isLoading ? [] : newInvoicesList;
if (hasErrored)
return (
<div>
{errorMessage} "Make sure you have launched "npm run json-server"
</div>
);
if (isLoading) return <div>Loading...</div>;
const dataProps = {
data: invoices,
titlefield: "invoiceNumber",
titleHeader: "Invoice Number:",
childPathRoot: "invoiceDetail",
childIdField: "invoiceId",
childDataCollection: "invoiceData"
};
var divStyle = {
height: context.windowHeight - 100 + "px"
};
return (
<main>
<ListFilterContainer />
<section style={divStyle} id="invoices" className="card-container">
<InvoiceList data={dataProps} />
</section>
</main>
);
};
Invoices.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default Invoices;
Actual result is described above. My main concern is to not have the api calls more than once.
Here is some additional code to help. It is the filter control mentioned above. It, as you will notice really just contains dropdowns and a text box. The first dropdown is the one that calls the api twice. The second two are not visible until that one is selected.
import React, { useState, useMemo } from "react";
import CompaniesDropdown from "../Dropdowns/CompaniesDropdown";
import LocationsDropdown from "../Dropdowns/LocationsDropdown";
import AccountsDropdown from "../Dropdowns/AccountsDropdown";
import Search from "./SearchFilter/SearchFilter";
const InvoiceFilters = props => {
const [company, setCompany] = useState("");
const [location, setLocation] = useState(undefined);
const [account, setAccount] = useState(undefined);
const handleClientChange = clientValue => {
setCompany(clientValue);
};
const handleLocationsChange = locationValue => {
setLocation(locationValue);
};
const handleAccountsChange = AccountValue => {
setAccount(AccountValue);
};
return (
<section className="filter-container mb-3">
<div className="form-row">
<div className="col-auto">
<CompaniesDropdown change={e => handleClientChange(e)} />
</div>
<div className="col-auto">
<LocationsDropdown
selectedCompany={company}
change={e => handleLocationsChange(e)}
/>
</div>
<div className="col-auto">
<AccountsDropdown
selectedCompany={company}
change={e => handleAccountsChange(e)}
/>
</div>
<div className="col-auto">
<Search />
</div>
</div>
</section>
);
};
InvoiceFilters.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default InvoiceFilters;
Also the datalist
import React from "react";
import Link from "next/link";
import InvoiceListRecord from "./InvoiceListRecord";
const InvoiceList = props => {
let dataCollection = props.data.data;
return dataCollection.length == 0 ? "" : dataCollection.map((item, index) => {
return (
<section key={"item-" + index} className="card text-left mb-3">
<header className="card-header">
<span className="pr-1">{props.data.titleHeader}</span>
<Link
href={
"/" +
props.data.childPathRoot +
"?invoiceId=" +
item[props.data.childIdField]
}
as={
"/" +
props.data.childPathRoot +
"/" +
item[props.data.childIdField]
}
>
<a>{item[props.data.titlefield]}</a>
</Link>{" "}
</header>
<div className="card-body">
<div className="row">
<InvoiceListRecord
data={item}
childDataCollection={props.data.childDataCollection}
/>
</div>
</div>
</section>
);
});
};
InvoiceList.getInitialProps = async ({ req }) => {
console.log("Get Intitial Props works: Invoices Page!");
const isServer = !!req;
return { isServer };
};
export default InvoiceList;
and the list items component.
import React from "react";
const InvoiceListRecord = props => {
var invoiceData = JSON.parse(props.data[props.childDataCollection]);
return invoiceData.map((invKey, index) => {
return (
<div className="col-3 mb-1" key={"item-data-" + index}>
<strong>{invKey.MappedFieldName}</strong>
<br />
{invKey.Value}
</div>
);
});
};
export default InvoiceListRecord;
The API is not called more than once if the url is the same. It just gets the value from data variable. The api call is not made again, unless the url changes.
I created an example from your code, changing all the unknown components to div. I added a console.log in the useEffect of the useAxiosFetch hook. And to re-render the component, I added a button to increment the count.
You'll see that the console.log from the hook is printed only once, even though the component re-renders on every button click. The value just comes from the data variable from the hook and the api call is not made again and again.