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
Related
I've been struggling with this one for 2-3 days and hope someone can help. I am moving a blog project over from React to Next, and in one particular case a setState function isn't working.
The code below lives in the _app.tsx function at the top of my project. The editPost function is called from a button in a child component. The code pulls the selected blog post from the database then updates the state of postToEdit. This data is meant to be injected into an edit form via props-- and works fine in the React version of the blog.
In this case, the setState (setPostToEdit) function seems to do nothing. In the console.log function after setPostToEdit(newPostToEdit), you can see that the data has been pulled from Postgres correctly, but the state doesn't change.
In the deletePost and getPosts function in this same _app component, everything works fine. Weird! Any help sincerely appreciated, I'm new to both React and Next.
import '../styles/globals.css'
import React, { useState, useEffect } from 'react'
import type { AppProps } from 'next/app'
import Layout from '../components/Layout'
export default function App({ Component, pageProps }: AppProps) {
const initPostToEdit = {
post_id: '',
title: 'initial post title',
sub_title: '',
main_content: '',
post_url: 'initial URL',
page_title: '',
meta_description: '',
meta_keywords: ''
}
const [posts, setPosts] = useState([]);
const [postToEdit, setPostToEdit] = useState(initPostToEdit);
const [blogValues, setBlogValues] = useState(initPostToEdit);
const deletePost = async (id) => {
try {
await fetch(`http://localhost:5001/blog-edit/${id}`, {
method: "DELETE"
})
setPosts(posts.filter(post => post.post_id !== id))
} catch (error) {
console.error(error.message)
}
}
const editPost = async (id) => {
try {
const response = await fetch(`http://localhost:5001/blog-edit/${id}`, {
method: "GET"
})
const newPostToEdit = await response.json()
setPostToEdit(newPostToEdit)
console.log('postToEdit:', newPostToEdit[0], postToEdit)
window.location.assign("/admin/blog-edit");
} catch (error) {
console.error(error.message)
}
}
const getPosts = async () => {
try {
const response = await fetch("http://localhost:5001/blog-edit");
const jsonData = await response.json();
setPosts(jsonData);
} catch (error) {
console.error(error.message)
}
}
useEffect(() => { getPosts(); }, [])
return (
<div>
<Layout>
<Component
{...pageProps}
editPost={editPost}
postToEdit={postToEdit}
setPostToEdit={setPostToEdit}
blogValues={blogValues}
setBlogValues={setBlogValues}
posts={posts}
deletePost={deletePost}
/>
</Layout>
</div>
)
}
Edit: final solution at the bottom.
I am trying to build a simple web-app to display some data stored in firestore database, using React Table v7. I'm pretty new to React and javascript in general so forgive me if I use the wrong terminology.
At first I had put the function to fetch data inside App.jsx, passing it to state with setState within a useEffect hook, and it was working without an issue.
Then a colleague suggested that it is a good practice passing data to a component state instead of the app state, and that's where problems started to arise.
As of now, I cannot manage to populate the table. The header gets rendered but there's nothing else, and the only way I can make it show data is to make a small change in Table.jsx while npm start is running (such as adding or changing the output of any console.log) and saving the file. Only then, data is displayed.
I've been trying everything I could think of for about 2 days now (last thing I tried was wrapping Table.jsx into another component, but nothing changed).
I tried console.loging all the steps where data is involved to try and debug this, but I'm failing to understand where the problem is. Here's the output when the app loads first:
My code currently:
utility function to fetch data from Firestore
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
},
);
console.log('func output', dataArray);
return dataArray;
};
export default parseData;
Table.jsx
import { useTable } from 'react-table';
const Table = ({ columns, data }) => {
const tableInstance = useTable({ columns, data });
console.log('table component data received', data);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = tableInstance;
return (
// html boilerplate from https://react-table.tanstack.com/docs/quick-start#applying-the-table-instance-to-markup
);
};
export default Table;
TableContainer.jsx
import { useState, useEffect, useMemo } from 'react';
import parseData from '../utils';
import Table from './Table';
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
const getData = async () => {
const dataFromServer = await parseData(db);
setData(dataFromServer);
console.log('container-useEffect', dataFromServer);
};
getData();
}, [db]);
const columns = useMemo(() => [
{
Header: 'ID',
accessor: 'id',
},
// etc...
], []);
console.log('container data', data);
return (
<>
<Table columns={columns} data={data} />
</>
);
};
export default TableContainer;
App.jsx
import firebase from 'firebase/app';
import 'firebase/firestore';
import TableContainer from './components/TableContainer';
import Navbar from './components/Navbar';
// ############ INIT FIRESTORE DB
const firestoreCreds = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
};
if (!firebase.apps.length) {
firebase.initializeApp(firestoreCreds);
}
const db = firebase.firestore();
function App() {
return (
<div>
<Navbar title="This is madness" />
<div>
<TableContainer db={db} />
</div>
</div>
);
}
export default App;
Edit: in the end, using the suggestions below, this is the final solution I could come up with. I was annoyed by having to wrap the API call in an async func within useEffect, so this is what I did.
utility function to fetch data from Firestore
const parseData = (db) => {
const snapshot = db.collection('collection_name').get();
return snapshot.then(
(querySnapshot) => {
const dataArray = [];
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
return dataArray;
},
);
};
export default parseData;
TableContainer.jsx
Here I also added the flag didCancel within useEffect to avoid race conditions, according to this and this it seems to be a best practice.
// imports
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
let didCancel = false;
parseData(db)
.then((dataFromServer) => (!didCancel && setData(dataFromServer)));
return () => { didCancel = true; }
}, [db]);
// ...
In parseData function this line return dataArray; is execute before the snapshot is resolved. You need to change parseData and return a Promise and resolve when data is ready:
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
return new Promise(resolve => {
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
resolve(dataArray); //--> resolve when data is ready
},
);
})
};
In your TableContainer, you initialize data with an empty array. That's being sent along to Table until you finish getting data from your server. If you don't want that to happen, you should change your default (in useState) to something like false and explicitly handle that case (e.g. display "Please wait. Loading" if data === false).
The asynchronicity in the parseData function is plain wrong (and the implementation a smidge too complex...). Reimplement it something like this...
const parseData = async (db) => {
// Note `await` here.
const snapshot = await db.collection('collection-name').get();
return snapshot.map((doc) => ({ ...doc.data(), id: doc.id }));
};
export default parseData;
*** 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;
I'm trying to display data from firestore on the home page but i dont seem to get it working. I want to show all the data in the collection. the collection includes name, id, location etc...
import Firebase from "./lib/firebase";
import { useEffect, useState } from "react";
import { SnapshotViewIOS } from "react-native";
// export async function getRestaurants(restaurantsRetrieved) {
// var restaurantList = [];
// var snapshot = await firebase.firestore().collection("Restaurants").get();
// snapshot.forEach((doc) => {
// restaurantList.push(doc.data());
// });
// restauantsRetrieved(restaurantList);
// }
export default () => {
const [restaurantsList, setRestaurantsList] = useState([]); //Initialise restaurant list with setter
const [errorMessage, setErrorMessage] = useState("");
const getRestaurants = async () => {
try {
const list = [];
var snapshot = await Firebase.firestore().collection("Restaurants").get();
console.log("Here");
snapshot.forEach((doc) => {
list.push(doc.data());
});
setRestaurantsList(list);
} catch (e) {
setErrorMessage(
"There's nae bleeding restaurants, I told you to upload them!"
);
}
};
//Call when component is rendered
useEffect(() => {
getRestaurants();
}, []);
return (
<View style={tailwind('py-10 px-5')}>
<Text style={tailwind('text-4xl font-bold')}>
{restaurantsList}
</Text>
};
The State is not updating becuase it just taking the reference of list. So you need to update the like code below
.....
setRestaurantsList([...list]);
......
I've created a react function component for the context as follows:
const ItemContext = createContext()
const ItemProvider = (props) => {
const [item, setItem] = useState(null)
const findById = (args = {}) => {
fetch('http://....', { method: 'POST' })
.then((newItem) => {
setItem(newItem)
})
}
let value = {
actions: {
findById
},
state: {
item
}
}
return <ItemContext.Provider value={value}>
{props.children}
</ItemContext.Provider>
}
In this way, I have my context that handles all the API calls and stores the state for that item. (Similar to redux and others)
Then in my child component further down the line that uses the above context...
const smallComponent = () =>{
const {id } = useParams()
const itemContext = useContext(ItemContext)
useEffect(()=>{
itemContext.actions.findById(id)
},[id])
return <div>info here</div>
}
So the component should do an API call on change of id. But I'm getting this error in the console:
React Hook useEffect has a missing dependency: 'itemContext.actions'. Either include it or remove the dependency array react-hooks/exhaustive-deps
If I add it in the dependency array though, I get a never ending loop of API calls on my server. So I'm not sure what to do. Or if I'm going at this the wrong way. Thanks.
=== UPDATE ====
Here is a jsfiddle to try it out: https://jsfiddle.net/zx5t76w2/
(FYI I realized the warning is not in the console as it's not linting)
You could just utilize useCallback for your fetch method, which returns a memoized function:
const findById = useCallback((args = {}) => {
fetch("http://....", { method: "POST" }).then(newItem => {
setItem(newItem);
});
}, []);
...and put it in the useEffect:
...
const { actions, state } = useContext(ItemContext)
useEffect(() => {
actions.findById(id)
}, [id, actions.findById])
...
Working example: https://jsfiddle.net/6r5jx1h7/1/
Your problem is related to useEffect calling your custom hook again and again, because it's a normal function that React is not "saving" throughout the renders.
UPDATE
My initial answer fixed the infinite loop.
Your problem was also related to the way you use the context, as it recreates the domain objects of your context (actions, state, ..) again and again (See caveats in the official documentation).
Here is your example in Kent C. Dodds' wonderful way of splitting up context into state and dispatch, which I can't recommend enough. This will fix your infinite loop and provides a cleaner structure of the context usage. Note that I'm still using useCallback for the fetch function based on my original answer:
Complete Codesandbox https://codesandbox.io/s/fancy-sea-bw70b
App.js
import React, { useEffect, useCallback } from "react";
import "./styles.css";
import { useItemState, ItemProvider, useItemDispatch } from "./item-context";
const SmallComponent = () => {
const id = 5;
const { username } = useItemState();
const dispatch = useItemDispatch();
const fetchUsername = useCallback(async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/" + id
);
const user = await response.json();
dispatch({ type: "setUsername", usernameUpdated: user.name });
}, [dispatch]);
useEffect(() => {
fetchUsername();
}, [fetchUsername]);
return (
<div>
<h4>Username from fetch:</h4>
<p>{username || "not set"}</p>
</div>
);
};
export default function App() {
return (
<div className="App">
<ItemProvider>
<SmallComponent />
</ItemProvider>
</div>
);
}
item-context.js
import React from "react";
const ItemStateContext = React.createContext();
const ItemDispatchContext = React.createContext();
function itemReducer(state, action) {
switch (action.type) {
case "setUsername": {
return { ...state, username: action.usernameUpdated };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function ItemProvider({ children }) {
const [state, dispatch] = React.useReducer(itemReducer, {
username: "initial username"
});
return (
<ItemStateContext.Provider value={state}>
<ItemDispatchContext.Provider value={dispatch}>
{children}
</ItemDispatchContext.Provider>
</ItemStateContext.Provider>
);
}
function useItemState() {
const context = React.useContext(ItemStateContext);
if (context === undefined) {
throw new Error("useItemState must be used within a CountProvider");
}
return context;
}
function useItemDispatch() {
const context = React.useContext(ItemDispatchContext);
if (context === undefined) {
throw new Error("useItemDispatch must be used within a CountProvider");
}
return context;
}
export { ItemProvider, useItemState, useItemDispatch };
Both of these blog posts helped me a lot when I started using context with hooks initially:
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/how-to-use-react-context-effectively
OK, I didn't want to write an answer as Bennett basically gave you the fix, but I think it is missing the part in the component, so here you go:
const ItemProvider = ({ children }) => {
const [item, setItem] = useState(null)
const findById = useCallback((args = {}) => {
fetch('http://....', { method: 'POST' }).then((newItem) => setItem(newItem))
}, []);
return (
<ItemContext.Provider value={{ actions: { findById }, state: { item } }}>
{children}
</ItemContext.Provider>
)
}
const smallComponent = () => {
const { id } = useParams()
const { actions } = useContext(ItemContext)
useEffect(() => {
itemContext.actions.findById(id)
}, [actions.findById, id])
return <div>info here</div>
}
Extended from the comments, here's the working JSFiddle