Incorrect use of useEffect() when filtering an array - reactjs

I have this React app that's is getting data from a file showing in cards. I have an input to filter the cards to show. The problem I have is that after I filter once, then it doesn't go back to all the cards. I guess that I'm using useEffect wrong. How can I fix this?
import { data } from './data';
const SearchBox = ({ onSearchChange }) => {
return (
<div>
<input
type='search'
placeholder='search'
onChange={(e) => {
onSearchChange(e.target.value);
}}
/>
</div>
);
};
function App() {
const [cards, setCards] = useState(data);
const [searchField, setSearchField] = useState('');
useEffect(() => {
const filteredCards = cards.filter((card) => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
});
setCards(filteredCards);
}, [searchField]);
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={cards} />
</div>
);
}

you should Include both of your state "Card", "searchedField" as dependincies to useEffect method.once any change happens of anyone of them, your component will re-render to keep your data up to date,
useEffect(() => { // your code }, [searchField, cards]);

cards original state will be forever lost unless you filter over original data like const filteredCards = data.filter().
though, in a real project it's not interesting to modify your cards state based on your filter. instead you can remove useEffect and create a filter function wrapped at useCallback:
const filteredCards = useCallback(() => cards.filter(card => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
}), [JSON.stringify(cards), searchField])
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={filteredCards()} />
</div>
);
working example
about array as dependency (cards)
adding an object, or array as dependency at useEffect may crash your app (it will throw Maximum update depth exceeded). it will rerun useEffect forever since its object reference will change everytime. one approach to avoid that is to pass your dependency stringified [JSON.stringify(cards)]

Related

React dynamically added components not rendered

I'm dynamically adding instances of a custom (Kendo-React) component into an array in my main App.
The component:
const PersonDD = () => {
const ages = ["Child", "Adult", "Senior"];
return (
<div>
<div>Person:</div>
<DropDownList
data={ages} style={{ width: "300px", }}
/>
</div>
);
};
I'm adding one instance on initial render, and another two instances after the result from an Ajax call returns.
const SourceTab = (SourceTabProps) => {
....
var componentList = [];
componentList.push(<PersonDD/>);
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
console.log(res.data.item);
componentList.push(<PersonDD/>);
componentList.push(<PersonDD/>);
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{componentList}
</div>);
};
The problem I have is that the one instance in the initial array are rendered, but the two created after the Ajax call are not rendered.
Do I need to call .render() or something similar to refresh?
You can simply use react useState to rerender component and in jsx map them.
like this :
const SourceTab = (SourceTabProps) => {
const [componentList,setComponentList] = useState([PersonDD])
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
console.log(res.data.item);
setComponentList([...componentList,PersonDD,PersonDD])
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{componentList.map((Component,index)=> <Component key={index} />)}
</div>);
};
You need to remember that React only re-renders (refreshes the UI/view) when a state changes. Your componentList is not a state at the moment but just an ordinary variable. make it a state by using useState hook.
Not sure if it is a bad practice or not but I haven't seen any react project that keeps an entire component as a state so instead of creating a state with an array of components, just push a data representation of the components you want to render. Then display the component list using your list and using .map
Here's how it would look like.
....
const [personList, setPersonList] = useState([1]);
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
setPersonList(state => state.push(2)); //you can make this dynamic so it can rerender as much components as you like, for now im pushing only #2
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{personList.map((item, key) => <PersonDD key={key} />)}
</div>);
};
Need to use the map to render a list
<div className='assignment_div_css'>
{componentList.map(component => <>{component}</>)}
</div>);
also, use a usestate to variable
const [componentList , setComponentList ]= React.useState[<PersonDD/>];
inside function set like this
console.log(res.data.item);
setComponentList(state => [...state, <PersonDD/>, <PersonDD/>]);

How to execute useEffect only once inside looped component

I have component where I have array of data that is being looped using map and rendered a new component base one that and inside the looped component I have a useEffect that fetches the data from the api but it runs same api twice.
Here is the code
I am looping through array of rule_set_versions which is in this case size of 2
const ExpandedContent = ({ experiment }) => {
return experiment.rule_set_versions &&
experiment.rule_set_versions.map((ruleSetVersion) => <RuleSetVersionCollapse key={ruleSetVersion.id} ruleSetVersion={ruleSetVersion} />)
}
const ExperimentsCollapse = ({ experiment }) => {
return <React.Fragment>
<div className={styles.experiment_collapse_root}>
<Collapse>
<Collapse.Panel className={styles.experiment_item} extra={<ExtraTest experiment={experiment} />}>
<ExpandedContent experiment={experiment} />
</Collapse.Panel>
</Collapse>
</div>
</React.Fragment>
}
Here is my RuleSetVersionCollapse snippet
const ruleSet = useSelector(state => state.ruleSet)
React.useEffect(() => {
if (!ruleSet.id) {
dispatch(getRuleSetAction(ruleSetVersion.rule_set_id))
}
}, [dispatch])
And the useEffect runs twice even though the ruleSetVersion.rule_set_id is same on both the case.
Can anyone suggest any way I can solve this issue.
Thanks

