I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);
Related
I need to change the useState without rendering the page.
First is it possible?
const UsersComponent = ({valueProp}) => {
const [users, setUsers] = useState(valueProp);
const [oldUsers, setoldUsers] = useState(value);
const allUsers = useSelector((state) =>
state.users
);
useEffect(() => {
dispatch(getUsersData());
}, [dispatch]);
useEffect(() => {
// assign users to state oldUsers
}, [dispatch]);
const onClickMergeTwoArrayOfUsers = () => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
console.log("filteredUsers", filteredUsers); // not changed
};
I tried everything nothing helps me.
useEffect(() => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
}, [users]); // RETURN INFINITIVE LOOP
I am also try ->
useEffect(() => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
}, []);
Load only one and that doesn't mean anything to me..
I am try with useRef ,but that doesn't help me in this case.
I will try to explain the basis of the problem.
I need to get one get data. After that get on the click of a button, I need to merge oldUsers and users without rendering, change the state. That is problem.
If there is no solution to this problem, tell me what I could do to solve the problem?
I am googling but without succes ... I am also try this solution from interent ->
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
no work.
I am also try with ->
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
Here is problem because I need to asynchronous get only after that can I looping.
Using a ref is probably a better option for whatever it is you're ultimately trying to do.
Yes, it is possible, but it violates one of the core rules of React state: Do Not Modify State Directly.
React compares state values using Object.is equality, so if you simply mutate an object in state instead of replacing it with a new value that is not object-equal, then the state "update" will not cause a re-render (but this is considered a bug in your program!). Anyway, this is how you'd do it:
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.1/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useCallback, useState} = React;
function Example () {
const [state, setState] = useState([1]);
const logState = useCallback(() => console.log(state.join(', ')), [state]);
// Don't actually do this!!!
const mutateState = () => {
setState(arr => {
arr.push(arr.at(-1) + 1);
return arr;
});
};
return (
<>
<div>{state.join(', ')}</div>
<button onClick={mutateState}>Mutate state</button>
<button onClick={logState}>Log state</button>
</>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
I have a React component that fetches data using the useEffect hook like so:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
useEffect(() => {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}, [key, options])
return <p>{data}</p>;
}
This runs the hook every time key or options change. However, I'm also caching the data locally, and only want the effect to run when both key AND options change (since for each key/options combination the data will always be the same).
Is there a clean way to depend on the combination of key AND options rather than key OR options using React Hooks?
You can create this sort of logic with useRef(). Consider the following example and sandbox: https://codesandbox.io/s/react-hooks-useeffect-with-multiple-reqs-6ece5
const App = () => {
const [name, setName] = useState();
const [age, setAge] = useState();
const previousValues = useRef({ name, age });
useEffect(() => {
if (
previousValues.current.name !== name &&
previousValues.current.age !== age
) {
//your logic here
console.log(name + " " + age);
console.log(previousValues.current);
//then update the previousValues to be the current values
previousValues.current = { name, age };
}
});
return (
<div>
<input
placeholder="name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
placeholder="age"
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
);
};
Workflow:
We create a ref object for the two values we want to keep track of,
in this case its a name and age. The ref object is previousValues.
useEffect is defined but we do not provide it any dependencies.
Instead, we just have it execute whenever there is a state-change to
name or age.
Now inside useEffect we have conditional logic to check whether the
previous/initial values of both name and age are different than
their corresponding state-values. If they are then good we execute
our logic (console.log).
Lastly after executing the logic, update the ref object (previousValues) to the current values (state).
In order to run the effect when both values change, you need to make use of the previous values and compare them within the hook when either key or options change.
You can write a usePrevious hook and compare old and previous state as mentioned in this post:
How to compare oldValues and newValues on React Hooks useEffect?
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const previous = usePrevious({key, options});
useEffect(() => {
if(previous.key !== key && previous.options !== options) {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}
}, [key, options])
return <p>{data}</p>;
}
All provided solutions are perfectly fine, However there are some more complex situation e.g., When useEffect function should be called ONLY when dependency A and B changed while it also depends on C's value.
So I suggest using sequence of useEffects and intermediate States to provide more space for future logics. Implementation of this approach for asked question would be:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const [needsUpdate, setNeedsUpdate] = useState(()=>({key:false, option:false}));
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, key:true}));
},[key])
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, options:true}));
},[options])
useEffect(() => {
if (needsUpdate.key && needsUpdate.options){
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
setNeedsUpdate(()=>({key:false, option:false}));
}
}, [needsUpdate, key, options])
return <p>{data}</p>;
}
In this way we can apply almost any logic on our useEffect dependencies, However it has own drawbacks which is few more rendering cycle.
You can create a new custom hook which calls the callback with an argument with index/names of dependencies
const useChangesEffect = (callback, dependencies, dependencyNames = null) => {
const prevValues = useRef(dependencies);
useEffect(() => {
const changes = [];
for (let i = 0; i < prevValues.current.length; i++) {
if (!shallowEqual(prevValues.current[i], dependencies[i])) {
changes.push(dependencyNames ? dependencyNames[i] : i);
}
}
callback(changes);
prevValues.current = dependencies;
}, dependencies);
};
useChangesEffect((changes) => {
if (changes.includes(0)) {
console.log('dep1 changed');
}
if (changes.includes(1)) {
console.log('dep2 changed');
}
}, [dep1, dep2]);
I'm trying to update the state of an array with React Hooks, using an input received from a child component.
This is the code for the array I'm trying to update (in my App.js file):
const [results, setResults] = useState([]);
const submitHandler = newResult => {
const newArray = [...results, newResult];
setResults(newArray);
console.log(newArray);
console.log(results);
}
The newArray is updated and logged properly, with all the items that are submitted through the child component. But the state of results is never updated, and it always logs an empty array. It should be noted that other useState hooks in my app are working properly, only the one I'm using for this array isn't working. Does anyone know what could be wrong and how can it be fixed?
If it helps, this is the code that submits the items from the child component (Shorten.js) - these hooks are working perfectly fine:
const [urlInput, setUrlInput] = useState("");
const [error, setError] = useState(false);
const changeHandler = event => {
setUrlInput(event.target.value);
}
const submitHandler = event => {
event.preventDefault();
if (urlInput === "") {
setError(true);
}
else {
setError(false);
axios.post("https://rel.ink/api/links/", {
url: urlInput
})
.then(response => {
const newResult = {
original: urlInput,
shortened: "https://rel.ink/" + response.data.hashid
}
props.submit(newResult);
})
setUrlInput("");
}
}
In your example, you cannot guarantee the results state has been updated at the point of your console.log(results). This is because the React state update as asynchronous and applied in batches under the hood.
If you had your console.log call under const [result, setResults] = useState([]) then it will get called on every render pass, and therefore you should see the updated value logged out once the setState function has applied your new state.
For example:
const [results, setResults] = useState([]);
console.log(results);
const submitHandler = newResult => {
const newArray = [...results, newResult];
setResults(newArray);
console.log(newArray);
}
should log your new state on the next render pass.
You could also put your console.log in a useEffect which will let you know for sure that React knows your results have changed.
const [results, setResults] = useState([]);
useEffect(() => {
console.log(results);
}, [results);
const submitHandler = newResult => {
const newArray = [...results, newResult];
setResults(newArray);
console.log(newArray);
}
This means your console.log(results) will only be called when results changed, rather then on every render.
States
const [searchTerm, setSearchTerm] = useState("");
const [URL, SetURL] = useState<URL | String>();
const [searchResults, setSearchResults] = useState([]);
resetSearch Function
const resetSearch = () => {
setSearchTerm("");
setSearchResults([]);
inputRef.current.value = ""
}
ResetQuery Function
const resetSearchQueryOnRouteChange = () => {
SetURL(window.location.href)
if (window.location.href != URL) {
resetSearch();
}
}
useEffect
useEffect(() => {
resetSearchQueryOnRouteChange()
}, [window.location.href, URL])
The issue is that i need the the resetSearchQueryOnRouteChange() function to be run on every route render, but when i change routes the function is not called, but the function itself is fully functional.
How can i make the function run when i change route/url?
If you are not using 'react-router' each location change will trigger whole re-render of the page. So, this block of code wouldn't execute.
You need to start to use 'react-router' and you'll get access to location
I want to set state as props in react using hooks and I'm getting error:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
▶ 34 stack frames were collapsed.
My code:
First Component :
const List = () => {
const [items, setItems] = useState([{}])
useEffect(() => {
const fetchData = async () => {
const data = await fetch(
'http://localhost:5000/api',
);
const result = await data.json();
setItems(result);
};
fetchData();
}, []);
return (
<ActualList items={items}/>
)
}
and the second component:
const ActualList = props => {
const [items, setItems] = useState([{}])
setItems(props.items)
}
...
You are calling setItem in every render. Each time you change a state value, your component will be re-rendered, which will cause another state change, another re-render....
You should conditionally call setItems
You can directly pass props to useState:
const ActualList = props => {
const [items, setItems] = useState(props.items) // pass props.items as an initial state
}
So I eventually figured out how to do this, in case someone needs it here is the code :
const [items, setItems] = useState([{}]);
useEffect(() => setItems(props.items), [props])