I am changing the text colour in a list when the item is clicked. Ideally, I would like to have a toggle function so only 1 item is highlighted at a time. The following code works however I can't stop thinking there is a much better way.
import React, { useState } from "react";
const listItems = [
{
id: 1,
title: "About",
selected: true,
},
{
id: 2,
title: "Contact",
selected: false,
},
{
id: 3,
title: "Products",
selected: false,
},
];
const ListView = () => {
const [menuItems, setMenuItems] = useState(listItems);
const handleListClick = (id) => {
setMenuItems([...menuItems.map((item)=> {
if (item.id === id){
item.selected=!item.selected
}
return item
})])
};
return (
<>
<ul>
{listItems.map((item, index) => {
return (
<li
key={item.id}
onClick={() => handleListClick(item.id)}
style={item.selected ? { color: "red" } : { color: "Blue" }}
>
{item.title}
</li>
);
})}
</ul>
</>
);
};
export default ListView;
Any ideas on simplifying this...
You can use the array index to update the item only without traversing the array like this
import React, { useState } from "react";
const listItems = [
{
id: 1,
title: "About",
selected: true,
},
{
id: 2,
title: "Contact",
selected: false,
},
{
id: 3,
title: "Products",
selected: false,
},
];
const ListView = () => {
const [menuItems, setMenuItems] = useState(listItems);
const handleListClick = (index) => {
const items = [...menuItems];
items[index].selected = !items[index].selected;
setMenuItems(items);
};
return (
<>
<ul>
{listItems.map((item, index) => {
return (
<li
key={item.id}
onClick={() => handleListClick(index)}
style={item.selected ? { color: "red" } : { color: "Blue" }}
>
{item.title}
</li>
);
})}
</ul>
</>
);
};
export default ListView;
Related
I have an application where the user will select a seat and then will click reserve and the seats will be greyed out. For some reason my object array of seats are not updating in the array and the seats are not greying out. when I log the seating sometimes, the isReserved is true, and when I log it again it goes back to false.
Here is what the code looks like:
const seats: any[] = [
{ id: 1, isSelected: false, isReserved: false },
{ id: 2, isSelected: false, isReserved: false },
{ id: 3, isSelected: false, isReserved: false },
{ id: 4, isSelected: false, isReserved: true },
{ id: 5, isSelected: false, isReserved: false },
{ id: 6, isSelected: false, isReserved: false },
];
const Seatbooking = () => {
const [isSelected, setIsSelected] = useState(0);
const [seating, setSeating] = useState(seats);
function onSelected(select: any) {
console.log(select.id);
console.log("selected ", select);
setIsSelected(select.id);
console.log("it is selected ", select.id);
}
const onReserved = (id: any) => {
setSeating((seat) => {
return seat.map((item) => {
return item.id === id
? { ...item, isReserved: !item.isReserved }
: item;
});
});
};
return (
<>
<div className="grid-container">
{seats.map((seat) => (
<div style={{ width: "50%" }}>
<button
key={seat.id}
style={{
backgroundColor:
seat.isReserved === true
? "grey"
: seat.id === isSelected
? "red"
: "#2d95c9",
}}
className="seat_buttons"
onClick={() => onSelected(seat)}
>
{seat.id}
</button>
</div>
))}
</div>
<button className="seat_booking" onClick={() => onReserved(isSelected)}>
Reserve seat
</button>
</>
);
};
Working solution with fixed naming and optimised conditions.
import { useState } from 'react';
const seats = [
{ id: 1, isSelected: false, isReserved: false },
{ id: 2, isSelected: false, isReserved: false },
{ id: 3, isSelected: false, isReserved: false },
{ id: 4, isSelected: false, isReserved: true },
{ id: 5, isSelected: false, isReserved: false },
{ id: 6, isSelected: false, isReserved: false },
];
export const Seatbooking = () => {
const [selectedSeatId, setSelectedSeatId] = useState(0);
const [seating, setSeating] = useState(seats);
function onSelected(select) {
console.log(select.id);
console.log('selected ', select);
setSelectedSeatId(select.id);
console.log('it is selected ', select.id);
}
const onReserved = (id) => {
const updatedArr = seating.map((item) => {
return item.id === id ? { ...item, isReserved: !item.isReserved } : item;
});
setSeating(updatedArr);
};
return (
<>
<div className="grid-container">
{seating.map((seat) => (
<div key={seat.id} style={{ width: '50%', display: 'flex' }}>
<button
style={{
backgroundColor: seat.isReserved
? 'grey'
: seat.id === selectedSeatId
? 'red'
: '#2d95c9',
}}
className="seat_buttons"
onClick={() => onSelected(seat)}
>
{seat.id}
</button>
</div>
))}
</div>
<button className="seat_booking" onClick={() => onReserved(selectedSeatId)}>
Reserve seat
</button>
</>
);
};
You should work with state in your component, not with constant.
You do not need callback in your setSeating.
isSelected - name for boolean value. You store id - call it selectedSeatId.
Key should be on div, not on button.
seats.map((seat) => (
You're mapping over the original array, not your state variable. Change it to:
seating.map((seat) => (
As shown in the picture, I have created a similar collapsible table, but I am unable to display the number of items in the collapsible rows. I am referring to the headings written in blue colour (in the picture)
Can someone help me to understand, how I can display the name of first items followed by the number of remaining items in heading of each main row ( for eg ( basketball..+2). Also I am using api call to fetch data in the table
enter code here
export const columns = [
{
Header: () => (
<div />
),
accessor: 'name',
maxWidth: 300,
Cell: row => (
<div className='first-column'>{row.value}</div>
)
}
];
export const subComponent = row => {
return (
<div>
{row.original.types.map((type, id) => {
return (
<div className='subRow' key={ id }>{ type.name }</div>
);
})}
</div>
);
};
export const data = [
{
id: '12345',
name: 'sports',
types: [
{
name: 'basketball',
id: '1'
},
{
name: 'soccer',
id: '2'
},
{
name: 'baseball',
id: '3'
}
]
},
{
id: '678910',
name: 'food',
types: [
{
name: 'pizza',
id: '4'
},
{
name: 'hamburger',
id: '5'
},
{
name: 'salad',
id: '6'
}
]
}
]
First, your columns have to use useMemo hooks. You can read the details here.
const columns = useMemo(() => [
...
],[])
Next, change the types property into subRows in your data so the subRows rows can be auto rendered and no additional subRow component required.
const data = [
{
id: "12345",
name: "sports",
subRows: [ // <---- change types into subRows
{
name: "basketball",
id: "1",
},
...
],
},
...
];
The subRows informations is also required to get the remaining items in heading. Here, we grab and describe the information at column's Cell
{
Header: () => <div />,
accessor: "name",
maxWidth: 300,
Cell: ({ row }) => {
// we can get subRows info here after change types into subRows
const { subRows } = row,
// describe the required information here
isSubRow = subRows[0] ? false : true,
firstSubRowName = subRows[0] ? subRows[0].original.name : "",
remainingSubRowsLength = subRows[1] ? `..+${subRows.length - 1}` : "";
return (
<span>
{row.original.name}
// conditionally render the additional info
{!isSubRow && (
<span
style={{ color: "blue" }}
>{` (${firstSubRowName}${remainingSubRowsLength})`}</span>
)}
</span>
);
}
}
And the final code will become:
import { useMemo } from "react";
import { useTable, useExpanded } from "react-table";
const data = [
{
id: "12345",
name: "sports",
subRows: [
{
name: "basketball",
id: "1"
},
{
name: "soccer",
id: "2"
},
{
name: "baseball",
id: "3"
}
]
},
{
id: "678910",
name: "food",
subRows: [
{
name: "pizza",
id: "4"
},
{
name: "hamburger",
id: "5"
},
{
name: "salad",
id: "6"
}
]
}
];
export default function App() {
const columns = useMemo(
() => [
{
Header: () => null,
id: "expander",
Cell: ({ row }) => {
return (
<span {...row.getToggleRowExpandedProps()}>
{row.depth === 0 && (row.isExpanded ? "▼" : "►")}
</span>
);
}
},
{
Header: () => <div />,
accessor: "name",
maxWidth: 300,
Cell: ({ row }) => {
const { subRows } = row,
isSubRow = subRows[0] ? false : true,
firstSubRowName = subRows[0] ? subRows[0].original.name : "",
remainingSubRowsLength = subRows[1] ? `..+${subRows.length - 1}` : "";
return (
<span>
{row.original.name}
{!isSubRow && (
<span
style={{ color: "blue" }}
>{` (${firstSubRowName}${remainingSubRowsLength})`}</span>
)}
</span>
);
}
}
],
[]
);
const { getTableProps, getTableBodyProps, rows, prepareRow } = useTable(
{
columns,
data
},
useExpanded
);
return (
<>
<h1>React Table v.7 SubRows</h1>
<br />
<table
{...getTableProps()}
style={{ borderCollapse: "collapse", width: "50%" }}
>
<tbody {...getTableBodyProps}>
{rows.map((row, index) => {
prepareRow(row);
return (
<tr
{...row.getRowProps}
key={index}
style={{
border: "1px solid black",
height: "30px",
background: row.subRows[0] ? "#e9ebfb" : "#fff"
}}
>
{row.cells.map((cell, i) => {
return (
<td {...cell.getCellProps()} key={i}>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</>
);
}
Here is the example at codesanbox:
I'm a newbie and I try to set up a search engine that will render the products, base on the value I type on the input.
With my code below, I tried that way but it seems that my logic isn't correct.
Could someone go through my code and check it out and can give me some insight afterward, please.
Thank you in advance.
import data from "./utils/data";
const App = () => {
const [searchValue, setSearchValue] = useState("");
const handleChange = (e) => {
setSearchValue(e.target.value);
};
return (
<div>
<SearchInput handleChange={handleChange} searchValue={searchValue} />
<Products data={data} searchValue={searchValue} />
</div>
);
};
const SearchInput = ({ searchValue, handleChange }) => {
return (
<div>
<input
type="text"
placeholder="Search specific item..."
value={searchValue}
onChange={handleChange}
/>
</div>
);
};
export default SearchInput;
function Products({ data, searchValue }) {
const [productsInfo, setProductsInfo] = useState([]);
useEffect(() => {
filteredProducts(data);
}, []);
const filteredProducts = (products) => {
if (searchValue.toLowerCase().trim() === "") {
setProductsInfo(products);
} else {
const seekedItem = productsInfo.filter(
(product) =>
product.name.toLowerCase().trim().includes(searchValue) ===
searchValue.toLowerCase().trim()
);
setProductsInfo(seekedItem);
}
};
const productsData =
productsInfo.length <= 0 ? (
<div>Loading...</div>
) : (
<div>
{productsInfo.map((product, index) => {
return (
<div
key={index}
style={{ backgroundColor: "grey", maxWidth: "300px" }}
>
<h4>{product.name}</h4>
<p>{product.category}</p>
<p> {product.price} </p>
<hr />
</div>
);
})}
</div>
);
return productsData;
}
export default Products;
const data = [
{
category: "Sporting Goods",
price: "$49.99",
stocked: true,
name: "Football",
},
{
category: "Sporting Goods",
price: "$9.99",
stocked: true,
name: "Baseball",
},
{
category: "Sporting Goods",
price: "$29.99",
stocked: false,
name: "Basketball",
},
{
category: "Electronics",
price: "$99.99",
stocked: true,
name: "iPod Touch",
},
{
category: "Electronics",
price: "$399.99",
stocked: false,
name: "iPhone 5",
},
{ category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7" },
];
export default data;
If you return empty array in second params in useEffect This function fired only once. Try that:
useEffect(() => {
filteredProducts(data);
}, [searchValue]);
.includes return true or false (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)
Try that:
const filteredProducts = (products) => {
if (searchValue.toLowerCase().trim() === "") {
setProductsInfo(products);
} else {
const seekedItem = productsInfo.filter(
(product) =>
product.name.toLowerCase().trim().includes(searchValue)
);
setProductsInfo(seekedItem);
}
};
How can I make a separate trigger for each Collapsible element with a single useState (preferably)?
CodeSandBox - https://codesandbox.io/s/separate-triggers-question-forked-vgs5b
App.js
import React from "react";
import Collapsible from "react-collapsible";
function App() {
const database = [
{ id: 1, name: "Name1", description: "Desc1" },
{ id: 2, name: "Name2", description: "Desc2" },
{ id: 3, name: "Name3", description: "Desc3" },
{ id: 4, name: "Name4", description: "Desc4" },
{ id: 5, name: "Name5", description: "Desc5" }
];
const [items, setItems] = React.useState(database);
const [open, setOpen] = React.useState(false);
return (
<div className="tracker_master">
{items.map((item, index) => (
<div onClick={() => setOpen(!open)} key={item.id}>
{item.name.toUpperCase()}
<Collapsible open={open}>
<div>{item.description.toUpperCase()}</div>
</Collapsible>
</div>
))}
</div>
);
}
export default App;
To have seperate triggers for each item I recommend to abstract each element into its own component that has its own open state.
So you could do something like this:
const Item = ({ item }) => {
const [open, setOpen] = React.useState(false);
return (
<div onClick={() => setOpen(!open)} key={item.id}>
{item.name.toUpperCase()}
<Collapsible open={open}>
<div>{item.description.toUpperCase()}</div>
</Collapsible>
</div>
);
};
function App() {
const database = [
{ id: 1, name: "Name1", description: "Desc1" },
{ id: 2, name: "Name2", description: "Desc2" },
{ id: 3, name: "Name3", description: "Desc3" },
{ id: 4, name: "Name4", description: "Desc4" },
{ id: 5, name: "Name5", description: "Desc5" }
];
return (
<div className="tracker_master">
{database.map((item) => (
<Item item={item} key={item.id} />
))}
</div>
);
}
sandbox example
If you want to use single useState then your single state object should manage open flag of all the items as shown below (I have updated your code and tested its working fine).
openFlags is single state which maintains the open flag of each item by id and use it for triggering collabse and expand the items independently.
import React from "react";
import Collapsible from "react-collapsible";
function App() {
const database = [
{ id: 1, name: "Name1", description: "Desc1" },
{ id: 2, name: "Name2", description: "Desc2" },
{ id: 3, name: "Name3", description: "Desc3" },
{ id: 4, name: "Name4", description: "Desc4" },
{ id: 5, name: "Name5", description: "Desc5" }
];
const [items, setItems] = React.useState(database);
let initialOpenFlags = {}
items.forEach((i) => {
initialOpenFlags = {
...initialOpenFlags,
[i.id]: false
};
});
const [openFlags, setOpenFlags] = React.useState(initialOpenFlags);
return (
<div className="tracker_master">
{items.map((item, index) => (
<div
onClick={() =>
setOpenFlags({ ...openFlags, [item.id]: !openFlags[item.id] })
}
key={item.id}
>
{item.name.toUpperCase()}
<Collapsible open={openFlags[item.id]}>
<div>{item.description.toUpperCase()}</div>
</Collapsible>
</div>
))}
</div>
);
}
export default App;
I'm trying to create dynamic TreeView with Reactjs and material UI, but when I have the levels of recursion I got error Uncaught RangeError: Maximum call stack size exceeded
my treeItem code
you need to consider understand this part{item.sub && TreeItems(item.sub)}
when the item has a sub this return a <TreeItem otherwise it doesn't do anything
also note I created this the TreeView without recursion and it worked very fun just like this
<TreeView
disableSelection
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
>
<TreeItem nodeId="1" label="1">
<TreeItem nodeId="12" label="12">
<TreeItem nodeId="3" label="3">
<TreeItem nodeId="13" label="13"></TreeItem>
</TreeItem>
</TreeItem>
</TreeItem>
</TreeView>
my data setrcture
const myData = [
{ id: 1, text: "value" },
{
id: 2,
text: "xxx",
sub: [
{
id: 10,
text: "value",
sub: [
{
id: 100,
text: "value",
sub: [{ id: 20, text: "value", sub: [{ id: 200, text: "value" }] }],
},
],
},
],
},
];
my recursive function
function TreeItems(myData) {
return myData.map((item, index) => (
<TreeItem
onLabelClick={(e) => e.preventDefault()}
key={index}
nodeId={`${index}`}
label={
<div
style={{ display: "flex" }}
draggable={isDraggable}
{...Draggable(item, index, Update, setDraggable)}
>
<DragIndicatorIcon
onMouseDown={(e) => setDraggable(true)}
onMouseUp={(e) => setDraggable(false)}
/>
<Text>{item.text}</Text>
</div>
}
>
{item.sub && TreeItems(item.sub)}
</TreeItem>
));
}
--
note: probably it is not necessary to see my full but just in case.
my full code
app.tsx
import React from "react";
import "./App.css";
import Tree from "./Components/Tree";
// import { DataGrid } from "#material-ui/data-grid";
import RouterBreadcrumbs from "./Components/Nav";
import { BrowserRouter, Route } from "react-router-dom";
import SimpleBreadcrumbs from "./Components/BreadCrumbs";
const My = [
{
id: 0,
text: (
<div>
this is an example <b>data</b>
</div>
),
},
{ id: 1, text: "test tow" },
{ id: 2, text: "test three" },
{
id: 3,
text: "main",
sub: [
{ id: 4, text: "sub 1" },
{ id: 5, text: "sub 2" },
],
},
];
const Home = [
{ id: 1, text: "value" },
{
id: 2,
text: "xxx",
sub: [
{
id: 10,
text: "value",
sub: [
{
id: 100,
text: "value",
sub: [{ id: 20, text: "value", sub: [{ id: 200, text: "value" }] }],
},
],
},
],
},
];
function App() {
return (
<BrowserRouter>
<div className="App">
<SimpleBreadcrumbs />
<RouterBreadcrumbs />
<Route exact path="/Home" component={(e: any) => Tree(Home)} />
<Route exact path="/My" component={(e: any) => Tree(My)} />
</div>
</BrowserRouter>
);
}
export default App;
export function my(x: string) {
return x.toUpperCase();
}
tree.tsx
import React, { useReducer, useState } from "react";
import TreeView from "#material-ui/lab/TreeView";
import ExpandMoreIcon from "#material-ui/icons/ExpandMore";
import ChevronRightIcon from "#material-ui/icons/ChevronRight";
import TreeItem from "#material-ui/lab/TreeItem";
import Draggable from "../Functions/Draggable";
import DragIndicatorIcon from "#material-ui/icons/DragIndicator";
import Text from "./Text";
function deleting(state, action) {
state.forEach((item) => {
if (item.id === action.i.item.id) {
state.splice(action.i.index, 1);
} else if (item.id !== action.i.item.id && item.sub) {
deleting(item.sub, action);
}
});
return state;
}
function adding(state, action) {
state.forEach((item) => {
if (item.id === action.item.id) {
state.splice(action.f, 0, action.i.item);
} else if (item.sub) {
adding(item.sub, action);
}
});
return state;
}
function reordering(state, action) {
const newState = deleting(state, action);
const newState2 = adding(newState, action);
return newState2;
}
function subbing(state, action) {
state.forEach((item) => {
if (item.id === action.item.id) {
if (item.sub) {
Object.assign(item.sub, [...item.sub, action.i.item]);
}
if (!item.sub) {
Object.assign(item, { sub: [action.i.item] });
console.log(state);
}
} else if (item.sub) {
console.log("infient");
subbing(item.sub, action);
}
});
return state;
}
function Tree(myData) {
const [isDraggable, setDraggable] = useState(false);
const [state, setData] = useState(myData);
console.log(state);
function Update(item, i, f, p) {
const action = { item: item, i: i, f: f, p: p };
setData((pre) => {
if (p === "on") {
const newPre = deleting(pre, action);
const newPre2 = subbing(newPre, action);
return newPre2;
} else {
const newPre = deleting(pre, action);
const newPre2 = reordering(newPre, action);
return newPre2;
}
});
}
function TreeItems(state) {
return state.map((item, index) => (
<TreeItem
onLabelClick={(e) => e.preventDefault()}
key={index}
nodeId={`${index}`}
label={
<div
style={{ display: "flex" }}
draggable={isDraggable}
{...Draggable(item, index, Update, setDraggable)}
>
<DragIndicatorIcon
onMouseDown={(e) => setDraggable(true)}
onMouseUp={(e) => setDraggable(false)}
/>
<Text>{item.text}</Text>
</div>
}
>
{item.sub && TreeItems(item.sub)}
</TreeItem>
));
}
return (
<div>
<TreeView
disableSelection
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
>
{TreeItems(state)}
</TreeView>
</div>
);
}
export default Tree;