paginating requests to an API, using HackerNews API - reactjs

*** The question is quite simple, I just wrote a lot to be specific. ***
I've been looking online for a few hours, and I can't seem to find answer. Most pagination is about after you have received the data from the API call, or for backend node.js built with it's own server.
My issue, I have an API request that returns an array of 500 ID's. Then a second multi API call, looping through each ID making a promise API call. I use the Promise.all method.
It takes 2-3 minutes to complete this request.
Currently, I made a quick filter to get the first ten results, so it'll display and I can render the data to work on other things like the render component and styling.
My question, I'd like to be able to paginate the data while API calls are still being made.
Basically, Promise.all send an array of 10 id's (ten API calls), get continually. But after the first set of ten, I'd like to start receiving the data to render.
Right now, I can only get ten with my filter method. Or wait 2-3 min for all 500 to render.
Here is my request.js file, (it's part of my App component, I just separated it for clarity).
import axios from 'axios';
import BottleNeck from 'bottleneck'
const limiter = new BottleNeck({
maxConcurrent: 1,
minTime: 333
})
export const Request = (setResults, searchBarType, setLoading) => {
const searchBar = type => {
const obj = {
'new': 'newstories',
'past': '',
'comments': 'user',
'ask': 'askstories',
'show': 'showstories',
'jobs': 'jobstories',
'top': 'topstories',
'best': 'beststories',
'user': 'user'
}
return obj[type] ? obj[type] : obj['new'];
}
let type = searchBar(searchBarType)
const getData = () => {
const options = type
const API_URL = `https://hacker-news.firebaseio.com/v0/${options}.json?print=pretty`;
return new Promise((resolve, reject) => {
return resolve(axios.get(API_URL))
})
}
const getIdFromData = (dataId) => {
const API_URL = `https://hacker-news.firebaseio.com/v0/item/${dataId}.json?print=pretty`;
return new Promise((resolve, reject) => {
return resolve(axios.get(API_URL))
})
}
const runAsyncFunctions = async () => {
setLoading(true)
const {data} = await getData()
let firstTen = data.filter((d,i) => i < 10);
Promise.all(
firstTen.map(async (d) => {
const {data} = await limiter.schedule(() => getIdFromData(d))
console.log(data)
return data;
})
)
.then((newresults) => setResults((results) => [...results, ...newresults]))
setLoading(false)
// make conditional: check if searchBar type has changed, then clear array of results first
}
runAsyncFunctions()
}
and helps, here's my App.js file
import React, { useState, useEffect } from 'react';
import './App.css';
import { SearchBar } from './search-bar';
import { Results } from './results';
import { Request } from '../helper/request'
import { Pagination } from './pagination';
function App() {
const [results, setResults] = useState([]);
const [searchBarType, setsearchBarType] = useState('news');
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [resultsPerPage] = useState(3);
// Select search bar button type
const handleClick = (e) => {
const serachBarButtonId = e.target.id;
console.log(serachBarButtonId)
setsearchBarType(serachBarButtonId)
}
// API calls
useEffect(() => {
Request(setResults, searchBarType, setLoading)
}, [searchBarType])
// Get current results
const indexOfLastResult = currentPage * resultsPerPage;
const indexOfFirstResult = indexOfLastResult - resultsPerPage;
const currentResults = results.slice(indexOfFirstResult, indexOfLastResult);
// Change page
const paginate = (pageNumber) => setCurrentPage(pageNumber);
return (
<div className="App">
<SearchBar handleClick={handleClick} />
<Results results={currentResults} loading={loading} />
<Pagination resultsPerPage={resultsPerPage} totalResults={results.length} paginate={paginate} />
</div>
);
}
export default App;
I hope it's generic looking enough to follow guide lines. Please ask me anything to help clarify. I've spent 8-10 hours searching and attempting to solve this...

You can continue with your filter, but you have to do some changes, for totalResults props of the component Pagination you have to set 500 rows so the user can select the page he wants because if you set 10 rows, the pages a user can select are 1,2,3,4, but we don't need that we need to put all pages 1 to 34 pages because we have 500 ids. The second point, we need to fetch data from the server by page with a page size equal to 3 we need to pass to Request startIndex and lastIndex to Request.
Request.js
import axios from 'axios';
import BottleNeck from 'bottleneck'
const limiter = new BottleNeck({
maxConcurrent: 1,
minTime: 333
})
export const Request = (setResults, searchBarType, setLoading, startIndex, lastIndex) => {
const searchBar = type => {
const obj = {
'new': 'newstories',
'past': '',
'comments': 'user',
'ask': 'askstories',
'show': 'showstories',
'jobs': 'jobstories',
'top': 'topstories',
'best': 'beststories',
'user': 'user'
}
return obj[type] ? obj[type] : obj['new'];
}
let type = searchBar(searchBarType)
const getData = () => {
const options = type
const API_URL = `https://hacker-news.firebaseio.com/v0/${options}.json?print=pretty`;
return new Promise((resolve, reject) => {
return resolve(axios.get(API_URL))
})
}
const getIdFromData = (dataId) => {
const API_URL = `https://hacker-news.firebaseio.com/v0/item/${dataId}.json?print=pretty`;
return new Promise((resolve, reject) => {
return resolve(axios.get(API_URL))
})
}
const runAsyncFunctions = async () => {
setLoading(true)
const {data} = await getData()
let ids = data.slice(firstIndex, lastIndex+1) // we select our ids by index
Promise.all(
ids.map(async (d) => {
const {data} = await limiter.schedule(() => getIdFromData(d))
console.log(data)
return data;
})
)
.then((newresults) => setResults((results) => [...results, ...newresults]))
setLoading(false)
// make conditional: check if searchBar type has changed, then clear array of results first
}
runAsyncFunctions()
}
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import { SearchBar } from './search-bar';
import { Results } from './results';
import { Request } from '../helper/request'
import { Pagination } from './pagination';
function App() {
const [results, setResults] = useState([]);
const [searchBarType, setsearchBarType] = useState('news');
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [resultsPerPage] = useState(3);
// Select search bar button type
const handleClick = (e) => {
const serachBarButtonId = e.target.id;
console.log(serachBarButtonId)
setsearchBarType(serachBarButtonId)
}
// API calls
useEffect(() => {
Request(setResults, searchBarType, setLoading, 0, 2) //we fetch the first 3 articles
}, [searchBarType])
// Change page
const paginate = (pageNumber) => {
// Get current results
const indexOfLastResult = currentPage * resultsPerPage;
const indexOfFirstPost = indexOfLastResult - resultsPerPage;
Request(setResults, searchBarType, setLoading, indexOfFirstPost , indexOfLastResult) //we fetch the 3 articles of selected page
setCurrentPage(pageNumber);
}
return (
<div className="App">
<SearchBar handleClick={handleClick} />
<Results results={results} loading={loading} />
<Pagination resultsPerPage={resultsPerPage} totalResults={500} paginate={paginate} />
</div>
);
}
export default App;

Related

How to automatically refresh getstream.io FlatFeed after a new post using reactjs?

I would like to understand how can I auto-update the feed after submitting the form through the StatusUpdateForm component. At the moment I have to refresh the page to see the changes.
In general, my task is to differentiate feeds based on the user's location, I requested extended permissions from support so that different users can post to one feed, and therefore I use the modified doFeedRequest parameters of the FlatFeed component to show the feed without being tied to the current user and it works.
I do not use notification, I want the posted messages to appear immediately in the feed.
If I wrote my own custom feed (FeedCustom) component to display data, it would work fine, but how do I make it work with FlatFeed of getstream.io? Any help would be greatly appreciated.
import React, { useEffect, useState } from 'react';
import { StreamApp, FlatFeed, StatusUpdateForm } from 'react-activity-feed';
import 'react-activity-feed/dist/index.css';
// import FeedCustom from './FeedCustom';
const STREAM_API_KEY = 'XXXXXXXXXXXXXXXX';
const STREAM_APP_ID = 'XXXXX';
const App = () => {
const [userToken, setUserToken] = useState(null);
const [loading, setLoading] = useState(true);
const [locationId, setLocationId] = useState(null);
const [data, setData] = useState([]);
const callApi = async () => {
const response = await fetch('https://localhost:8080/user-token')
const userResponse = await response.json();
return userResponse;
};
useEffect(() => {
callApi()
.then(response => {
const resp = JSON.parse(response.body);
setLoading(false);
setUserToken(resp.userToken);
setLocationId(resp.locationId);
})
.catch(e => alert(e));
}, []);
const customDoFeedRequest = (client, feedGroup = 'timeline', userId = locationId, options) => {
const feed = client.feed(feedGroup, userId);
const feedPromise = feed.get(options);
feedPromise.then((res) => {
setData((data) => res.results);
});
return feedPromise;
}
return loading ? (
<div>.... Loading ....</div>
) : (
<StreamApp
apiKey={STREAM_API_KEY}
appId={STREAM_APP_ID}
token={userToken}
>
{/* <FeedCustom dataFeed={ data } /> */}
<FlatFeed doFeedRequest={customDoFeedRequest} />
<StatusUpdateForm
userId={locationId}
feedGroup={'timeline'}
onSuccess={(post) => setData((data) => [...data, post])}
/>
</StreamApp>
)
};
export default App;
My backend https://localhost:8080/user-token returns an object kind of:
{
userToken: 'XXXXXXX'
locationId: 'XXXXXXX'
}

useState in context provider loses data when a child components gets removed

So I am using a context provider to give the base data to my app for example: [{id: "a", name: "a"}].
Now I have a component that required the data portion of this object, I check if this data property is not yet there, I get it from my api, fill it and then it should not have to recall the api to get it again.
My provider:
import { createContext, useState, useCallback, useMemo, useContext } from "react";
import axios from "axios";
export const StockContext = createContext();
export const useStock = () => useContext(StockContext);
export const StockProvider = ({ children }) => {
const [stocks, setStocks] = useState();
const getDataFromStock = useCallback(
async ({ id }) => {
let method = "GET";
let url = `${config.base_url}data/${id}`;
try {
const { data: response } = await axios({ method, url });
const { succes, data, error } = response;
let updated = stocks;
updated.find((s) => s.id === id).data = data;
console.log("set stocks", updated);
setStocks(updated);
}
return succes;
}
},
[stocks]
);
const value = useMemo(
() => ({
getDataFromStock,
stocks,
}),
[getDataFromStock, stocks]
);
return <StockContext.Provider value={value}>{children}</StockContext.Provider>;
};
Note: I removed some error handling for simplicity sake.
After the function getDataFromStock with the id is called. The stocks object should look like this: [{id: "a", name: "a", data: [{id: "c", ...}]}].
Now I have my component to show the details (data) from this object. It first checks if it is not already in the 'stocks' object and if not gets it.
export default function StockDetailPage() {
const { id } = useParams();
const [currentStock, setCurrentStock] = useState({});
const { stocks, getDataFromStock, testStocks } = useContext(StockContext);
// TODO find why data keep disappearing
useEffect(() => {
const getData = async () => {
if (!stocks.find((s) => s.id === id).data) {
console.log("getting");
await getDataFromStock({ id });
// TODO Indication on loading?
} else {
console.log("not getting");
}
};
getData();
}, [getDataFromStock, id, stocks]);
return (
<div>
Stock: {currentStock?.owner?.username}
{stocks
?.find((s) => s.id === id)
.data?.map((p) => (
<ProductPreview key={p.product_id} {...p} />
))}
</div>
);
}
Now if I look at my react debugger, I see the state of the stocks has this data object, but once I navigate away from the details, this property seems to disappear. Now how could I implement this in the right way or fix the problem?
Kind regards

