React how to update array useState on button click - reactjs

I have seen a hundred videos and read about this, and it still doesn't make sense to me. I'm trying to update an array on a button click but when I log out the items array, it's one behind, meaning it's not reactive and gets added on the next paint. Also, I wrapped my Employee component inside a div only to be able to add an 'onClick' to it. Ideally, I'd like the onClick to be on the Employee component itself, but it doesn't work. Thanks.
import React, {ReactElement, useState } from "react";
import {TextContainer, Text } from "react-md";
import model from '../Models'
import Employee from './Employee/Employee'
import styles from '../home.module.scss'
export default function Home(): ReactElement {
const [items, setItems] = useState<Array<any>>([]);
const addItem = (val:any) => {
setItems([...items, val ])
console.log('items :', items)
}
return (
<div className='center'>
<TextContainer className='center'>
<Text type='headline-4' style={{color: 'white'}}>Employee List</Text>
</TextContainer>
<section className={styles.emp_list}>
{model.map((props, index) =><div key={index} onClick={() => addItem(props.name)}><Employee key={index} name={props.name} role={props.role} markets={props.markets} image={props.image}/></div>)}
</section>
</div>
)}
And my Employee component:
import React from "react";
import {MediaContainer} from "#react-md/media";
import styles from './employee.module.scss'
import { Card, CardContent, CardHeader} from "react-md";
function Employee(props: any) {
return (
<Card className={styles.emp_card}>
<CardHeader>
<div className={styles.emp_text}>
<p key={props.name}>Name: {props.name}</p>
<p key={props.role}>Title: {props.role}</p>
<p key={props.markets}>Markets: {props.markets[0]} {props.markets[1] && <span>and {props.markets[1]}</span>}</p>
</div>
</CardHeader>
<CardContent>
<MediaContainer>
<img key={props.image} src={props.image} alt="employee"/>
</MediaContainer>
</CardContent>
</Card>
)
}
export default Employee;

I don't think the documentation is clear enough on this, but if you want to setState using the previous state, then you should pass a function to useState. For example:
const addItem = (val:any) => {
setItems(prevItems => [...prevItems, val ])
}
https://reactjs.org/docs/hooks-reference.html#functional-updates

Related

Is there a way of supplying a void function and supplying the innards of function when invoked in another place?

Basically what I'm trying to achieve is that, let's say that I have a function in a parent component in React. In a child component i want to do some calculation like lets say distance to nearest elements containing box. But I want to invoke this method via a button click or something in the parent. I have refs pointing to my child component and I can achieve this if I add the click to these children. But since I don't have acces to the parent method; how am I to achieve such behaviour?
Here's my child component:
import { FC, useRef } from 'react'
import styles from './Card.module.scss'
const Card: FC<CardProps> = ({ image, title, info }) => {
const cardRef = useRef<HTMLDivElement>(null)
const scroll = (offset: number) => {
if (cardRef.current) {
cardRef.current.scrollLeft += offset
}
}
return (
<div className={styles.card} ref={cardRef}>
<img src={image} alt={image.split('.')[0]} height={170} width={96} />
<div className={styles['card-info-container']}>
<h3>{title}</h3>
<p>{info}</p>
<img src="Chevron.svg" alt="Chevron" />
</div>
</div>
)
}
export default Card
interface CardProps {
image: string
title: string
info: string
}
and here is my parent component:
import Card from '../Card/Card'
import styles from './Campaigns.module.scss'
import { campaignData } from '../../mockdata/campaigndata'
const Campaigns = () => {
const scroll = (offset: number) => {}
return (
<section className={styles.campaigns}>
<h3></h3>
<div className={styles['cards-container']}>
{campaignData.map(({ image, title, description }, index) => (
<Card image={image} title={title} info={description} key={index} />
))}
</div>
</section>
)
}
export default Campaigns
I think I've found a solution but it's not really an elegant one in my opinion. Instead of agonizing over creating refs directly in the child I've opted to create an array of refs from the length of the content using React's createRef fucntion here's what I have in my parent component.
import Card from '../Card/Card'
import styles from './Campaigns.module.scss'
import { campaignData } from '../../mockdata/campaigndata'
import { createRef } from 'react'
const Campaigns = () => {
const refs = campaignData.map(() => createRef())
console.log(refs)
return (
<section className={styles.campaigns}>
<h3></h3>
<div className={styles['cards-container']}>
{campaignData.map(({ image, title, description }, index) => (
<Card
image={image}
title={title}
info={description}
key={index}
forwardRef={refs[index]}
/>
))}
</div>
</section>
)
}
export default Campaigns
Now I can reference every element as a seperate entity and without the hassle

