I created a page that displays goods, next to every good there is a button with which you can select or deselect the good. My problem is, when I click on any of my buttons, all buttons change. How can I make it so that the callback function responsible for changing the state of the button only applies to the one that has been clicked?
import React from 'react';
import { useState } from 'react';
import 'bulma/css/bulma.css';
import './App.scss';
export const goods = [
'Dumplings',
'Carrot',
'Eggs',
'Ice cream',
'Apple',
'Bread',
'Fish',
'Honey',
'Jam',
'Garlic',
];
export const App: React.FC = () => {
const [selected, selectGood] = useState(true);
const [selectedGood, changeSelectedGood] = useState(goods[0])
const handleChange = (button: any) => {
changeSelectedGood(button.target.id);
return selectGood(!selected);
}
const clearAll = () => {
return selectGood(!selected);
}
return (
<main className="section container">
{!selected && (
<h1 className="title">No goods selected</h1>
)}
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
{selected && (
<h1 className="title is-flex is-align-items-center">
{selectedGood + ' is selected'}
<button
data-cy="ClearButton"
type="button"
className="delete ml-3"
onClick={clearAll}
/>
</h1>
)}
<table className="table">
<tbody>
{goods.map((good) => (
<tr
data-cy="Good"
key={good}
className={selected
? "has-background-success-light"
: ""
}
>
<td>
{good && (
<button
data-cy="AddButton"
type="button"
className={"button"}
id={good}
onClick={(good) => {handleChange(good)}}
>
{selected ? '-' : '+'}
</button>
)}
</td>
<td
data-cy="GoodTitle"
className="is-vcentered"
>
{good}
</td>
</tr>
))}
</tbody>
</table>
</main>
)
}
What you basically need is to have a state in which you can maintain the checked status and the value, on clicking the button, map over the status and update the status of element, that has been clicked by the user (in order to check which element user has clicked you either compare values or keep additional field such as id in the array). Below is a simpler(in sense of code) implementation of the same issue.
import * as React from 'react';
import './style.css';
import { useState } from 'react';
export default function App() {
const [goods, setGoods] = useState([
{ status: false, name: 'Dumplings' },
{ status: false, name: 'carrot' },
]);
const onClick = (event) => {
setGoods(
goods.map((item) => {
if (item === event.target.name)
return { ...item, status: !item.status };
else return item;
})
);
};
return (
<div>
{goods.map((good, idx) => {
return (
<div key={idx}>
<input
value={good.name}
type={'checkbox'}
onClick={onClick}
name={good.name}
/>
<label htmlFor={good.name}> {good.name}</label>
</div>
);
})}
</div>
);
}
Why you don't do like this:
import { useState } from 'react'
import 'bulma/css/bulma.css'
import './App.scss'
export const goods = [ 'Dumplings', 'Carrot', 'Eggs', 'Ice cream', 'Apple', 'Bread', 'Fish', 'Honey', 'Jam', 'Garlic']
const App = () => {
const [selectedGoods, setSelectedGoods] = useState<string[]>([])
const clearAll = () => {
return setSelectedGoods([])
}
const handleSelectGood = (good: string) => {
setSelectedGoods(
(selectedGoods: string[]) => selectedGoods.includes(good)
? selectedGoods.filter((selectedGood) => selectedGood !== good)
: [...selectedGoods, good]
)
}
return (
<main className="section container">
{/* header */}
{selectedGoods.length < 1
? <h1 className="title">No goods selected</h1>
: (
<h1 className="title is-flex is-align-items-center">
{selectedGoods.join(', ') + ' is selected'}
<button
onClick={clearAll}
className="delete ml-3"
type="button"
data-cy="ClearButton"
/>
</h1>
)}
{/* table */}
<table className="table">
<tbody>
{goods.map((good) => {
const isSelected = selectedGoods.includes(good)
return (
<tr
key={good}
className={isSelected ? "has-background-success-light" : ""}
data-cy="Good"
>
<td>
<button
id={good}
onClick={() => handleSelectGood(good)}
className={"button"}
data-cy="AddButton"
>
{isSelected ? '-' : '+'}
</button>
</td>
<td
className="is-vcentered"
data-cy="GoodTitle"
>
{good}
</td>
</tr>
)
})}
</tbody>
</table>
</main>
)
}
export default App
Related
I know this is common question and something is wrong with state, but I still need help with understanding of all features of Redux and Redux-toolkit. So, in my PET project I'm trying to edit an invoice, but UI isn't updating, however it logs to the console changes which you make (here is screenshot).
And if I try to edit items(item name or unit costs or unit) it shows an error.
It's kind of 2 problems in one post, but related to the same topic :)
Now the code.
invoice-slice.js file with editInvoice reducer where might be the problem of updating the state, but i don't know where it can be:
editInvoice(state) {
const existingItem = state.invoices;
existingItem.map((item) => {
if (existingItem.id === item.id) {
return {
id: item.id,
bill_from: item.billFrom,
bill_from_info: item.billFromInfo,
bill_to: item.billTo,
bill_to_info: item.billToInfo,
invoice_num: item.invoiceNumber,
status: item.status,
order_date: item.order_date,
ITEMS: [...item.ITEMS],
};
}
return item;
});
},
EditInvoice.js file:
import React from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import classes from "./EditInvoice.module.css";
import EditInvoiceItem from "./EditInvoiceItem";
const EditInvoice = () => {
const { invoiceId } = useParams();
const invoices = useSelector((state) => state.invoice.invoices);
const invoice = invoices.find((invoice) => invoice.id === invoiceId);
return invoice ? (
<EditInvoiceItem
invoiceNumber={invoice.invoice_num}
billFrom={invoice.bill_from}
billFromInfo={invoice.bill_from_info}
billTo={invoice.bill_to}
billToInfo={invoice.bill_to_info}
status={invoice.status}
orderDate={invoice.order_date}
items={invoice.ITEMS}
itemName={invoice.item_name}
unitCosts={invoice.unit_costs}
units={invoice.units}
/>
) : (
<div className={classes.centered}>Invoice Not Found.</div>
);
};
export default EditInvoice;
EditInvoiceItem.js file:
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { uiActions } from "../../store/ui-slice";
import classes from "./AddInvoiceItem.module.css";
import { useFormik } from "formik";
import Wrapper from "../../UI/Wrapper";
import Card from "../../UI/Card";
import Footer from "../../UI/Footer";
import Button from "../../UI/Button";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faCalendar } from "#fortawesome/free-solid-svg-icons";
import { faEllipsis } from "#fortawesome/free-solid-svg-icons";
import { invoiceActions } from "../../store/invoice-slice";
import { useNavigate } from "react-router-dom";
const EditInvoiceItem = (props) => {
const navigate = useNavigate();
const date = new Date();
const options = ["Pending", "Shipped", "Delivered"];
const inputs = [{ item_name: "", unit_costs: "", unit: "" }];
const [startDate, setStartDate] = useState(date);
// const [startDate, setStartDate] = useState(props.orderDate || date);
const [selectedOption, setSelectedOption] = useState(
props.status || options[0]
);
const [listItems, setListItems] = useState(props.items || inputs);
// console.log(props.orderDate.toJSON());
const optionClickHandler = (value) => () => {
setSelectedOption(value);
dispatch(uiActions.toggleMoreOptions());
};
const editInvoiceHandler = (invoice) => {
console.log(invoice);
dispatch(
invoiceActions.editInvoice({
id: invoice.id,
invoiceNumber: invoice.invoiceNumber,
billFrom: invoice.billFrom,
billFromInfo: invoice.billFromInfo,
billTo: invoice.billTo,
billToInfo: invoice.billToInfo,
status: selectedOption,
order_date: startDate.toLocaleDateString(),
ITEMS: [...updateValuesOnSubmit()],
})
);
};
const formikEditInvoice = useFormik({
initialValues: {
invoiceNumber: props.invoiceNumber,
billFrom: props.billFrom,
billFromInfo: props.billFromInfo,
billTo: props.billTo,
billToInfo: props.billToInfo,
status: props.status,
order_date: props.orderDate,
item_name: props.itemName,
unit_costs: props.unitCosts,
units: props.units,
},
onSubmit: (val) => {
editInvoiceHandler(val);
navigate("/invoices", { replace: true });
},
});
const dispatch = useDispatch();
const toggleMoreOptions = () => {
dispatch(uiActions.toggleMoreOptions());
};
const showOtherOptions = useSelector(
(state) => state.ui.selectMoreOptionsIsVisible
);
let counter = 1;
const addItemHandler = () => {
setListItems(listItems.concat({ item_name: "", unit_costs: "", unit: "" }));
};
const updateItemHandler = (index, inputName, value) => {
listItems[index] = { ...listItems[index], [inputName]: value };
};
const updateValuesOnSubmit = () => {
return listItems;
};
const navigateBack = () => {
navigate(-1);
};
return (
<form onSubmit={formikEditInvoice.handleSubmit}>
<Wrapper isShrinked={props.isShrinked}>
<Card>
<div className={classes.content}>
<div className={classes["buttons-wrapper"]}>
<button
type="button"
className={classes["cancel-btn"]}
onClick={navigateBack}
>
Cancel
</button>
<Button>Save</Button>
</div>
<div className={classes["invoice-info-wrapper"]}>
<div className={classes["invoice-info"]}>
<h3>Invoice Info</h3>
<input
placeholder="Number"
type="text"
name="invoiceNumber"
id="invoiceNumber"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.invoiceNumber}
onBlur={formikEditInvoice.handleBlur}
></input>
</div>
<div className={classes["right-side-column"]}>
<div className={classes["order-status"]}>
<span>Order Status: </span>
<div className={classes.buttons}>
{showOtherOptions && (
<ul className={classes.options}>
{options.map((option, index) => (
<li onClick={optionClickHandler(option)} key={index}>
{option}
</li>
))}
</ul>
)}
<button type="button" className={classes.status}>
{selectedOption}
</button>
<button
type="button"
className={classes.dots}
onClick={toggleMoreOptions}
>
<FontAwesomeIcon icon={faEllipsis} />
</button>
</div>
</div>
<div className={classes["order-date"]}>
<span>Order Date:</span>
<DatePicker
className={classes["order-date-input"]}
selected={startDate}
onChange={(val) => setStartDate(val)}
/>
<FontAwesomeIcon
icon={faCalendar}
className={classes.calendar}
></FontAwesomeIcon>
</div>
</div>
</div>
<div className={classes["order-bills"]}>
<div className={classes["bill-from"]}>
<input
placeholder="Bill From"
type="text"
name="billFrom"
id="billFrom"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billFrom}
onBlur={formikEditInvoice.handleBlur}
></input>
<textarea
placeholder="Bill From Info"
name="billFromInfo"
id="billFromInfo"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billFromInfo}
onBlur={formikEditInvoice.handleBlur}
></textarea>
</div>
<div className={classes["bill-to"]}>
<input
placeholder="Bill To"
type="text"
name="billTo"
id="billTo"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billTo}
onBlur={formikEditInvoice.handleBlur}
></input>
<textarea
placeholder="Bill To Info"
name="billToInfo"
id="billToInfo"
onChange={formikEditInvoice.handleChange}
value={formikEditInvoice.values.billToInfo}
onBlur={formikEditInvoice.handleBlur}
></textarea>
</div>
</div>
<div className={classes["table-wrapper"]}>
<table>
<colgroup>
<col className={classes.col1}></col>
<col className={classes.col2}></col>
<col className={classes.col3}></col>
<col className={classes.col4}></col>
<col className={classes.col5}></col>
<col className={classes.col6}></col>
</colgroup>
<thead>
<tr>
<td className={classes["more-padding"]}>#</td>
<td>Item Name</td>
<td>Unit Costs</td>
<td>Unit</td>
<td>Price</td>
<td></td>
</tr>
</thead>
<tbody>
{listItems.map((item, index) => (
<tr data-1={item} key={index}>
<td className={classes["more-padding"]}>{counter++}</td>
<td>
<input
placeholder="Item Name"
className={classes.inputs}
name="itemName"
id="itemName"
onChange={(e) =>
updateItemHandler(
index,
"item_name",
e.currentTarget.value
)
}
value={item.item_name}
onBlur={formikEditInvoice.handleBlur}
></input>
</td>
<td>
<input
placeholder="Unit Costs"
className={classes.inputs}
name="unitCosts"
id="unitCosts"
onChange={(e) =>
updateItemHandler(
index,
"unit_costs",
e.currentTarget.value
)
}
value={item.unit_costs}
onBlur={formikEditInvoice.handleBlur}
></input>
</td>
<td>
<input
placeholder="Unit"
className={classes.inputs}
name="unit"
id="unit"
onChange={(e) =>
updateItemHandler(
index,
"unit",
e.currentTarget.value
)
}
value={item.unit}
onBlur={formikEditInvoice.handleBlur}
></input>
</td>
<td>0</td>
<td></td>
{/* There should be dynamic values later */}
</tr>
))}
</tbody>
</table>
<div className={classes["add-item-btn"]}>
<button
onClick={addItemHandler}
type="button"
className={classes["add-item-btn"]}
>
Add Item
</button>
</div>
<div className={classes.total}>
<p className={classes["sub-total"]}>
<span>Sub Total: </span>
<span>$0</span>
{/* Dynamic value later here */}
</p>
<div className={classes["total-vat"]}>
<span>Total Vat:</span>
<div className={classes["total-sum"]}>
<span className={classes["input-wrapper"]}>
<input type="text" defaultValue="10"></input>
<span>%</span>
</span>
<span className={classes.sum}>$0</span>
{/* Dynamic value later here */}
</div>
</div>
<div className={classes["grand-total"]}>
<h3>Grand Total</h3>
<div className={classes.input}>
<input type="text" defaultValue="$"></input>
<span>0</span>
{/* Dynamic value later here */}
</div>
</div>
</div>
</div>
<div className={classes.dummy}></div>
</div>
</Card>
<Footer />
</Wrapper>
</form>
);
};
export default EditInvoiceItem;
In my project I'm using Formik to listen for input changes, but in listening to items changes I'm not using it, because i had an issue with that and one guy suggested me that code.
So, I'm facing 2 issues:
UI doesn't update the changes, but i can see in console that changes are made(probably the code in invoice-slice.js is wrong);
When i click and want to change the inputs of Item Name, Unit Costs and Unit i get an error in console and can't change anything.
Please, try helping me and explaining what can cause such problems!
P.S. here is my github repo - https://github.com/stepan-slyvka/test-project
P.P.S. and here is CodeSandbox -
Issues
The main issue is that the editInvoice reducer function isn't consuming an action and its payload, so the passed updated invoice data isn't referenced, and the array mapping result isn't used.
Additionally, the EditInvoice component isn't passing along the invoice id to the EditInvoiceItem (*or EditInvoiceItem isn't capturing it from the path parameters. The invoice.id isn't populated into the form data so it's also not passed along to editInvoiceHandler to be dispatched to the store. This causes the array mapping to fail to find the invoice object that needs to be updated.
Solution
EditInvoice
EditInvoice needs to pass invoice.id as a prop to EditInvoiceItem. It would be quite a bit more clean and easier to maintain to simply pass the entire invoice object though.
const EditInvoice = () => {
const { invoiceId } = useParams();
const invoices = useSelector((state) => state.invoice.invoices);
const invoice = invoices.find((invoice) => invoice.id === invoiceId);
return invoice ? (
<EditInvoiceItem
id={invoice.id} // <-- passed here
invoiceNumber={invoice.invoice_num}
billFrom={invoice.bill_from}
billFromInfo={invoice.bill_from_info}
billTo={invoice.bill_to}
billToInfo={invoice.bill_to_info}
status={invoice.status}
orderDate={invoice.order_date}
items={invoice.ITEMS}
itemName={invoice.item_name}
unitCosts={invoice.unit_costs}
units={invoice.units}
/>
) : (
<div className={classes.centered}>Invoice Not Found.</div>
);
};
EditInvoiceItem
EditInvoiceItem should initialize the form state with the invoice id.
const formikEditInvoice = useFormik({
initialValues: {
id: props.id, // <-- pass id value...
invoiceNumber: props.invoiceNumber,
billFrom: props.billFrom,
billFromInfo: props.billFromInfo,
billTo: props.billTo,
billToInfo: props.billToInfo,
status: props.status,
order_date: props.orderDate,
item_name: props.itemName,
unit_costs: props.unitCosts,
units: props.units,
},
onSubmit: (val) => {
editInvoiceHandler(val); // <-- ...so it's passed along here
navigate("/invoices", { replace: true });
},
});
invoice-slice
The editInvoice case reducer should consume both the current state and the dispatched action so the payload containing the invoice data can be accessed.
editInvoice(state, action) {
const { payload } = action;
state.invoices = state.invoices.map((item) =>
item.id === payload.id
? {
...item,
bill_from: payload.billFrom,
bill_from_info: payload.billFromInfo,
bill_to: payload.billTo,
bill_to_info: payload.billToInfo,
invoice_num: payload.invoiceNumber,
status: payload.status,
order_date: payload.order_date,
ITEMS: payload.ITEMS.slice()
}
: item
);
}
i'm new to reactjs and i'm trying to make a table that shows the information from a array of objects and have a button of delete and an input to search among the users. The delete button is working correctly when i'm not searching anything, but when i'm searching it doesn't delete the corretly row, and deletes only the first one. I see that it is because the arrays that show the table are different with and without the search being used but I don't know how to make it work.
this is the component of the table:
import { formatDate } from "../../utils/formatDate";
import "./table.css";
import { useState } from "react";
function Table(props) {
const { headerData, bodyData, type, removeItem} = props;
const isUser = type === "user";
const buildTableItems = () => {
return bodyData.map((item, index) => (
<tr className="data-tr">
<td>{item.name}</td>
<td>{item.email}</td>
<td>{item.occupation}</td>
<td>{formatDate(item.birthday)}</td>
<td>
<button className="delete-button" onClick={() => removeItem(index)}>
Delete
</button>
</td>
</tr>
));
};
return (
<div className="user-data">
<table className="user-table">
<thead>
<tr className="data-th">
{headerData.map((headerTable) => (
<th >{headerTable}</th>
))}
</tr>
</thead>
<tbody>{buildTableItems()}</tbody>
</table>
</div>
);
}
export default Table;
Here the component of the search bar:
import "./searchBar.css"
function SearchBar({ searchedData, onSearch }) {
return (
<div className="search-bar">
<label>Search</label>
<input type="text" placeholder="Search User" value={searchedData} onChange={e => onSearch(e.target.value)} />
</div>
);
}
export default SearchBar;
and here is the home:
import "./Home.css";
import React, { useEffect, useState } from "react";
import Header from "../components/Header/Header";
import Table from "../components/Table/Table";
import AddData from "../components/AddData/AddData";
import SearchBar from "../components/SearchBar/SearchBar";
import { userArr } from "../mock/users";
const Home = () => {
const headerUser = ["Name", "Email", "Occupation", "Birthday"];
const [newUserArr, setNewUserArr] = useState(userArr);
const [searchedItem, setSearchedItem] = useState("");
const searchedArray = newUserArr.filter((item) => {
if (item.name.toLowerCase().includes(searchedItem.toLowerCase())) {
return true;
}
});
function onSearch(e) {
setSearchedItem(e);
}
const addDataToArr = (form) => {
setNewUserArr([...newUserArr, form]);
};
const deleteData = (indexUserArr) => {
let restOfDataArray = newUserArr.filter(
(element, ind) => ind !== indexUserArr
);
setNewUserArr(restOfDataArray);
};
return (
<>
<Header />
<SearchBar searchedData={searchedItem} onSearch={onSearch} />
<Table
type="user"
headerData={headerUser}
bodyData={newUserArr}
removeItem={(index) => deleteData(index)}
/>
<AddData saveData={(val) => addDataToArr(val)} />
</>
);
};
export default Home;
thank you
If you have ID in your user data then use that instead of index or create id keywords using concatenate with your values here is examples.
import { formatDate } from "../../utils/formatDate";
import "./table.css";
import { useState } from "react";
function Table(props) {
const { headerData, bodyData, type, removeItem} = props;
const isUser = type === "user";
const buildTableItems = () => {
return bodyData.map((item, index) => (
<tr className="data-tr">
<td>{item.name}</td>
<td>{item.email}</td>
<td>{item.occupation}</td>
<td>{formatDate(item.birthday)}</td>
<td>
<button className="delete-button" onClick={() => removeItem(`${item.name}${item.email}${item.occupation}`)}>
Delete
</button>
</td>
</tr>
));
};
return (
<div className="user-data">
<table className="user-table">
<thead>
<tr className="data-th">
{headerData.map((headerTable) => (
<th >{headerTable}</th>
))}
</tr>
</thead>
<tbody>{buildTableItems()}</tbody>
</table>
</div>
);
}
export default Table;
And here is your delete method ${item.name}${item.email}${index}
const deleteData = (data) => {
let restOfDataArray = newUserArr.filter(
(element, ind) => `${element.name}${element.email}${element.occupation}` !== data
);
setNewUserArr(restOfDataArray);
};
This will fixed your problem. If this doesn't work then you need to use ID to resolve this problem. There is a possibility that ${item.name}${item.email}${item.occupation} can be duplicates.
Never use index ever for deleting or any other operations. Use always ID.
I have a component A, which displays contents of a component B conditionally. Component B is contains a list of items, and when one clicks one of the items in the list, a new layout is supposed to be fired up showing details of the item. When i try to pass the props to switch to a new layout on a component B list item, i get an error toggleSearchType is not a function . Any assistance or recommendation on what i might be doing wrong will be appreciated.
My index file looks like this :
const PatientSearch: React.FC<PatientSearchProps> = ({ closePanel }) => {
const { t } = useTranslation();
const [searchType, setSearchType] = useState<SearchTypes>(SearchTypes.BASIC);
const toggleSearchType = (searchType: SearchTypes) => {
setSearchType(searchType);
};
return (
<>
<Overlay header={t('addPatientToList', 'Add patient to list')} closePanel={closePanel}>
<div className="omrs-main-content">
{searchType === SearchTypes.BASIC ? (
<BasicSearch toggleSearchType={toggleSearchType} />
) : searchType === SearchTypes.ADVANCED ? (
<PatientScheduledVisits toggleSearchType={toggleSearchType} />
) : searchType === SearchTypes.SCHEDULED_VISITS ? (
<AdvancedSearch toggleSearchType={toggleSearchType} />
) : null}
</div>
</Overlay>
</>
);
};
The searchtypes are as below :
export enum SearchTypes {
BASIC = 'basic',
ADVANCED = 'advanced',
SCHEDULED_VISITS = 'scheduled-visits'
}
My component A looks like this :
import React, { useEffect, useMemo, useState } from 'react';
interface BasicSearchProps {
toggleSearchType: (searchMode: SearchTypes) => void;
}
const BasicSearch: React.FC<BasicSearchProps> = ({ toggleSearchType }) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<any>(null);
const customRepresentation = '';
return (
<div className={searchResults?.length ? styles.lightBackground : styles.resultsContainer}>
{searchResults?.length ? (
<div className={styles.resultsContainer}>{<SearchResults toggleSearchType={searchResults} patients={searchResults} />}</div>
) : (
<div>
<div className={styles['text-divider']}>{t('or', 'Or')}</div>
<div className={styles.buttonContainer}>
<Button
kind="ghost"
iconDescription="Advanced search"
renderIcon={Search16}
onClick={() => toggleSearchType(SearchTypes.ADVANCED)}>
{t('advancedSearch', 'Advanced search')}
</Button>
</div>
</div>
)}
</div>
);
};
export default BasicSearch;
Component B looks like this :
interface SearchResultsProps {
patients: Array<any>;
hidePanel?: any;
toggleSearchType: (searchMode: SearchTypes) => void;
}
function SearchResults({ patients, toggleSearchType }: SearchResultsProps ) {
const fhirPatients = useMemo(() => {
return patients.map((patient) => {
const preferredAddress = patient.person.addresses?.find((address) => address.preferred);
});
}, [patients]);
return (
<>
{fhirPatients.map((patient) => (
<div key={patient.id} className={styles.patientChart} onClick={() => toggleSearchType(SearchTypes.SCHEDULED_VISITS)} >
<div className={styles.container}>
<ExtensionSlot
extensionSlotName="patient-header-slot"
state={{
patient,
patientUuid: patient.id,
// onClick: onClickSearchResult,
}}
/>
</div>
</div>
))}
</>
);
}
}
I've been scratching my head over this for ages now.
I am trying to change the background colour of a specific button that is in a nested array.
I have an array of names in pairs that I loop over twice using a map, once to get the pair and once again to get the value. I output and assign the values to a button for each and am displaying the pairs together (E.g. each pair is indexed 0 and 1).
When I click on the button I wish to change only the background colour of the selected button. Currently all the buttons change colour. The issue being is that the state of the buttons effects all of them when I use a boolean to define the selection.
The handler I am using to do this also adds the value of the button to an array to be passed into global state later on as well.
Any help with this would be greatly greatly appreciated as I can't seem to find a way past it. Thanks!
import React, { Component } from "react";
import "../../App.scss";
import { Link } from "react-router-dom";
import Button from "../Button/Button";
class Matches extends Component {
constructor(props) {
super(props);
this.state = {
champ: [],
winningPlayers: [],
selected: false,
};
this.handleAddWinners = this.handleAddWinners.bind(this);
this.handleRound = this.handleRound.bind(this);
}
// Adds winners to a local array which is then sent
// to the global array using the handleNextRound action.
handleAddWinners = (e) => {
const winner = e.target.value;
const { champ } = this.state;
const { round } = this.props;
if (round !== 3) {
this.setState({
selected: !false,
winningPlayers: [...this.state.winningPlayers, winner],
});
} else {
this.setState({ champ: [...champ, winner] });
}
};
handleRound = () => {
const { round, handleNextRound, handleChampion } = this.props;
round !== 3 ? handleNextRound(this.state) : handleChampion(this.state);
this.setState({ winningPlayers: [] });
};
render() {
const { pairs, round, handleClear, roundWinners, champion } = this.props;
const { winningPlayers, selected, champ } = this.state;
const semi = roundWinners[0];
const final = roundWinners[1];
const champName = champion.map((item) => item);
const reset =
round !== 4 ? "block__reset__tournament" : "block__reset__new-game";
const newGame = `${round !== 4 ? "Reset" : "New Game?"}`;
const buttonClick = `${selected ? "selected" : "block__player"}`;
return (
<>
<div classname="container__wrapper">
<div className="container__tournament">
{round === 1 ? (
<section className="block__round ">
{pairs.map((item, index) => (
<div className="pairs" key={index}>
{item.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e)}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 2 ? (
<section className="block__round ">
{semi.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 3 ? (
<section className="block__round ">
{final.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : (
<section className="block__champion">
<p className="champion__greeting">
Congratulations
<br />
<span className="champion__name">{champName}!</span>
<br /> You've won the whole shebang!
</p>
</section>
)}
<Button
buttonClass={`${
round !== 4 ? "block__next-round" : "button__notActive"
}`}
label={`${round !== 3 ? "Next Round" : "See Winner"}`}
handleClick={this.handleRound}
disabled={disabled}
/>
<Link to={"/"} className={reset}>
<Button
buttonClass={reset}
handleClick={handleClear}
label={newGame}
/>
</Link>
</div>
</div>
</>
);
}
}
export default Matches;
This is the component that is handling most of this.
First I would like to say that you should always avoid from using array's index as keys. That is, unless your array is always at the same size and order.
Having said that - what you want to do is to know which button was selected - right?
So you need to store the last button that was selected. Because you don't use any ids anywhere, you can use the index of the pair and the index of the button to know which button was clicked.
Here's an example - I've changed only the round1 and the state code.
import React, { Component } from "react";
import "../../App.scss";
import { Link } from "react-router-dom";
import Button from "../Button/Button";
class Matches extends Component {
constructor(props) {
super(props);
this.state = {
champ: [],
winningPlayers: [],
selected: null,
};
this.handleAddWinners = this.handleAddWinners.bind(this);
this.handleRound = this.handleRound.bind(this);
}
// Adds winners to a local array which is then sent
// to the global array using the handleNextRound action.
handleAddWinners = (e, pairIndex, itemIndex) => {
const winner = e.target.value;
const { champ } = this.state;
const { round } = this.props;
if (round !== 3) {
this.setState({
selected: `${pairIndex}-${itemIndex}`,
winningPlayers: [...this.state.winningPlayers, winner],
});
} else {
this.setState({ champ: [...champ, winner] });
}
};
handleRound = () => {
const { round, handleNextRound, handleChampion } = this.props;
round !== 3 ? handleNextRound(this.state) : handleChampion(this.state);
this.setState({ winningPlayers: [] });
};
render() {
const { pairs, round, handleClear, roundWinners, champion } = this.props;
const { winningPlayers, selected, champ } = this.state;
const semi = roundWinners[0];
const final = roundWinners[1];
const champName = champion.map((item) => item);
const reset =
round !== 4 ? "block__reset__tournament" : "block__reset__new-game";
const newGame = `${round !== 4 ? "Reset" : "New Game?"}`;
const buttonClick = `${selected ? "selected" : "block__player"}`;
return (
<>
<div classname="container__wrapper">
<div className="container__tournament">
{round === 1 ? (
<section className="block__round ">
{pairs.map((item, pairIndex) => (
<div className="pairs" key={pairIndex}>
{item.map((names, itemIndex) => (
<Button
key={itemIndex}
handleClick={(e) => this.handleAddWinners(e, pairIndex, itemIndex)}
label={names}
buttonClass={`${pairIndex}-${itemIndex}` === selected ? '<enterYourBackgroundClass' : buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 2 ? (
<section className="block__round ">
{semi.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : round === 3 ? (
<section className="block__round ">
{final.map((names, index) => (
<div className="pairs" key={index}>
{names.map((names, index) => (
<Button
key={index}
handleClick={(e) => this.handleAddWinners(e, "value")}
label={names}
buttonClass={buttonClick}
value={names}
/>
))}
</div>
))}
</section>
) : (
<section className="block__champion">
<p className="champion__greeting">
Congratulations
<br />
<span className="champion__name">{champName}!</span>
<br /> You've won the whole shebang!
</p>
</section>
)}
<Button
buttonClass={`${
round !== 4 ? "block__next-round" : "button__notActive"
}`}
label={`${round !== 3 ? "Next Round" : "See Winner"}`}
handleClick={this.handleRound}
disabled={disabled}
/>
<Link to={"/"} className={reset}>
<Button
buttonClass={reset}
handleClick={handleClear}
label={newGame}
/>
</Link>
</div>
</div>
</>
);
}
}
export default Matches;
so I have two buttons in my react App.js and when clicked I want my current state(list) to change to descending order according to which button i press(order by date or order by upvotes). My articles.js have the code that display the list of articles. But I'm having a hard time showing the list sorted after clicking the button tag found on my App.js which is the parent component.
import React, { useState } from 'react';
function Articles({articles}) {
const [list, setList] = useState(articles)
return (
<div className="card w-50 mx-auto">
<table>
<thead>
<tr>
<th>Title</th>
<th>Upvotes</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{list.map((a, i) =>
<tr data-testid="article" key={i}>
<td data-testid="article-title">{a.title}</td>
<td data-testid="article-upvotes">{a.upvotes}</td>
<td data-testid="article-date">{a.date}</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
export default Articles;
import React from 'react';
import './App.css';
import 'h8k-components';
import Articles from './components/Articles';
const title = "Sorting Articles";
function App({articles}) {
//set article to state then pass
const handleUpvotes = () => {
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
console.log(articles)
}
const handleDates = () => {
return
}
return (
<div className="App">
<h8k-navbar header={title}></h8k-navbar>
<div className="layout-row align-items-center justify-content-center my-20 navigation">
<label className="form-hint mb-0 text-uppercase font-weight-light">Sort By</label>
<button data-testid="most-upvoted-link" className="small" onClick={handleUpvotes}>Most Upvoted</button>
<button data-testid="most-recent-link" className="small" onClick={handleDates}>Most Recent</button>
</div>
<Articles articles={articles}/>
</div>
);
}
export default App;
The useState should be in the App
const [list, setList] = useState(articles)
//set article to state then pass
const handleUpvotes = () => {
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
setList(articles)
}
You should use the Effect Hook (https://reactjs.org/docs/hooks-effect.html).
useEffect(() => {
// articles was changed
}, [articles])
the problem that you are facing is that a misunderstanding of the React reactivity model, now lets take a look at this line
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
here you are successfully updating the array, but think about it. if React updated the UI whenever a variables inside the component updates that would be ineffective and problematic.
so in order to notify React about what has changed and it needs to update the UI, whenever you change a variable and you need the UI to update, you use useState from react.
and another point is that in your Article component you are expecting props, and calling useState at the time.
so moving the useState into the App component dose the work
const [list, setList] = useState(articles)
const handleUpvotes = () => {
articles.sort((a, b) => a.upvotes - b.upvotes).reverse()
setList(articles)
}
It is not clear where articles come from and if they need to be used in multiple components so I'll put them in context, that way you can use it anywhere in your application.
const ArticleContext = React.createContext();
const ArticleProvider = ({ children }) => {
const [articles, setArticles] = React.useState([
{ title: '1', upvotes: 1, date: 1 },
{ title: '3', upvotes: 3, date: 3 },
{ title: '2', upvotes: 2, date: 2 },
{ title: '4', upvotes: 4, date: 4 },
]);
const sortDirection = React.useRef(-1);
const sortByUpvotes = React.useCallback(() => {
//toggle sort direction
sortDirection.current = sortDirection.current * -1;
setArticles((articles) =>
[...articles].sort(
(a, b) =>
(a.upvotes - b.upvotes) * sortDirection.current
)
);
}, [setArticles]);
return (
<ArticleContext.Provider
value={{
articles,
sortByUpvotes,
}}
>
{children}
</ArticleContext.Provider>
);
};
function Articles() {
const { articles } = React.useContext(ArticleContext);
return (
<div className="card w-50 mx-auto">
<table>
<thead>
<tr>
<th>Title</th>
<th>Upvotes</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{articles.map((a, i) => (
<tr data-testid="article" key={i}>
<td data-testid="article-title">{a.title}</td>
<td data-testid="article-upvotes">
{a.upvotes}
</td>
<td data-testid="article-date">{a.date}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function App() {
const { sortByUpvotes } = React.useContext(
ArticleContext
);
return (
<div className="App">
<div className="layout-row align-items-center justify-content-center my-20 navigation">
<label className="form-hint mb-0 text-uppercase font-weight-light">
Sort By
</label>
<button
data-testid="most-upvoted-link"
className="small"
onClick={sortByUpvotes}
>
Most Upvoted
</button>
</div>
{/* no need to pass articles, they are in context */}
<Articles />
</div>
);
}
ReactDOM.render(
<ArticleProvider>
<App />
</ArticleProvider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The next example shows how to sort using multiple fields:
const ArticleContext = React.createContext();
const ArticleProvider = ({ children }) => {
const [articles, setArticles] = React.useState([
{ title: '1', upvotes: 1, date: 3 },
{ title: '3', upvotes: 3, date: 3 },
{ title: '2', upvotes: 2, date: 4 },
{ title: '4', upvotes: 4, date: 2 },
]);
const sortDirection = React.useRef([-1, -1]);
const sortPriority = React.useRef([0, 1]);
const sortFunctions = React.useMemo(
() => [
(a, b) =>
(a.upvotes - b.upvotes) * sortDirection.current[0],
(a, b) =>
(a.date - b.date) * sortDirection.current[1],
],
[]
);
const sort = React.useCallback(() => {
setArticles((articles) =>
[...articles].sort((a, b) =>
sortPriority.current.reduce(
(result, fnIndex) =>
result === 0
? sortFunctions[fnIndex](a, b)
: result,
0
)
)
);
}, [sortFunctions]);
const setDirectionAndPriority = (num) => {
if (sortPriority.current[0] === num) {
sortDirection.current[num] =
sortDirection.current[num] * -1;
}
sortPriority.current = [
num,
...sortPriority.current.filter((n) => n !== num),
];
};
const sortByUpvotes = () => {
setDirectionAndPriority(0);
sort();
};
const sortByDate = () => {
setDirectionAndPriority(1);
sort();
};
return (
<ArticleContext.Provider
value={{
articles,
sortByUpvotes,
sortByDate,
}}
>
{children}
</ArticleContext.Provider>
);
};
function Articles() {
const { articles } = React.useContext(ArticleContext);
return (
<div className="card w-50 mx-auto">
<table>
<thead>
<tr>
<th>Title</th>
<th>Upvotes</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{articles.map((a, i) => (
<tr data-testid="article" key={i}>
<td data-testid="article-title">{a.title}</td>
<td data-testid="article-upvotes">
{a.upvotes}
</td>
<td data-testid="article-date">{a.date}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function App() {
const { sortByUpvotes, sortByDate } = React.useContext(
ArticleContext
);
return (
<div className="App">
<div className="layout-row align-items-center justify-content-center my-20 navigation">
<label className="form-hint mb-0 text-uppercase font-weight-light">
Sort By
</label>
<button
data-testid="most-upvoted-link"
className="small"
onClick={sortByUpvotes}
>
Most Upvoted
</button>
<button
data-testid="most-recent-link"
className="small"
onClick={sortByDate}
>
Most Recent
</button>
</div>
{/* no need to pass articles, they are in context */}
<Articles />
</div>
);
}
ReactDOM.render(
<ArticleProvider>
<App />
</ArticleProvider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>