How to update a function in react when firing off onChange event

I have a function that filters the customers based on their levels (intermediate, beginner ), I'm passing this function through a component that has React select to filter my Data(async)
The filter is working only when I filter the first time but when I choose another value to filter it gave me a blank page?
I tried useEffect to keep it updated but it not working
Do you have any suggestions?
//APP.js
import React,{useState, useEffect} from "react";
import YogaCourses from "./components/YogaCourses/YogaCourses";
import Loading from "./components/IsLoading/Loading";
import LevelsFilter from './components/LevelsFilter/LevelsFilter';
//API to fetch the data
const url = 'https://gist.githubusercontent.com/Tayarthouail/8fb14fe117fdd718ceabd6ee05ed4525/raw/8c86c4bb89fc51667ba0578b2dcba14a0b21f08c/Yoga-courses-api.json';
function App() {
//states
const [yogaCourses, setYogaCourses] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [levels, setLevels] = useState([]);
//Filter by Levels
const filterLevels = (level) => {
const getLevels = yogaCourses.filter((singleLevel)=> singleLevel.level === level.value);
setYogaCourses(getLevels);
}
//Function to fetch the data from the API
const GetCourses = async () => {
const response = await axios.get(url)
const {data} = response;
return data;
}
//UseEffect to run the function on every render
useEffect(()=> {
const GetCoursesYoga = async () => {
const result = await GetCourses();
setYogaCourses(result);
console.log(result);
setLevels(Array.from(new Set(result.map((result)=> result.level))));
}
GetCoursesYoga();
}, []);
//check if the we got response
useEffect(()=> {
if(yogaCourses.length > 0) {
setIsLoading(false);
}
}, [yogaCourses])
if(isLoading) {
return (
<Loading/>
)
}
else {
return (
<main>
<div className="title">
<h2>YOUR PRACTICE REIMAGINED</h2>
</div>
<LevelsFilter levels={levels} filterLevels={filterLevels}/>
<YogaCourses yogaCourses= {yogaCourses}/>
</main>
);
}
}
export default App;
//LevelsFilter component
import React from 'react';
import Select from 'react-select';
import './LevelsFilter.css';
const LevelsFilter = ({levels, filterLevels}) => {
const option = levels.map((level)=> ({value : level, label: level}));
return (
<div>
<Select
options ={option}
className="select-option"
placeholder={"Type..."}
onChange={filterLevels}
/>
</div>
)
}
export default LevelsFilter;
Issue
You are replacing your state with the filtered data and subsequent filtering filters from there, so you only ever reduce your data.
Solution
I suggest storing an active filter state (i.e. level) and do the filtering inline when rendering so you skip the issue of stale/bad state.
function App() {
//states
const [yogaCourses, setYogaCourses] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [levels, setLevels] = useState([]);
const [level, setLevel] = useState('');
const levelChangeHandler = ({ value }) => {
setLevel(value);
}
//Filter by Levels
const filterLevels = (level) => {
return yogaCourses.filter(
(singleLevel) => level ? singleLevel.level === level : true
);
}
...
if(isLoading) {
return (
<Loading/>
)
}
else {
return (
<main>
<div className="title">
<h2>YOUR PRACTICE REIMAGINED</h2>
</div>
<LevelsFilter levels={levels} onChange={levelChangeHandler}/>
<YogaCourses yogaCourses={filterLevels(level)}/>
</main>
);
}
}
LevelsFilter
import React from 'react';
import Select from 'react-select';
import './LevelsFilter.css';
const LevelsFilter = ({ levels, onChange }) => {
const option = levels.map((level)=> ({value : level, label: level}));
return (
<div>
<Select
options ={option}
className="select-option"
placeholder={"Type..."}
onChange={onChange}
/>
</div>
)
}
You need a copy state.
Your code is replacing the data source with filtered data. When you first time selects the option then your state replaces it with that one and you no longer have previous state data. On the second time, you don't have data that why it's blank on-screen.
Just copy and replace the below app.js code:
import React,{useState, useEffect} from "react";
import YogaCourses from "./components/YogaCourses/YogaCourses";
import Loading from "./components/IsLoading/Loading";
import LevelsFilter from './components/LevelsFilter/LevelsFilter';
//API to fetch the data
const url = 'https://gist.githubusercontent.com/Tayarthouail/8fb14fe117fdd718ceabd6ee05ed4525/raw/8c86c4bb89fc51667ba0578b2dcba14a0b21f08c/Yoga-courses-api.json';
function App() {
//states
const [yogaCourses, setYogaCourses] = useState([]);
const [filteredYogaCourses, setFillteredYogaCourses] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [levels, setLevels] = useState([]);
//Filter by Levels
const filterLevels = (level) => {
const getLevels = yogaCourses.filter((singleLevel)=> singleLevel.level === level.value);
setFillteredYogaCourses(getLevels);
}
//Function to fetch the data from the API
const GetCourses = async () => {
const response = await axios.get(url)
const {data} = response;
return data;
}
//UseEffect to run the function on every render
useEffect(()=> {
const GetCoursesYoga = async () => {
const result = await GetCourses();
setYogaCourses(result);
setLevels(Array.from(new Set(result.map((result)=> result.level))));
}
GetCoursesYoga();
}, []);
//check if the we got response
useEffect(()=> {
if(yogaCourses.length > 0) {
setIsLoading(false);
}
}, [yogaCourses])
if(isLoading) {
return (
<Loading/>
)
}
else {
return (
<main>
<div className="title">
<h2>YOUR PRACTICE REIMAGINED</h2>
</div>
<LevelsFilter levels={levels} filterLevels={filterLevels}/>
<YogaCourses yogaCourses= {filteredYogaCourses}/>
</main>
);
}
}
export default App;
I hope it will work, if not then please debug it because I haven't tested it but the idea will be same. :)

