I have a component with an array of objects, which among other things i am filtering based on a string.
Problem is when I try to set the return of this filter to the local state, it's throwing errors that i am not quite understanding the reason.
import React, { useState, useEffect } from 'react';
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag'
const ProductsGrid = () => {
const [productsList, setProductsList] = useState([]);
const { loading, data } = useQuery(GET_PRODUCTS);
if (loading) return <div><h4>bla bla bla</h4></div>
const { merchants } = data;
let filtered = [];
merchants.map(merchant => {
merchant.products.map(product => {
if (product.name.includes('Polo')) {
filtered.push(product);
}
})
})
console.log({ filtered });
}
This is printing the following:
So, because I want this array in my state, I decided to do this: setProductsList(filtered);
and what happened after inserting this line was this:
It started rendering multiple times. I assumed that, every time the state changes, it re-renders the component (correct me if im wrong). I don't know why it did it multiple times though.
So, I thought on using useEffect to achieve the expected behaviour here.
useEffect(() => {
console.log('useeffect', data);
if (data) {
const { merchants } = data;
console.log({merchants })
let filtered = [];
merchants.map(merchant => {
merchant.products.map(product => {
if (product.name.includes('Polo')) {
filtered.push(product);
// console.log({ filtered });
}
})
})
console.log({ filtered });
setProductsList(filtered);
}
}, [data])
and the output was it:
So, I am understanding what's going on here and what is this last error about.
I assume my approaching is towards the right direction, using useEffect to run the function only once.
Your problem is due to the useEffect call occurring after the if (loading) condition, which returns early.
Calling hooks after a conditional return statement is illegal as it violates the guarantee that hooks are always called in exactly the same order on every render.
const { loading, data } = useQuery(GET_PRODUCTS);
const [productsList, setProductsList] = useState([]);
if (loading)
return (
<div>
<h4>bla bla bla</h4>
</div>
); // <-- Cannot use hooks after this point
useEffect(/* ... */)
To fix, move the useEffect call to be before the conditional.
Related
I understand that the isPending return in React 18 is used so that lower priority state updates can happen last. I'm struggling to figure out how to make isPending work in a simple REST GET call example. I'm trying to write the code below without having to use a separate isLoading type state.
Is there anything I can do to make this happen? That is, with only isPending render a "loading..." message until the data has been retrieved?
(the results I get from the code below is I see a flash of "loading", then immediately see [] followed by my data. I want to see "loading" until my data actually loads)
import axios from "axios";
import { useEffect, useState, useTransition } from "react";
export default function Test1() {
const [data, setData] = useState([])
const [isPending, startTransition] = useTransition();
useEffect(() => {
async function fetchMyAPI() {
startTransition(async () => {
const results = await axios.get("/api/rest");
setData(results.data);
})
}
fetchMyAPI()
}, [])
if (isPending) return <div>Loading...</div>
return (
<div>{JSON.stringify(data)}</div>
);
}
i have a problem with an hooks.
When i make
export default function Checkout() {
const { readRemoteFile } = usePapaParse();
const [parsedCsvData, setParsedCsvData] = useState([]);
const handleReadRemoteFile = file => {
readRemoteFile(file, {
complete: (results) => {
setParsedCsvData(results.data);
},
});
};
handleReadRemoteFile(CSVsource);
}
I have an infinite loop but when i make
export default function Checkout() {
const { readRemoteFile } = usePapaParse();
const [parsedCsvData, setParsedCsvData] = useState([]);
const handleReadRemoteFile = file => {
readRemoteFile(file, {
complete: (results) => {
console.log(results.data);
},
});
};
handleReadRemoteFile(CSVsource);
}
I have'nt the problem.
Have you an idea ?
Thank's Yann.
While the component code you posted is still incomplete (CSVsource is undefined, there's no return), I think the problem is visible now:
You're running handleReadRemoteFile(CSVsource) on every render (because you're calling it directly). When the file is read, you are setting parsedCsvData, which (because state updates and prop updates trigger a rerender) renders the component again, triggering handleReadRemoteFile once again...
So what's the solution?
Data fetching is a classical example of using the useEffect hook. The following code should do what you want
const { readRemoteFile } = usePapaParse();
const [parsedCsvData, setParsedCsvData] = useState([]);
useEffect(()=>{
readRemoteFile(CSVSource, {
complete: results => setParsedCsvData(results.data);
});
}, [CSVsource]); // rerun this whenever CSVSource changes.
Please read up on the useEffect hook in the React docs.
Note: there's one problem with the code I posted above. When the component is unmounted while the CSV is still loading, you will get an error "state update on unmounted component" as soon as the loading finished. To mitigate this, use the return function of useEffect in combination with a local variable.
So the final code looks like this:
useEffect(()=>{
let stillMounted = true;
readRemoteFile(CSVSource, {
complete: results => {
if(!stillMounted) return; // prevent update on unmounted component
setParsedCsvData(results.data);
}
});
return ()=>stillMounted = false; // this function is ran before the effect is ran again and on unmount
}, [CSVsource]);
My useEffect populates tempStocksData, which is passed into setStockData() when the Promise is fulfilled. As shown in the code below, print out tempStocksData and stockData, which is supposed to be populated since I called setStockData(tempStocksData). You can see that Promise is fulfilled since it executes the prints. However, stockData is empty. For some reason setStockData is not working since stockData is not being populated. Below is the code for reference:
const [ stockData, setStockData ] = useState([])
const getStocksData = (stock) => {
return axios.get(`${BASE_URL}?symbol=${stock}&token=${TOKEN}`).catch((error) => {
console.error("Error", error.message)
})
}
useEffect(()=> {
let tempStocksData = []
const stocksList = ["AAPL", "MSFT", "TSLA", "FB", "BABA", "UBER", "DIS", "SBUX"];
let promises = [];
stocksList.map((stock) => {
promises.push(
getStocksData(stock)
.then((res) =>
{tempStocksData.push({
name: stock,
...res.data
})
})
)
})
Promise.all(promises).then(()=>{
console.log(tempStocksData)
setStockData(tempStocksData)
console.log(stockData)
})
}, [])
Please help me resolve this issue. Let me know if there is something I'm missing or something that I'm doing that is not up to date with versions/dependencies or if I'm doing Promise() js wrong.
Are you even entering your Promise.all sequence to begin with?
You are already ending the promise by having a .then function after getting the stockdata.
stocksList.map((stock) => {
promises.push(
getStocksData(stock)
)
})
Promise.all(promises).then((result)=>{
const tempStocks = result.map((stock) => {
return {
name: stock.name,
data: stock.data
}
});
console.log(tempStocksData)
setStockData(tempStocksData)
console.log(stockData)
})
Note: Above code is untested but is made to show the point
Try using the spread operator when you setStockData
like this
setStockData([...tempStocksData])
Since I've stumbled across this issue today while looking up a setData issue, let me clarify some things.
Others have pointed out that your use of promises is probably not what you actually intend to do.
Regardless, it is important to understand that a console.log of stockData inside the same useEffect that issues setStockData (even when the setter is called "before" the logging attempt) will not show the updated data in the console.
This is because all setters from useState are batched together inside useEffect calls and the corresponding getter (stockData in this case) will only reflect the updated value in the next rendering loop. It will, however, be made available when rendering or to any other hooks listening to changes to stockData.
You can find an example implementation on StackBlitz. Note that the console.log will show an empty array even though the view is updated with the API query results.
The code example from StackBlitz reproduced here:
import * as React from 'react';
import './style.css';
import { useState, useEffect } from 'react';
import axios from 'axios';
const TOKEN = 'IAPYYRPR0LN9K0K4';
const BASE_URL = 'https://www.alphavantage.co/query?function=GLOBAL_QUOTE';
export default function App() {
const [stockData, setStockData] = useState([]);
const getStocksData = (stock: string) => {
return axios
.get<{ 'Global Quote': { [data: string]: string } }>(
`${BASE_URL}&symbol=${stock}&apikey=${TOKEN}`
)
.then((result) => result.data)
.catch((error) => {
console.error('Error', error.message);
});
};
useEffect(() => {
const stocksList = ['AAPL', 'MSFT', 'TSLA'];
let promises: Promise<void | {
'Global Quote': { [data: string]: string };
}>[] = [];
stocksList.map((stock) => {
promises.push(getStocksData(stock));
});
Promise.all(promises).then((result) => {
setStockData(result);
console.log(stockData);
});
}, []);
return <pre>{JSON.stringify(stockData, undefined, ' ')}</pre>;
}
I have a custom hook that fetches a local JSON file that many components make use of.
hooks.js
export function useContent(lang) {
const [content, setContent] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
fetch(`/locale/${lang}.json`, { signal: signal })
.then((res) => {
return res.json();
})
.then((json) => {
setContent(json);
})
.catch((error) => {
console.log(error);
});
return () => {
abortController.abort();
};
}, [lang]);
return { content };
}
/components/MyComponent/MyComponent.js
import { useContent } from '../../hooks.js';
function MyComponent(props) {
const { content } = useContent('en');
}
/components/MyOtherComponent/MyOtherComponent.js
import { useContent } from '../../hooks.js';
function MyOtherComponent(props) {
const { content } = useContent('en');
}
My components behave the same, as I send the same en string to my useContent() hook in both. The useEffect() should only run when the lang parameter changes, so seeing as both components use the same en string, the useEffect() should only run once, but it doesn't - it runs multiple times. Why is that? How can I update my hook so it only fetches when the lang parameter changes?
Hooks are run independently in different components (and in different instances of the same component type). So each time you call useContent in a new component, the effect (fetching data) is run once. (Repeated renders of the same component will, as promised by React, not re-fetch the data.) Related: React Custom Hooks fetch data globally and share across components?
A general React way to share state across many components is using a Context hook (useContext). More on contexts here. You'd want something like:
const ContentContext = React.createContext(null)
function App(props) {
const { content } = useContent(props.lang /* 'en' */);
return (
<ContentContext.Provider value={content}>
<MyComponent>
<MyOtherComponent>
);
}
function MyComponent(props) {
const content = useContext(ContentContext);
}
function MyOtherComponent(props) {
const content = useContext(ContentContext);
}
This way if you want to update the content / language / whatever, you would do that at the app level (or whatever higher level you decide makes sense).
I'm hoping someone can explain to me the correct usage of React hook in this instance, as I can't seem to find away around it.
The following is my code
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// This is a trick so that the debounce doesn't run on initial page load
// we use a ref, and set it to true, then set it to false after
const firstUpdate = React.useRef(true);
const UserSearchTimer = React.useRef()
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
function _debounceSearch() {
clearTimeout(UserSearchTimer.current);
UserSearchTimer.current = setTimeout( async () => {
_getUsers();
}, DEBOUNCE_TIMER);
}
async function _getUsers(query = {}) {
if(type) query.type = type;
if(search) query.search = search;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
So essentially I have a table in which i am displaying users, when the page changes, or the perPage, or the order, or the type changes, i want to requery my user list so i have a useEffect for that case.
Now generally I would put the _getUsers() function into that useEffect, but the only problem is that i have another useEffect which is used for when my user starts searching in the searchbox.
I don't want to requery my user list with each and every single letter my user types into the box, but instead I want to use a debouncer that will fire after the user has stopped typing.
So naturally i would create a useEffect, that would watch the value search, everytime search changes, i would call my _debounceSearch function.
Now my problem is that i can't seem to get rid of the React dependency warning because i'm missing _getUsers function in my first useEffect dependencies, which is being used by my _debounceSearch fn, and in my second useEffect i'm missing _debounceSearch in my second useEffect dependencies.
How could i rewrite this the "correct" way, so that I won't end up with React warning about missing dependencies?
Thanks in advance!
I would setup a state variable to hold debounced search string, and use it in effect for fetching users.
Assuming your component gets the query params as props, it would something like this:
function Component({page, perPage, order, type, search}) {
const [debouncedSearch, setDebouncedSearch] = useState(search);
const debounceTimer = useRef(null);
// debounce
useEffect(() => {
if(debounceTime.current) {
clearTimeout(UserSearchTimer.current);
}
debounceTime.current = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_DELAY);
}, [search]);
// fetch
useEffect(() => {
async function _getUsers(query = {}) {
if(type) query.type = type;
if(debouncedSearch) query.search = debouncedSearch;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
_getUsers();
}, [page, perPage, order, type, debouncedSearch]);
}
On initial render, debounce effect will setup a debounce timer... but it is okay.
After debounce delay, it will set deboucedSearch state to same value.
As deboucedSearch has not changed, ferch effect will not run, so no wasted fetch.
Subsequently, on change of any query param except search, fetch effect will run immediately.
On change of search param, fetch effect will run after debouncing.
Ideally though, debouncing should be done at <input /> of search param.
Small issue with doing debouncing in fetching component is that every change in search will go through debouncing, even if it is happening through means other than typing in text box, say e.g. clicking on links of pre-configured searches.
The rule around hook dependencies is pretty simple and straight forward: if the hook function use or refer to any variables from the scope of the component, you should consider to add it into the dependency list (https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies).
With your code, there are couple of things you should be aware of:
1.With the first _getUsers useEffect:
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// Correctly it should be:
useEffect(() => {
_getUsers()
}, [_getUsers])
Also, your _getUsers function is currently recreated every single time the component is rerendered, you can consider to use React.useCallback to memoize it.
2.The second useEffect
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
// Correctly it should be
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [firstUpdate, _debounceSearch])