React Hook "useState" cannot be called inside a callback - javascript

Is there another way to do this without having to place the useState variables outside the map function?
const slimy = [];
{
slimy.map(function (item) {
const [count, setCount] = useState("");
return (
<ListItem
key={item.createdAt}
style={{ display: "flex", justifyContent: "center" }}
>
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)} />
</div>
</ListItem>
);
});
}

You can define a custom component.
{
slimy.map(function (item) {
return (
<CustomItem key={item.createdAt} names={item.names} />
);
});
}
const CustomItem = (props) => {
console.log(props.names); // <----------------
const [count, setCount] = useState(0);
return (
<ListItem style={{ display: "flex", justifyContent: "center" }}>
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)} />
</div>
</ListItem>
)
}

You can put an array into state instead:
const [countArr, setCountArr] = useState(
() => new Array(slimy.length).fill(0)
);
return slimy.map(function(item, i){
<ListItem key={item.createdAt} style={{ display: 'flex', justifyContent: 'center'}}>
<div>
<p>{countArr[i]}</p>
<button onClick={() => { setCountArr(
countArr.map((count, j) => i === j ? count + 1 : count)
) }} />
</div>
</ListItem>)
});

Related

Component rerendering only after double click

I have a parent component that is passing products down into a subcomponent as state along with the product's filters. For some reason I have to double click the "filters" in order for the parent component to rerender with the filtered products. I understand because it is running asynchronously it is not updating the state immediately, but how can I force the update and rerender to run as soon as I add a filter without using forceUpdate? Is this where redux would come in to play?
Parent component
const [products, setProducts] = React.useState(data.pageContext.data);
const handleCount = () => {
setCount(count + 24);
}
return (
<div style={{width: "100%"}}>
<Header/>
<div style={{display: "flex", flexDirection: "row", justifyContent: "center"}}>
<Sidebar
products={products}
setProducts={setProducts}
baseProducts={data.pageContext.data}
/>
<div style={{display: "flex", flexDirection: "column"}}>
<h1 style={{width: "50%"}}>Cast vinyl</h1>
<h3>Product Count: {products.length}</h3>
<ProductList>
{products.slice(0, count).map(product => {
return (
<a href={`/vinyl/${product.data.sku}`}><div>
{product.data.field_product_image.length > 0 ?
<ProductImage images={data.data.allFiles} sku={`${product.data.sku}`}/> :
<StaticImage src="http://stagingsupply.htm-mbs.com/sites/default/files/default_images/drupalcommerce.png" width={250} alt=""/>}
<h3>{product.data.title}</h3>
<h5>{product.data.sku}</h5>
</div></a>
)
})}
</ProductList>
<h3 onClick={handleCount}>Load more</h3>
</div>
</div>
</div>
)
Child Component
const Sidebar = ({ setProducts, baseProducts }) => {
const [filters, setFilters] = React.useState([]);
const [click, setClick] = React.useState(false);
const handleClick = () => {
setClick(!click);
}
const onChange = (e) => {
if (!filters.includes(e)) {
setFilters([...filters, e])
}
if (filters.length > 0) {
const filteredProducts = baseProducts.filter(product => filters.includes(product.data.field_product_roll_size));
setProducts(filteredProducts);
}
}
const clearFilters = () => {
setFilters([]);
setProducts(baseProducts);
setClick(false);
}
const rollSize = [...new Set(baseProducts.map(fields => fields.data.field_product_roll_size))]
return (
<SidebarContainer>
<h3>Mbs Sign Supply</h3>
<ul>Sub Categories</ul>
<li>Calendered Vinyl</li>
<li>Cast Vinyl</li>
<h3>Filters</h3>
{filters.length > 0 ? <button onClick={clearFilters}>Clear Filters</button> : null}
<li onClick={() => handleClick()}>Roll Size</li>
{/*map through roll size array*/}
{/*each size has an onclick function that filters the products array*/}
{click ? rollSize.sort().map(size => {
return (
<span style={{display: "flex", flexDirection: "row", alignItems: "center", height: "30px"}}>
<Checkbox onClick={() => {onChange(size)}} />
<p >{size}</p>
</span>
)
}) : null}
<li>Width</li>
demo can be found at http://gatsby.htm-mbs.com:8000/cast-vinyl, clicking "Roll Size" from the left and then double clicking a filter
Thanks in advance
All I needed was a little useEffect
React.useEffect(() => {
if (filters.length > 0) {
const filteredProducts = baseProducts.filter(product => filters.includes(product.data.field_product_roll_size));
setProducts(filteredProducts);
}
}, [filters]);

How do I associate seperate state to each button?

Hello
I am trying to associate a like button with each PaperCard component as shown in the code below. I have included the relevant code. Currently, The like button shows up and every time you click it the counter increases BUT all the buttons share the same state. So I am trying to fix that. I am new to JS and React.
Any help will be appreciated. Thanks!
function Home() {
const [likes, setLikes] = useState(0);
const incrementLikes = () => {
const addToLikes = likes + 1;
setLikes(addToLikes)
}
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);}
return (
<div>
<div style={{display:'flex', justifyContent:'center'}}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Grid key={paper.title}>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
))}
<div style={{display:'flex', justifyContent: 'center'}}>
<Button variant="contained" onClick={loadMorePapers}>Load More</Button>
</div>
</div>
)
}
The element from the map callback is extracted as a component, and now every button has its own state.
function Home() {
return (
<div>
<div style={{ display: "flex", justifyContent: "center" }}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<LikeButton paper={paper} key={paper.title} />
))}
<div style={{ display: "flex", justifyContent: "center" }}>
<button variant="contained" onClick={loadMorePapers}>Load More</button>
</div>
</div>
);
}
function LikeButton(paper) {
const [likes, setLikes] = useState(0);
const incrementLikes = () => {
const addToLikes = likes + 1;
setLikes(addToLikes);
};
return (
<div key={paper.title}>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</div>
);
}
Create a new functional component called LikeButton (or something relevant) to house the state for each button independently.
In that component, add the state values you want to track per each button. In your case it seems to just be the likes.
So could be something like:
const LikeButton = () => {
const [likes, setLikes] = useState(0); //likes controlled by state of component
const incrementLikes = () => {
setLikes((prevState) => prevState + 1);
};
return <button onClick={incrementLikes}>Likes: {likes}</button>;
};
Then add that component in place of your existing button and remove the state for likes in the Home component. E.g.:
function Home() {
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);
};
return (
<div>
<div style={{ display: "flex", justifyContent: "center" }}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Grid key={paper.title}>
<LikeButton/>
<PaperCard title={paper.title} abstract={paper.abstract} />
</Grid>
))}
<div style={{ display: "flex", justifyContent: "center" }}>
<Button variant="contained" onClick={loadMorePapers}>
Load More
</Button>
</div>
</div>
);
}
Should you want to control state from the Home component, you can pass the likes as props, but it doesn't seem necessary for what you want.
In this situation you should consider using a reusable button component in order to control state within the component itself. Then you do not have to worry about the buttons sharing the same state. Here would be a simple example of a button component that will track it's count independent of the other buttons that are rendered:
import React, { useState } from 'react';
export default function CounterButton() {
const [count, setCount] = useState(0);
function incrementLikes() {
setCount(count + 1);
}
return (
<button onClick={incrementLikes}>
{count} likes
</button>
);
}
You could simply render these buttons like in the pseudo code below:
{[0, 1, 2, 3].map((num: number, index: number) => (
<div key={index}>
<CounterButton />
</div>
))}
I think you're doing too much in one component. The "likes" in your example are for an individual paper, not for the whole site, right?
Maybe something like this...
function Home() {
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);
}
return (
<div>
<div style={{display:'flex', justifyContent:'center'}}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Paper {...paper} key={paper.title} />
))}
<div style={{display:'flex', justifyContent: 'center'}}>
<Button variant="contained" onClick={loadMorePapers}>Load More</Button>
</div>
</div>
);
}
function Paper(props){
const [likes, setLikes] = useState(0);
const incrementLikes = () => setLikes(likes + 1)
return (
<Grid>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
)
}
If the data from the api has a key/id you can pass that to your incrementLikes function and use it to increment the likes for the right item.
const [apiData, setApidData] = useState(...)
const incrementLikes = (id) => {
const updated = apiData.map((paper) => {
if (paper.id === id) {
return {
...paper,
likes: paper.likes + 1
};
}
return paper;
});
setApidData(updated);
};
Then pass the id in the button
<button onClick={() => incrementLikes(paper.id)}>Likes: {paper.likes}</button>
// Get a hook function
const { useState } = React;
const PaperCard = ({ title, abstract }) => {
return (
<div>
<p>{title}</p>
<p>{abstract}</p>
</div>
);
};
const Header = () => {
const [apiData, setApidData] = useState([
{
title: 'Testing likes',
id: 1,
likes: 0,
abstract: 'abs',
},
{
title: 'More likes',
id: 3,
likes: 5,
abstract: 'abstract',
}
]);
const incrementLikes = (id) => {
const updated = apiData.map((paper) => {
if (paper.id === id) {
return {
...paper,
likes: paper.likes + 1
};
}
return paper;
});
setApidData(updated);
};
const loadMorePapers = (e) => {};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<h1>Latest Papers</h1>
</div>
{apiData.map((paper) => (
<div key={paper.title}>
<button onClick={() => incrementLikes(paper.id)}>Likes: {paper.likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract} />
</div>
))}
<div style={{ display: 'flex', justifyContent: 'center' }}>
<button variant='contained' onClick={loadMorePapers}>
Load More
</button>
</div>
</div>
);
};
// Render it
ReactDOM.render(<Header />, document.getElementById('react'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Cannot make element disappear/appear properly inside of my React App

So I have built a movie search app.
On the 4th page we have the ability to search for a specific movie or TV show.
Now I have built a logic that will display "Movies(Tv Shows) not found" when there are no search results.
Here is the code of the entire "Search" Component:
const Search = () => {
const [type, setType] = useState(0);
const [page, setPage] = useState(1);
const [searchText, setSearchText] = useState("");
const [content, setContent] = useState([]);
const [numOfPages, setNumOfPages] = useState();
const [noSearchResults, setNoSearchResults] = useState(false);
const fetchSearch = async () => {
try {
const { data } = await axios.get(`https://api.themoviedb.org/3/search/${type ? "tv" : "movie"}?api_key=${process.env.REACT_APP_API_KEY}&language=en-US&query=${searchText}&page=${page}&include_adult=false`);
setContent(data.results);
setNumOfPages(data.total_pages);
} catch (error) {
console.error(error);
}
};
const buttonClick = () => {
fetchSearch().then(() => {
if (searchText && content.length < 1) {
setNoSearchResults(true);
} else {
setNoSearchResults(false);
}
});
};
useEffect(() => {
window.scroll(0, 0);
fetchSearch();
// eslint-disable-next-line
}, [page, type]);
return (
<div>
<div style={{ display: "flex", margin: "25px 0" }}>
<TextField className="textBox" label="Search" variant="outlined" style={{ flex: 1 }} color="secondary" onChange={e => setSearchText(e.target.value)} />
<Button variant="contained" style={{ marginLeft: "10px" }} size="large" onClick={buttonClick}>
<SearchIcon color="secondary" fontSize="large" />
</Button>
</div>
<Tabs
value={type}
indicatorColor="secondary"
onChange={(event, newValue) => {
setPage(1);
setType(newValue);
}}
style={{
marginBottom: "20px",
}}
>
<Tab style={{ width: "50%" }} label="Search Movies" />
<Tab style={{ width: "50%" }} label="Search TV Shows" />
</Tabs>
<div className="trending">
{content && content.map(c => <SingleContent key={c.id} id={c.id} poster={c.poster_path} title={c.title || c.name} date={c.first_air_date || c.release_date} media_type={type ? "tv" : "movie"} vote_average={c.vote_average} />)}
{noSearchResults && (type ? <h2>Tv Shows not found</h2> : <h2>Movies not found</h2>)}
</div>
{numOfPages > 1 && <CustomPagination setpage={setPage} numOfPages={numOfPages} />}
</div>
);
};
You can see this in action here.
The problem that happens is that even when I have something in my search results, it still shows the Movies(Tv Shows) not found message.
And then if you click the search button again it will disappear.
A similar thing happens when there are no search results.
Then the Movies(Tv Shows) not found message will not appear the first time, only when you press search again.
I don't understand what is going on. I have used .then after my async function and still it does not execute in that order.
Try adding noSearchResults to your useEffect hook. That hook is what tells React when to re-render, and right now it's essentially not listening to noSearchResult whenever it changes because it's not included in the array.

Can't able to clear the input value to empty string

I have made a table with a search bar functionality, it filter the data when press the search button and reset the filter function and show unfilter data when click the clear button but it's not clearing the current input value from the display. however it clear the filetr function and show unfilter data. I tired setting state to empty string but still not able to clear the input value, I'm new in react need assistance to understand the issue
1. App.js having search bar and all the state and function
function App() {
const [searchTerm, setSearchTerm] = useState("");
const classes = useStyles();
const [buttonSearch, setButtonSearch] = useState("");
const getSearchTerm = (event) => {
let searchWord = event.target.value;
setSearchTerm(searchWord);
console.log(searchWord);
}
const doSearch = () => {
console.log('this is from the doSearch func', searchTerm)
setButtonSearch(searchTerm);
}
const clearSearch = () => {
console.log('im working')
setSearchTerm("");
setButtonSearch("");
}
return (
<div className="App">
<div className="wrapper">
<div className="container-table">
<div className="head">
<h5 className='management'>MANAGEMENT</h5>
<div className="head-middle">
<h2>Clients</h2>
<div className="button-collection">
<Button style={{ backgroundColor: '#5900B4', color: '#FFFFFF', fontSize: '15px', fontWeight: '900', width: '206px', height: '42px' }}
variant="contained"
className='add-collection-btn'
startIcon={<AddIcon />}
>
New Collection
</Button>
</div>
</div>
<div className="head-bottom">
<div className="head-button">
<div className="search">
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search..."
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
onChange={getSearchTerm}
/>
</div>
</div>
<Button onClick={doSearch}
style={{ backgroundColor: 'white', color: 'black', width: '100px', height: '40px', marginLeft: '20px', marginRight: '20px' }} variant="contained">
Search
</Button>
<Button onClick={clearSearch}
style={{ backgroundColor: 'white', color: 'black', width: '100px', height: '40px' }} variant="contained">
Clear
</Button>
</div>
<Button
style={{ backgroundColor: 'transparent', color: '#5900B4', width: '206px', height: '42px', borderColor: '#5900B4', fontSize: '15px', fontWeight: '900' }}
variant="outlined" color="primary"
startIcon={<FilterListIcon />}
>
SHOW FILTER
</Button>
</div>
<div className="table">
<EnhancedTable
searchTerm={buttonSearch}
/>
</div>
</div>
</div>
</div>
</div>
);
}
2. table.js having filter and map function
export default function EnhancedTable(props) {
console.log("these r props for table component", props);
const { searchTerm } = props;
console.log("table searchTerm value", searchTerm)
const classes = useStyles();
const [order, setOrder] = React.useState('asc');
const [orderBy, setOrderBy] = React.useState('calories');
const [selected, setSelected] = React.useState([]);
const [page, setPage] = React.useState(0);
const [dense, setDense] = React.useState(false);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
const isSelected = (name) => selected.indexOf(name) !== -1;
const [data, setData] = useState([]);
const getData = async () => {
try {
const data = await axios.get("something");
console.log('This is data from axios', data.data);
setData(data.data);
} catch (e) {
console.log("this is error for fetching data", e)
}
};
useEffect(() => {
getData();
}, [])
const filtered = useMemo(() => {
if (!searchTerm) {
return data;
}
const term = searchTerm.toLowerCase()
return data.filter(({ clientName, clientEmail }) => clientName.toLowerCase().includes(term)
|| clientEmail.toLowerCase().includes(term)
)
}, [data, searchTerm])
return (
<div className={classes.root}>
<Paper className={classes.paper}>
<EnhancedTableToolbar numSelected={selected.length} />
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size={dense ? 'small' : 'medium'}
aria-label="enhanced table"
>
<EnhancedTableHead
classes={classes}
numSelected={selected.length}
order={order}
orderBy={orderBy}
/>
<TableBody>
{filtered
.map((item, index) => {
return (
<TableRow
hover
role="checkbox"
tabIndex={-1}
>
<TableCell padding="checkbox">
<Checkbox
/>
</TableCell>
<TableCell component="th" scope="row" padding="none">{item.clientName}</TableCell>
<TableCell align="right">{item.clientEmail}</TableCell>
<TableCell align="right">{item.clientWorkPhone}</TableCell>
<TableCell align="right">{item.clientIndustry}</TableCell>
<TableCell align="right">{item.tenantId}</TableCell>
<TableCell align="right">{item.clientWebsite}</TableCell>
<TableCell align="right">
<Button style={{ backgroundColor: 'transparent', color: '#5900B4' }}
variant="outlined" color="primary" href="#outlined-buttons" >
{<CreateIcon />}
</Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</Paper>
</div>
);
}
Try this option
<InputBase value={searchTerm}

Reactstrap dropdown not working with hooks

Goal
Dynamically create dropdown menus via hooks.
Challenge
When I change a hook value to my dropdown, the dropdown does not open or close. Its stays closed.
When I hard code the dropdown in the return, the open and close functions correctly.
Doesn't work Using hooks
///Various required imports are here..
export default function main(){
const [testb, state_set_testb] = useState(<div></div>);
function toggle_dropdown_function(toggle_request) {
console.log("fffffffffffffffffffffff",toggle_request)
state_set_dropdown_open(toggle_request)
}
state_set_testb(<div onMouseEnter={() => toggle_dropdown_function(true)} onMouseLeave={() => toggle_dropdown_function(false)}>
<Dropdown id="testa" isOpen={dropdownOpen} toggle={toggle_dropdown}>
<DropdownToggle style={{ backgroundColor: "transparent", border: "none", paddingTop: '8px', paddingBottom: '8px', paddingleft: '10px', paddingRight: '10px', color: "grey", fontSize: "14px " }}
color="light" >
othera
</DropdownToggle >
<DropdownMenu >
<DropdownItem style={{ fontSize: "14px " }}>Some Action</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>)
return <div>{testb}</div>
}
Works Not using hooks
///Various required imports are here..
export default function main(){
function toggle_dropdown_function(toggle_request) {
console.log("fffffffffffffffffffffff",toggle_request)
state_set_dropdown_open(toggle_request)
}
return <div onMouseEnter={() => toggle_dropdown_function(true)} onMouseLeave={() => toggle_dropdown_function(false)}>
<Dropdown id="testa" isOpen={dropdownOpen} toggle={toggle_dropdown}>
<DropdownToggle style={{ backgroundColor: "transparent", border: "none", paddingTop: '8px', paddingBottom: '8px', paddingleft: '10px', paddingRight: '10px', color: "grey", fontSize: "14px " }}
color="light" >
othera
</DropdownToggle >
<DropdownMenu >
<DropdownItem style={{ fontSize: "14px " }}>Some Action</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
}
Here is how you could accomplish it. Essentially you have a separate component called Dropdown and you push it to an array of dropdowns.
const { useState } = React;
const Dropdown = () => {
const [active, setActive] = useState('Select');
const [isOpen, setOpen] = useState(false);
const items = ["orange", "pear", "apple"];
return <div className={`dropdown`}>
<div onClick={()=> setOpen(!isOpen)} className={"dropdown__header"} >{active}</div >
{isOpen &&
<div className={"dropdown__body"}>
{items.map((item, index) => {
return <div key={index} onClick={(e) => {
setActive(item);
setOpen(false);
}}>{item}</div>
})}
</div>
}
</div>
}
const Main = () => {
const [dropdowns, setDropdowns] = useState([])
const addDropdowns = () => {
let updatedDropdowns = [...dropdowns];
updatedDropdowns.push(<Dropdown />)
setDropdowns(updatedDropdowns);
}
return (
<div className={"main"}>
<button onClick={addDropdowns}>
Add Dropdown
</button>
{dropdowns.map((dropdown, index) => {
return <div key={index}>{dropdown}</div>
})}
</div>
)
}
ReactDOM.render(<Main />, document.getElementById('app'))
Here is some codepen.
UPDATE
I managed to use reactstrap using the same approach and I did not notice any problems.
Here is a codepen

Categories