Filtering data using react hooks

Hey guys I've been learning react for a few weeks now so please be easy on me =). When I was using dummy data, the filter function worked and showed the correct products in the category. I built the back end api using django and now my filter function doesn't work anymore. It does filter but the data totally disappears after pressing the different filter buttons. Can anyone help?
import React, { useState, useEffect } from "react";
import axios from "axios";
import ButtonList from "../components/ButtonList";
import ProductList from "../components/ProductList";
const ProductPage = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProduct = async () => {
const { data } = await axios.get("/api/products/");
setProducts(data);
};
fetchProduct();
}, []);
const filter = (button) => {
if (button === "All") {
setProducts(products);
return;
}
const filteredData = products.filter(
(products) => products.category === button
);
setProducts(filteredData);
};
return (
<div>
<ButtonList onClickFilter={filter} />
<ProductList product={products} />
</div>
);
};
export default ProductPage;
You are losing the original list of products as your setting filtered data in it. So, currently there is no way to get the original products list back.
To fix it, you can set search in a state and use that to filter the products. This way original data is always present in products but filtered data is used for rendering the list:
const ProductPage = () => {
const [products, setProducts] = useState([])
const [search, setSearch] = useState('ALL') // New State for search
// ...
const filter = (button) => {
setSearch(button)
}
return (
<div>
<ButtonList onClickFilter={filter} />
<ProductList
product={products.filter((p) => search === 'ALL' || p.category === search)}
/>
</div>
)
}
Right now, after filtering, you're losing the full products array information permanently, since it only exists in the stateful products variable that setProducts will essentially overwrite. Add another state, one which contains the full products, and filter off of it instead.
const ProductPage = () => {
const [fullProducts, setFullProducts] = useState([]);
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProduct = async () => {
const { data } = await axios.get("/api/products/");
setFullProducts(data);
};
fetchProduct();
}, []);
const filter = (button) => {
if (button === "All") {
setProducts(fullProducts);
return;
}
const filteredData = fullProducts.filter(
(product) => product.category === button
);
setProducts(filteredData);
};
return (
<div>
<ButtonList onClickFilter={filter} />
<ProductList product={products} />
</div>
);
};