How can I display my API Data from search in React?

So I'm having trouble writing the function to display the API data whenever I search something. I tried to follow tutorials online, but they are using different API's, so the function and setup is completely different from mine.
I have everything working as far as displaying data. I only need my searchCoin function to showcase whatever coin I look up and to display it.
From the API, the name and id show the exact same thing, so you could target the id or name to find the correct coin
Here is my code
function App() {
const [coins, setCoins] = useState([]);
const [search, setSearch] = useState('');
useEffect(() => {
axios
.get(
'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=3&page=1&sparkline=false'
)
.then(res => {
setCoins(res.data);
console.log(res.data);
})
.catch(error => console.log(error));
}, []);
const handleChange = e => {
setSearch(e.target.value);
};
const searchCoin = e => {
e.preventDefault();
I'm trying to fix this code below to work properly
// setSearch(
coins.filter(coin =>
coin.name.toLowerCase().includes(search.toLowerCase())
)
);
};
return (
<div className='coin-app'>
<div className='coin-search'>
<h1>Search a currency</h1>
<form onSubmit={searchCoin}>
<input type='text' onChange={handleChange} />
<button>Search</button>
</form>
</div>
{coins.map(coin => {
return (
<Coin
key={coin.id}
name={coin.name}
price={coin.current_price}
symbol={coin.symbol}
marketcap={coin.total_volume}
volume={coin.market_cap}
image={coin.image}
/>
);
})}
</div>
);
}
React Only Updates What’s Necessary
React DOM compares the element and its children to the previous one, and only applies the DOM updates necessary to bring the DOM to the
desired state. #reactjs.org
Therefore, every time when coins or search change, the App will re-render.
so we can just create a variable that filters using the search value, creating a variable FilteredCoins
const filteredCoins = coins.filter((coin) =>
coin.name.toLowerCase().includes(search.toLowerCase())
Notice that we are using search, which will be different every time the state change.
Then you can just iterate filteredCoins in this manner:
{filteredCoins.map((coin) => (
<Coin
key={coin.id}
name={coin.name}
price={coin.current_price}
symbol={coin.symbol}
marketcap={coin.total_volume}
volume={coin.market_cap}
image={coin.image}
/>
))}
sandbox to demonstrate:
https://codesandbox.io/s/coins-8ukzf
Here are the steps you need to do.
Add another state hook to hold the filtered coins list
const [filteredCoins, setFilteredCoins] = useState(coins);
add a useEffect to update the filteredCoins once the coins have been populated
useEffect(() => {
setFilteredCoins(coins)
} ,[coins])
Use the filteredCoins in your map
{filteredCoins.map((coin) => {
Set a useEffect to update the filteredCoins from the original coins on every search change
useEffect(() => {
setFilteredCoins(
coins.filter(coin =>
coin.name.toLowerCase().includes(search.toLowerCase())
)
)
} ,[search])
Or run it in your searchCoin function like this
const searchCoin = e => {
e.preventDefault();
I'm trying to fix this code below to work properly
setCoins(
coins.filter(coin =>
coin.name.toLowerCase().includes(search.toLowerCase())
)
);
};
This will run everytime search is updated and filter out only the ones that match

React Hook useEffect() run continuously although I pass the second params

I have problem with this code
If I pass the whole pagination object to the second parameters of useEffect() function, then fetchData() will call continuously. If I only pass pagination.current_page so It will call only one time, but when I set new pagination as you see in navigatePage() function, the useEffect() does not call to fetchData() although pagination has changed.
How to solve this. Thank you very much!
Besides I do not want the use useEffect() call when first time component mounted because the items is received from props (It is fetch by server, this is nextjs project).
import React, {useEffect, useState} from 'react';
import Filter from "../Filter/Filter";
import AdsListingItem from "../AdsListingItem/AdsListingItem";
import {Pagination} from "antd-mobile";
import styles from './AdsListing.module.css';
import axios from 'axios';
const locale = {
prevText: 'Trang trước',
nextText: 'Trang sau'
};
const AdsListing = ({items, meta}) => {
const [data, setData] = useState(items);
const [pagination, setPagination] = useState(meta);
const {last_page, current_page} = pagination;
const fetchData = async (params = {}) => {
axios.get('/ads', {...params})
.then(({data}) => {
setData(data.data);
setPagination(data.meta);
})
.catch(error => console.log(error))
};
useEffect( () => {
fetchData({page: pagination.current_page});
}, [pagination.current_page]);
const navigatePage = (pager) => {
const newPagination = pagination;
newPagination.current_page = pager;
setPagination(newPagination);
};
return (
<>
<Filter/>
<div className="row no-gutters">
<div className="col-md-8">
<div>
{data.map(item => (
<AdsListingItem key={item.id} item={item}/>
))}
</div>
<div className={styles.pagination__container}>
<Pagination onChange={navigatePage} total={last_page} current={current_page} locale={locale}/>
</div>
</div>
<div className="col-md-4" style={{padding: '15px'}}>
<img style={{width: '100%'}} src="https://tpc.googlesyndication.com/simgad/10559698493288182074"
alt="ads"/>
</div>
</div>
</>
)
};
export default AdsListing;
The issue is you aren't returning a new object reference. You save a reference to the last state object, mutate a property on it, and save it again.
const navigatePage = (pager) => {
const newPagination = pagination; // copy ref pointing to pagination
newPagination.current_page = pager; // mutate property on ref
setPagination(newPagination); // save ref still pointing to pagination
};
In this case the location in memory that is pagination remains static. You should instead copy all the pagination properties into a new object.
const navigatePage = (pager) => {
const newPagination = {...pagination}; // shallow copy into new object
newPagination.current_page = pager;
setPagination(newPagination); // save new object
};
To take it a step further you really should be doing functional updates in order to correctly queue up updates. This is in the case that setPagination is called multiple times during a single render cycle.
const navigatePage = (pager) => {
setPagination(prevPagination => {
const newPagination = {...prevPagination};
newPagination.current_page = pager;
});
};
In the case of pagination queueing updates may not be an issue (last current page set wins the next render battle), but if any state updates actually depend on a previous value then definitely use the functional update pattern,

useState hook always 1 step behind

I have seen other questions with the exact same title, but none solved my problem:
This is my code:
import React, { useState } from 'react';
import Validation from './Validation/Validation';
import Char from './Char/Char';
function App() {
const [textState, textSetState] = useState('');
const [charsState, charsSetState] = useState([]);
let inputChange = (event) => {
textSetState(event.target.value);
charsSetState(event.target.value.split('').map((char, index) => {
return <Char char={char} key={index} click={() => { deleteCharHandler(index) }} />
}));
}
const deleteCharHandler = (index) => {
alert(textState);
}
return (
<div className="App">
<input type="text" onChange={(event) => inputChange(event)} />
<Validation text={textState} />
{charsState}
</div>
);
}
export default App;
This is the result:
When I click a character, it displays the value from 1 step behind, like the example above.
You're putting an array of rendered Char components in your state, then rendering it. There are a number of issues with this approach.
The local copy of the state (charsState) will not be updated immediately; instead React will re-render the component with the new value for state.
Since you are defining the onClick callback within the function, deleteCharHandler will always be referencing an outdated copy of the state.
Multiple state hooks being updated in lockstep in this way will cause additional re-renders to happen.
Since the naming in your example is a bit confusing it's hard to tell what the desired behavior is to make good recommendations for how to resolve or refactor.
So there are a few things which may or may not be causing an issue so lets just clear a few things up:
You don't need to pass the anonymous function on the input:
<input type="text" onChange={inputChange} /> should suffice
As with OG state, it's never a good idea to call two setStates simultaneously, so lets combine the two:
const [state, setState] = useState({text: '', char: []});
Once you've updated everything you should be setting one state object onClick.
Your Char object is using click instead of onClick? unless you are using that as a callback method i'd switch to:
return <Char char={char} key={index} OnClick={() => deleteCharHandler(index)} />
If that doesn't fix your solution at the end, you can simply pass the deleteCharHandler the updated text value instead of re-grabbing the state value
I think you need useCallback or to pass textState in parameter. the deleteCharHandler method doesn't change in time with the textState value.
try :
return <Char char={char} key={index} click={() => { deleteCharHandler(index, textState) }} />
...
const deleteCharHandler = (index, textState) => {
alert(textState);
}
or :
import React, { useState, useCallback } from 'react';
...
const deleteCharHandler = useCallback(
(index) => {
alert(textState);
}, [textState]);
Try this
function App() {
const [textState, textSetState] = useState('');
const inputChange = useCallback((event) => {
textSetState(event.target.value);
},[])
function renderChars(){
return textState.split('').map((char, index) => {
return <Char char={char} key={index} click={() => { deleteCharHandler(index) }} />
}));
}
const deleteCharHandler = useCallback( (index) => {
alert(textState);
}, [textState])
return (
<div className="App">
<input type="text" onChange={inputChange} />
<Validation text={textState} />
{renderChars()}
</div>
);
}

Resources