Re-Rendering a component

I'm doing a simple todo list using React. What I fail to do is to remove an item once I click on the button.
However, if I click delete and then add a new item, it's working, but only if I add a new todo.
Edit:I've edited the post and added the parent componenet of AddMission.
import React,{useState}from 'react';
import { Button } from '../UI/Button/Button';
import Card from '../UI/Card/Card';
import classes from '../toDo/AddMission.module.css'
const AddMission = (props) => {
const [done,setDone]=useState(true);
const doneHandler=(m)=>{
m.isDeleted=true;
}
return (
<Card className={classes.users}>
<ul>
{props.missions.map((mission) => (
<li className={mission.isDeleted?classes.done:''} key={mission.id}>
{mission.mission1}
<div className={classes.btn2}>
<Button onClick={()=>{
doneHandler(mission)
}} className={classes.btn}>Done</Button>
</div>
</li>
)) }
</ul>
</Card>
);
};
export default AddMission;
import './App.css';
import React,{useState} from 'react';
import { Mission } from './components/toDo/Mission';
import AddMission from './components/toDo/AddMission';
function App() {
const [mission,setMission]=useState([]);
const [isEmpty,setIsEmpty]=useState(true);
const addMissionHandler = (miss) =>{
setIsEmpty(false);
setMission((prevMission)=>{
return[
...prevMission,
{mission1:miss,isDeleted:false,id:Math.random().toString()},
];
});
};
return (
<div className="">
<div className="App">
<Mission onAddMission={addMissionHandler}/>
{isEmpty?<h1 className="header-title">Start Your Day!</h1>:(<AddMission isVisible={mission.isDeleted} missions={mission}/>)}
</div>
</div>
);
}
const doneHandler=(m)=>{
m.isDeleted=true;
}
This is what is causing your issue, you are mutating an object directly instead of moving this edit up into the parent. In react we don't directly mutate objects because it causes side-effects such as the issue you are having, a component should only re-render when its props change and in your case you aren't changing missions, you are only changing a single object you passed in to your handler.
Because you haven't included the code which is passing in the missions props, I can't give you a very specific solution, but you need to pass something like an onChange prop into <AddMission /> so that you can pass your edited mission back.
You will also need to change your function to something like this...
const doneHandler = (m) =>{
props.onChange({
...m,
isDeleted: true,
});
}
And in your parent component you'll then need to edit the missions variable so when it is passed back in a proper re-render is called with the changed data.
Like others have mentioned it is because you are not changing any state, react will only re-render once state has been modified.
Perhaps you could do something like the below and create an array that logs all of the ids of the done missions?
I'm suggesting that way as it looks like you are styling the list items to look done, rather than filtering them out before mapping.
import React, { useState } from "react";
import { Button } from "../UI/Button/Button";
import Card from "../UI/Card/Card";
import classes from "../toDo/AddMission.module.css";
const AddMission = (props) => {
const [doneMissions, setDoneMissions] = useState([]);
return (
<Card className={classes.users}>
<ul>
{props.missions.map((mission) => (
<li
className={
doneMissions.includes(mission.id)
? classes.done
: ""
}
key={mission.id}
>
{mission.mission1}
<div className={classes.btn2}>
<Button
onClick={() => {
setDoneMissions((prevState) => {
return [...prevState, mission.id];
});
}}
className={classes.btn}
>
Done
</Button>
</div>
</li>
))}
</ul>
</Card>
);
};
export default AddMission;
Hope that helps a bit!
m.isDeleted = true;
m is mutated, so React has no way of knowing that the state has changed.
Pass a function as a prop from the parent component that allows you to update the missions state.
<Button
onClick={() => {
props.deleteMission(mission.id);
}}
className={classes.btn}
>
Done
</Button>;
In the parent component:
const deleteMission = (missionId) => {
setMissions(prevMissions => prevMissions.map(mission => mission.id === missionId ? {...mission, isDeleted: true} : mission))
}
<AddMission missions={mission} deleteMission={deleteMission} />