setState react hook for array is not saving prior array elements

I have an API request that uses aysnc and await, grabs the data, then makes a second request with Promise.all, which makes multiple API requests with the id's. That part works out fine.
However, when I go to save the data inside a React hook called, "setItem", it only saves that one and over writes the others. I have a spread operator inside the setItem()
setItems(...items, data)
data being the response from the API request.
My API request is in the top layer of my react app, so I pulled it out into it's own little helper file, that's why "items" and "setItems", are arguments passed through.
import axios from 'axios';
import BottleNeck from 'bottleneck'
const limiter = new BottleNeck({
maxConcurrent: 1,
minTime: 333
})
export const Request = (items, setItems) => {
const getData = () => {
const options = 'newstories'
const API_URL = `https://hacker-news.firebaseio.com/v0/${options}.json?print=pretty`;
return new Promise((resolve, reject) => {
return resolve(axios.get(API_URL))
})
}
const getIdFromData = (dataId) => {
const API_URL = `https://hacker-news.firebaseio.com/v0/item/${dataId}.json?print=pretty`;
return new Promise((resolve, reject) => {
return resolve(axios.get(API_URL))
})
}
const runAsyncFunctions = async () => {
const {data} = await getData()
Promise.all(
data.map(async (d) => {
const {data} = await limiter.schedule(() => getIdFromData(d))
//****************** issue here ************************//
setItems([...items, data]);
})
)
}
runAsyncFunctions()
}
just in case you want to see the app.js file for reference
import React, { useState, useEffect } from 'react';
import './App.css';
import { SearchBar } from './search-bar';
import { Results } from './results';
import { Request } from './helper/request'
function App() {
const [input, setInput] = useState('');
const [items, setItems] = useState([]);
const handleChange = val => {
setInput(val)
}
// console.log(input)
// console.log(results)
// API calls
// call useEffect here, calls Request(), put results in useEffect
useEffect(() => {
Request(items, setItems)
}, [])
return (
<div className="App">
<SearchBar handleChange={handleChange}/>
<Results items={items} />
</div>
);
}
export default App;
At your Promise.all return each data, after you can chain with then that passes an array with all resolved data. This way you only need to call it once setItems:
Promise.all(
data.map(async (d) => {
const { data } = await limiter.schedule(() => getIdFromData(d));
return data;
})
).then((dataResults) => setItems((results) => [...results, ...dataResults]));

Resources