How to filter and sort product in ReactJs by Dropdown component of React bootstrap

I am creating a filter function for the product list in UI for the user. But I have an issue that, I do not know and never ever try with this function before, so I really difficult to resolve it. I have only 1 day left to do that, so I was very confused
This is my Dropdown Component
import React from "react";
import { Dropdown as BootstrapDropdown } from "react-bootstrap";
import PropTypes from "prop-types";
import "../Dropdown/index.css";
const Dropdown = ({ items }) => {
return (
<BootstrapDropdown className="sort-dropdown">
<BootstrapDropdown.Toggle
className="sort-dropdown-toggle"
variant="success"
id="dropdown"
>
<span className="toggle-text">Selection</span>
</BootstrapDropdown.Toggle>
<BootstrapDropdown.Menu className="sort-dropdown-menu">
{items.map((name, index) => (
<BootstrapDropdown.Item
className="sort-dropdown-item"
key={index}
href={`#/action-${index}`}
>
{name}
</BootstrapDropdown.Item>
))}
</BootstrapDropdown.Menu>
</BootstrapDropdown>
);
};
Dropdown.propTypes = {
items: PropTypes.array,
};
Dropdown.defaultProps = {
items: [],
};
export default Dropdown;
And this is my page, which the place I get the Dropdown component
import React from "react";
import { Row } from "react-bootstrap";
import Group from "../../../components/Group/index";
import Dropdown from "../../../components/Dropdown/index";
import "../GroupBar/index.css";
const GroupBar = () => {
return (
<Row className="group-bar">
<Group
title="Product group"
element={<Dropdown items={["Milk Tea", "Juice"]} />}
/>
<Group
title="Sort by price"
element={<Dropdown items={["Low to hight", "Hight to low"]} />}
/>
</Row>
);
}
export default GroupBar;
I would like to filter (by category) and sort (by price) my product page by items of the dropdown. When I select that item, the product will be filtered according to the item I chose.
This is my product list page
import React, { useEffect } from "react";
import { Container, Row, Col } from "react-bootstrap";
import ProductItem from "../../../components/ProductItem/index";
import Loading from "../../../components/Loading";
import PropTypes from "prop-types";
import "../../../common/index.css";
import "../ProductList/index.css";
const ProductList = ({ products, loading, fetchProductRequest }) => {
useEffect(() => {
fetchProductRequest();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (loading) {
return (
<Container>
<Row>
<Col>
<Loading />
</Col>
</Row>
</Container>
);
}
return (
<Container>
<Row>
{!!products && products.length > 0 ? (
products.map((product, index) => {
return (
<ProductItem
key={index}
image={product.image}
name={product.name}
price={product.price}
/>
);
})
) : (
<h4 className="center-title">Product list is empty!</h4>
)}
</Row>
</Container>
);
};
export default ProductList;
This is the page for that,
The list of product and the filter/sort are located in the same folder but different files. Like this
The Group bar it is contain the filter/sort. I get all values by redux, saga
The main page, contain all of them is here
import React from "react";
import { Container } from "react-bootstrap";
import GroupBar from "./GroupBar";
import ProductContainer from "../../containers/ProductContainer";
import Carousel from "../../components/Carousels";
import "../Product/index.css";
const Product = () => {
return (
<Container fluid className="p-0">
<Carousel />
<Container>
<GroupBar />
<ProductContainer />
</Container>
</Container>
);
};
export default Product;
How can I filter related to my list product when it different file like that.
Please anyone help me with this my problem, I just have one day to finish that function, I already research on the internet but it's doesn't make me understand more because it so different from my code and I can not apply that code for mine.
I really really need your support and help as well as you can, the full the better. It's not just helped me to understand also for others like me who are doesn't try it before also see the code is easy to understand too.
I always welcome all of your comments. That is my pleasure. Thank you so much.
You will need your components to keep track of some state. You can read about how to do that here and here.
Once you understand the concept of state, you need to keep track of which item in the list is selected. So for example, you need to keep a state variable that tracks whether "Milk Tea" is selected or "Juice" is selected.
Then, once you have that state, you can display your items using filter or sort on the items list.
Personally, I recommend using class components instead of function components, but here is a minimal working example using function components:
import React, { useState } from 'react';
import { Dropdown as BootstrapDropdown } from 'react-bootstrap';
import './App.css';
const Dropdown = (props) => {
return (
<BootstrapDropdown>
<BootstrapDropdown.Toggle variant='success' id='dropdown'>
<span>Selection</span>
</BootstrapDropdown.Toggle>
<BootstrapDropdown.Menu>
{props.items.map((name, index) => (
<BootstrapDropdown.Item
key={index}
onClick={(event) => {
console.log(event.target.text);
props.setSelected(event.target.text);
}}
value={name}
>
{name}
</BootstrapDropdown.Item>
))}
</BootstrapDropdown.Menu>
</BootstrapDropdown>
);
};
function App() {
const [typeFilter, setTypeFilter] = useState('');
const allItems = [
{ name: 'Coffee Milk Tea', type: 'Tea' },
{ name: 'Earl Gray Milk Tea', type: 'Tea' },
{ name: 'Orange Juice', type: 'Juice' },
{ name: 'Wheatgrass Juice', type: 'Juice' },
];
const itemsToShow = allItems
.filter((item) => {
if (typeFilter) {
return item.type === typeFilter;
}
return true;
})
.map((item, i) => {
return <li key={i}>{item.name}</li>;
});
return (
<div>
<Dropdown items={['Tea', 'Juice']} setSelected={setTypeFilter} />
<ol>{itemsToShow}</ol>
</div>
);
}
export default App;
Notice that the App component stores the state, and passes its state setter to the Dropdown component. The Dropdown gets the setter in its props and uses it to set the App's state when an option is clicked. The App then uses its state to determine which items to show (using items.filter).
This is an example of Lifting state up. Normally, we would think of tracking which item is selected as the job of the dropdown. But, since we need to access that state in another component, we have to "lift up" that state to something higher in the tree. In this small example case, it was App that stored the state. In general, if the tree looks like this:
A
B
C
D
E
F
G
H
and you want to share state between G and D, you need to put that state inside of A because A is the closest parent of both G and D. If you want to share state between C and D, then you need to put that state inside B, because B is the parent of C and D.
In reference to the comment below, you probably want to keep the state for which thing in the dropdown is selected inside of your Product component. Then you need to pass the state setter down the props chain all the way into the Dropdown component, which can call that setter and update the state.
Sorry to hear about your tight schedule. Hopefully this answer can be of some use to you.

How to toggle class of a div element by clicking on a button that is inside another functional component (another file)?

I want to toggle class of container (file 2) by an onClick on the button that is inside another component file.
The button has already an onClick function and I want to make it so it calls on two functions. Two toggle functions for the button and two class toggles for the container.
Hope it makes sense.
Here is my code so far:
Button component (File 1)
import React, {useState} from "react";
import "./Sort.scss";
const Sort = () => {
const [toggleSortIcon, toggleSortIconSet] = useState(false);
return (
<div className="tools">
<button
onClick={() => toggleSortIconSet(!toggleSortIcon)}
className={`sort ${toggleSortIcon ? "horizontal" : "vertical"}`}>
</button>
</div>
);
}
export default Sort;
Div container component that I want to change the class of (File 2)
import React, {useState} from "react";
import "./WordContainer.scss";
import UseAnimations from "react-useanimations";
const WordContainer = ({title, definition, example}) => {
const [toggleSize, toggleSizeState] = useState(false);
return (
<div className={`container ${toggleSize ? "big" : "small"}`}>
<div className="content">
<h2>{title}</h2>
<p>{definition}</p>
<h3>Example</h3>
<p>{example}</p>
</div>
<img onClick={() => toggleSizeState(!toggleSize)} src="./resize.svg" alt="resize" className="resize"/>
<div className="bookmark">
<UseAnimations
animationKey="bookmark"
size={26}
/>
</div>
</div>
);
}
export default WordContainer;
You could either have a global state management system (redux, or with custom hooks) that you can use to store the icon size.
Or you could simply provide a callback to your component that stores the icon size in a parent component that then feeds it back to your
Something like this:
const [size, setSize] = useState(/* default value */);
render() {
<>
<Sort onSizeToggle={setSize} />
<WordContainer size={size} />
</>
}

reactstrap tooltip dynamic id

I am developing a react application and using reactstrap.
I am using Tooltip Component of reactstrap which requires a target attribute, a value of target element's id. This id is being geneated dynamically and seems reactstrap tooltip doesn't like it.
Component looks like:
MovieCard.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Col, Card, CardImg, CardBody, CardTitle, CardSubtitle, CardText, Button, Tooltip } from 'reactstrap';
import { LimitedTextTitle } from '../custom-styled/CustomStyledComponents';
class MovieCard extends Component {
constructor (props) {
super(props);
this.state = {
open: false
};
this.toggle = this.toggle.bind(this);
}
toggle () {
this.setState({
open: !this.state.open
})
}
render () {
const { imdbID, Title, Year, Rated, Plot, Country, Poster } = this.props.movie;
return (
<Col md="4">
<Card>
<CardImg
top
width="100%"
src={Poster}
alt="blah"
/>
</Card>
<CardBody>
<CardTitle>
<LimitedTextTitle id={imdbID}>
{`${Title} - (${Year})`}
</LimitedTextTitle>
<Tooltip placement='top' target={imdbID} isOpen={this.state.open} toggle={this.toggle}>
{Title}
</Tooltip>
</CardTitle>
<CardSubtitle>{`Rated: ${Rated} Country: ${Country}`}</CardSubtitle>
<CardText>{Plot}</CardText>
<Button>Read More</Button>
</CardBody>
</Col>
);
}
}
MovieCard.propTypes = {
movie: PropTypes.object.isRequired // eslint-disable-line
};
export default MovieCard;
Any suggestions?
react vesion 16.2.0
reactstrap 5.0.0-alpha.4
Was dealing with a similar problem.
Adding the code as an answer because i cannot add a comment above...
Hope it will help you or anyone else who will come across this question.
Description:
Use reactstrap tooltip for elements that are getting generated dynamically.
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import { Button, Tooltip } from 'reactstrap';
class App extends React.Component {
state = {};
toggle = targetName => {
if (!this.state[targetName]) {
this.setState({
...this.state,
[targetName]: {
tooltipOpen: true
}
});
} else {
this.setState({
...this.state,
[targetName]: {
tooltipOpen: !this.state[targetName].tooltipOpen
}
});
}
};
isToolTipOpen = targetName => {
return this.state[targetName] ? this.state[targetName].tooltipOpen : false;
};
render() {
return (
<div>
{[1, 2, 3, 4, 5, 6].map((x, i) => (
<div key={`div-${i}`}>
<Button color="link" id={`btn-${i}`}>
{x}
</Button>
<Tooltip
placement="right"
isOpen={this.isToolTipOpen(`btn-${i}`)}
target={`btn-${i}`}
toggle={() => this.toggle(`btn-${i}`)}>
Hello world!
</Tooltip>
</div>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
react: 16.9.0
reactstrap: 8.0.1
https://codesandbox.io/embed/angry-taussig-fup7i?fontsize=14
EUREKA I GOT IT!!! Building on Meir Keller's answer, there's no need to check if that state for the tooltip already exist. If it doesn't exist, it's false by default...
So long as state is defined, even if it's an empty state, this works.
This is using reactstrap's Popover, but it's the same concept.
import React, { Component, Fragment } from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css'
import { Container, Row, Col, Input, Button, Popover } from 'reactstrap';
class App extends Component {
state = {};
toggle = (target) => {
// console.log(typeof target) // make sure this is a string
this.setState({
...state,
[target]: !this.state[target]
});
};
render() {
return (
<Container>
{["Hello", "Greetings"].map((name) => (
<Row>
<Fragment>
<Button id={name} type="button">{name}</Button>
<Popover placement="right"
isOpen={this.state[`${name}`]}
target={name}
toggle={() => this.toggle(`${name}`)}>
<PopoverBody>
You've got mail. Did you know?
</PopoverBody>
</Popover>
</Fragment>
</Row>
))}
</Container>
);
}
}
export default App;
Create a new component in modular or component directory and paste this code
import React, { useState } from "react";
import { Tooltip } from "reactstrap";
const TooltipItem = props => {
const { position='top', id } = props;
const [tooltipOpen, setTooltipOpen] = useState(false);
const toggle = () => setTooltipOpen(!tooltipOpen);
return (
<span>
<span id={"tooltip-" + id}>
{props.children}
</span>
<Tooltip
placement={position}
isOpen={tooltipOpen}
target={"tooltip-" + id}
toggle={toggle}
>
{props.title}
</Tooltip>
</span>
);
};
export default TooltipItem;
Now import and use this tooltip component
import TooltipItem from "../Tooltip";
<TooltipItem id={'edit' + data.id} title={'Edit Store'}>
<i className="fas fa-edit pointer" onClick={() => this.onEditClick(data)}/>
</TooltipItem>
I will Like to add an answer for it as already many people have mentioned many ways to deal with the problem.
But reactStrap works perfectly fine, mistakes most of the beginners are doing that while creating id they are using special characters like:
- _ / # and it can even be a space
Just keep the id a very simple combination of chars and numbers reactstrap will work totally fine
New component UncontrolledTooltip will solve the problem. Just use
<UncontrolledTooltip
placement="right"
target={`btn-${i}`}
>
{props.title}
</UncontrolledTooltip>
I tried a lot of solutions and was still having trouble with Reactstrap Tooltip crashing when the target element is not in the Dom.
I combined a couple other solutions that people posted and this is the only way it worked for me. Conditional rendering FTW.
const ElementWithTooltip = ({
dynamicIdentifier, // string, number, w/e
}): ReactElement => {
// Target element state.
const [isTargetReady, setIsTargetReady] = useState(false);
// Target element ref.
const tooltipRef = useRef(null);
// Hook to recognize that the target is ready.
useEffect(() => {
const targetElement = tooltipRef.current;
if (targetElement) {
setIsTargetReady(true);
}
}, [tooltipRef.current]);
// TSX.
return (
<>
<span ref={tooltipRef}>This is the target element</span>
{isTargetReady && <UncontrolledTooltip autohide={false} target={tooltipRef}>
Tooltippy text stuff
</UncontrolledTooltip>}
</>
);
The imdbID most probably is starting with digit i.e. 123abcdefghijklmno1234567890
Remember that tooltips can't work in that case when ID starts with a number i.e. the Tooltip's target cannot start with an integer.
all you need to do here is, change this:
<CardTitle>
<LimitedTextTitle id={imdbID}>
{`${Title} - (${Year})`}
</LimitedTextTitle>
<Tooltip placement='top' target={imdbID} isOpen={this.state.open} toggle={this.toggle}>
{Title}
</Tooltip>
</CardTitle>
to this:
<CardTitle>
<LimitedTextTitle id={`movie-${imdbID}`}>
{`${Title} - (${Year})`}
</LimitedTextTitle>
<Tooltip placement='top' target={`movie-${imdbID}`} isOpen={this.state.open} toggle={this.toggle}>
{Title}
</Tooltip>
</CardTitle>
You can avoid using state by simply switching to UncontrolledTooltip which handles all the toggle itself without asking you to handle that explicitly, like:
<CardTitle>
<LimitedTextTitle id={`movie-${imdbID}`}>
{`${Title} - (${Year})`}
</LimitedTextTitle>
<UncontrolledTooltip placement='top' target={`movie-${imdbID}`}>
{Title}
</UncontrolledTooltip>
</CardTitle>
Rendering dynamic content in tooltip in react js is very simple.
Use ReactTooltip.
For full understanding check below example.
Here I am adding requestId in tooltip as dynamically.
{
completedTransactions.map((item, id) => (
<tr key={id + 1}>
<td>{id + 1}</td>
<td>
<span data-tip={item.requestId} data-for="registerTip">
{item.TransactionId}
</span>
<ReactTooltip id="registerTip" place="top" />
</td>
<td>{item.groupName}</td>
<td>{item.purposeName}</td>
<td>{dateFormat(item.update, "dd-mm-yyyy hh:mm tt")}</td>
</tr>
));
}

Resources