I have a few buttons and "view all" button. The individual buttons load the coresponding data of that index or will show all the data by clicking the "view all" button. Problem I am running into is when I click my "view all" button in the parent it's not updating the state in the child component. On mounting it works as normal but on event handler in the "view all" it doesn't update. Any thoughts on where I am going wrong here?
JS:
...
const Context = createContext(false);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ id, styles, discription }) => {
const { activeViewAll, handleChange } = useContext(Context);
const [toggleThisButton, setToggleThisButton] = useState();
const handleClick = () => {
setToggleThisButton((prev) => !prev);
handleChange(discription, !toggleThisButton);
};
return (
<>
<Avatar
className={toggleThisButton && !activeViewAll ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(toggleThisButton)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item, idx) => (
<div key={idx}>Content {item}</div>
))}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(false);
useEffect(() => {
setActiveViewAll(true);
setSelected([...data]);
}, []);
const handleChange = (val, action) => {
let newVal = [];
if (activeViewAll) {
selected.splice(0, 3);
setActiveViewAll(false);
}
if (action) {
newVal = [...selected, val];
} else {
// If toggle off, then remove content from selected state
newVal = selected.filter((v) => v !== val);
}
console.log("action", action);
setSelected(newVal);
};
const handleViewAll = () => {
console.log("all clicked");
setActiveViewAll(true);
setSelected([...data]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer styles={classes} selected={selected} />
</div>
</Context.Provider>
);
}
Codesanbox:
https://codesandbox.io/s/72166087-forked-jvn59i?file=/src/App.js:260-3117
Issue
The issue seems to be that you are mixing up the management of the boolean activeViewAll state with the selected state.
Solution
When activeViewAll is true, pass the data array as the selected prop value to the ToggleContainer component, otherwise pass what is actually selected, the selected state.
Simplify the handlers. The handleViewAll callback only toggles the view all state to true, and the handleChange callback toggles the view all state back to false and selects/deselects the data item.
function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]); // none selected b/c view all true
const [activeViewAll, setActiveViewAll] = useState(true); // initially view all
const handleChange = (val, action) => {
setActiveViewAll(false); // deselect view all
setSelected(selected => {
if (action) {
return [...selected, val];
} else {
return selected.filter(v => v !== val)
}
});
};
const handleViewAll = () => {
setActiveViewAll(true); // select view all
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected} // pass all data, or selected only
/>
</div>
</Context.Provider>
);
}
In the ToggleContainer don't use the array index as the React key since you are mutating the array. Use the element value since they are unique and changing the order/index doesn't affect the value.
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update
Since it is now understood that you want to not remember what was previously selected before toggling activeViewAll then when toggling true clear the selected state array. Instead of duplicating the selected state in the children components, pass the selected array in the context and computed a derived isSelected state. This maintains a single source of truth for what is selected and removes the need to "synchronize" state between components.
const ToggleItem = ({ id, styles, description }) => {
const { handleChange, selected } = useContext(Context);
const isSelected = selected.includes(description);
const handleClick = () => {
handleChange(description);
};
return (
<>
<Avatar
className={isSelected ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(isSelected)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update the handleChange component to take only the selected value and determine if it needs to add/remove the value.
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(true);
const handleChange = (val) => {
setActiveViewAll(false);
setSelected((selected) => {
if (selected.includes(val)) {
return selected.filter((v) => v !== val);
} else {
return [...selected, val];
}
});
};
const handleViewAll = () => {
setActiveViewAll(true);
setSelected([]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange, selected }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={d}>
<ToggleItem id={id} styles={classes} description={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected}
/>
</div>
</Context.Provider>
);
}
Related
I have such a project. Here I want the button border save in the local storage.The buttons are divided into categories. For example when you refresh the page after selecting a sports button, the border of the button disappears. I want save btn border in the localstorage. I saved the categories in memory, but I can't make the border of the selected button.How can I fix it?
import React, { useEffect, useState } from "react";
import SpinnerLoad from './components/SpinnerLoad'
import NewsItem from "./components/NewsItem";
import Category from "./components/data/Category"
const App = () => {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false)
const [selected, setSelected] = useState('');
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
setLoading(false);
};
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
<button onClick={() =>{ fetchValue(category,i) ; setSelected(i)} }
style={{border : selected === i ? '1px solid red' : null}} >{category}</button>
);
useEffect(() => {
let categoryValue = localStorage.getItem("category") || "all";
fetchValue(categoryValue)
const select = localStorage.getItem("selected") || "";
setSelected(select);
}, []);
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value,i) => {
return <CategoryButton category={value} i={i}/>;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad />
:
state.map((data, index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
key={data.id}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default App;
According to the code looks like you want to display data specific to a category set when the user clicks on the category buttons. and after the click, the correct data is rendered and the current category button receives a change in its style highlighting it is the current state.
I don't understand why you need to store anything in a client's localstorage,
I would not recommend storing too much in localStorage as it is limited and is used by different sites a user visits, I only store authentication tokens in localstorage and I believe that is the norm.
I've tried to create the effect you want without the need to store in local storage
import React, { useState, useCallback, useEffect } from "react";
import ReactDOM from "react-dom";
import { cat } from "../categories.js";
import { news } from "../news.js";
function Example() {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState(null);
useEffect(() => {
function fetchFunction() {
setLoading(true);
for (let i = 0; i < news.length; i++) {
if (news[i].id === selected) {
const current = news[i].c;
setState(current);
}
}
setLoading(false);
}
fetchFunction();
}, [selected]);
return (
<>
<ol
style={{
width: "50%",
listStyle: "none",
display: "flex",
justifyContent: "space-between"
}}
>
{cat.map((item, index) => {
return (
<li key={index}>
<button
style={{ border: selected === item.id && "none" }}
onClick={() => {
setSelected(item.id);
}}
>
{item.name}
</button>
</li>
);
})}
</ol>
<section style={{ width: "100%", height: "70%" }}>
{state.map((item, index) => {
return (
<div
key={index}
style={{
width: "30%",
height: "30%",
background: "red",
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "1% 0 2% 0"
}}
>
{item.name}
</div>
);
})}
</section>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
You can save the selectedIndex in localStorage and retrieve it in the useEffect..
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
// setting selected as string instead of index for type checking
<button onClick={() =>{ fetchValue(category,i) ; setSelected(`${i}`)} }
style={{border : selected === `${i}` ? '1px solid red' : null}} >{category}</button>
);
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
// ...
}
useEffect(() => {
const select = localStorage.getItem("selected") || "";
// passing selectedIndex to the fetchValue, otherwise it becomes
//undefined..
fetchValue(categoryValue,select)
setSelected(select);
},[])
How can I render only the icon cartIcon dynamically? Because right now, like the code below, when I enter in the component with the mouse, all the icons appears not only the icon of the single product.
I think because of map but how can I render only to it?
interface IItemsProps {
products: ProductsType;
}
const Items: React.FunctionComponent<IItemsProps> = ({ products }) => {
const [state, setState] = React.useState<boolean>(false);
const handleMouseEnter = () => {
setState(true);
};
const handleMouseLeave = () => {
setState(false);
};
const itemUI = products.map((item: SingleProductsType) => {
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state && <IconsCarts />} ** //HERE I NEED TO SHOW THIS COMPONENT ONLY WHEN I
// ENTER WITH THE MOUSE BUT ONLY FOR THE SELECTED
//PRODUCT NOT ALL OF THEM **
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
});
return <WrapperItems>{itemUI}</WrapperItems>;
};
export default Items;
You could store the hovered _id in state, so you know which one it was.
const [state, setState] = React.useState<string | null>(null); // or `number` ?
Then
{state === _id && <IconsCarts />}
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={() => setState(_id)}
onMouseLeave={() => setState(null)}
/>
Or you could move the useState into a component that is called every loop of your map, so that each item has its own private state.
function MyItem({item}: { item: SingleProductsType }) {
const [state, setState] = React.useState<boolean>(false);
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state && <IconsCarts />}
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
}
Now you can do:
{products.map((item: SingleProductsType) => <MyItem item={item} />}
Lastly, if all you want to do is show/hide the cart icon when you enter some element with the mouse, this solution is probably way overkill. You can do this with CSS alone, which is going to be a far cleaner solution since it takes no javascript code whatsoever, and you don't have to track state at all.
.item {
width: 100px;
height: 100px;
background: #aaa;
margin: 10px;
}
.item button {
display: none;
}
.item:hover button {
display: block;
}
<div class="item">
Foo
<button>Add to cart</button>
</div>
<div class="item">
Bar
<button>Add to cart</button>
</div>
<div class="item">
Baz
<button>Add to cart</button>
</div>
With a boolean in state, all you know is whether to show an icon, but what about knowing which list item to show the icon on? Instead of state being a boolean, how about we use the index of the product.
interface IItemsProps {
products: ProductsType;
}
const Items: React.FunctionComponent<IItemsProps> = ({ products }) => {
const [state, setState] = React.useState<number>(-1);
const handleMouseEnter = (index) => {
setState(index);
};
const handleMouseLeave = () => {
setState(-1);
};
const itemUI = products.map((item: SingleProductsType, index: number) => {
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state === index && <IconsCarts />} ** //Check if index matches state before showing icon **
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
});
return <WrapperItems>{itemUI}</WrapperItems>;
};
export default Items;
Now the condition to show the icon is if the index of the list item matches the index in state. And we pass in the index to handleMouseEnter to set state to that index, and handleMouseLeave will reset it back to -1.
UPDATED!
I'm creating a wrapper for dropdown menu and use it in several components so I need to make such a Menu generic. The problem is I do not understand how to pass external variables into such a component.
My component:
const SelectOptionsPaginated = ({
alignment, minWidth, width,
rowData,
column
}) => {
..........
const Menu = (props) => {
const {options} = props;
const dropdownContainer = useRef(null);
const [maxMenuHeight, setMaxMenuHeight] = useState(300)
const [dropDownStyle, setDropDownStyle] = useState({
position: "absolute",
minWidth: `${minWidth ? minWidth + "px" : "100%"}`,
maxWidth: `${width}px`,
maxHeight: `${maxMenuHeight}px`,
top: `32px`
})
const getDropdownPosition = (elem) => {
setDropDownStyle({
...dropDownStyle,
...getDropdownAlignment(elem, setMaxMenuHeight, gridId, options, true)
})
};
useEffect(() => {
const optionsList = dropdownContainer.current
if (!optionsList) return
getDropdownPosition(optionsList)
}, [options])
return (
<div
className="dropdown-container"
ref={dropdownContainer}
style={dropDownStyle}
>
<components.Menu {...props} >
{props.children}
</components.Menu>
</div>
)
}
............
return <AsyncPaginate
additional={defaultAdditional}
isMulti={isMulti}
value={value}
loadOptions={loadOptions}
onChange={handleChange}
escapeClearsValue
isClearable
styles={getStylesForSelectorEditor(width, minWidth, newAlignment)}
components={{Menu}}
/>
such variables as minWidth, width should be passed externally to Menu.
I tried something like:
...............
return <AsyncPaginate
additional={defaultAdditional}
isMulti={isMulti}
value={value}
loadOptions={loadOptions}
onChange={handleChange}
escapeClearsValue
isClearable
styles={getStylesForSelectorEditor(width, minWidth, newAlignment)}
// pseudocode
components={{<Menu width={100}/>}} or
components={{Menu(100)}}
/>
but it doesn't work.
I tried to google but didn't find clear information. I'm new in react so will appreciate any help.
Did you meant something like that?
const AsyncPaginate= (props) => {
const {components} = props;
return (
<>
{components}
</>
)
}
const Menu = () => {
return (
<>
something...
</>
)
}
const App = () => {
return (
<>
<AsyncPaginate components={<Menu />}></AsyncPaginate>
</>
)
}
I can't understand why I can't pass those two functions in onClick event in my ParsedColors Component. Only one is running. And if I change the places of the functions, the other one is running only.
Here are the functions:
const ParsedColors = props => {
const [selected, setSelected] = useState('');
const handleClick = ([color, productId]) => {
setColor([productId, color])
setSelected(color)
}
return(
<AnimateSharedLayout>
<ul>
{props.product.color.map(color => {
const parsed = JSON.parse(color)
return(
<Color
key={parsed.value}
color={parsed.value}
isSelected={selected === parsed.value}
onClick={() => handleClick([parsed.value, props.product._id])}
/>
)
})}
</ul>
</AnimateSharedLayout>
)
}
const Color = ({ color, isSelected, onClick }) => {
return (
<li className="item" onClick={onClick} style={{ backgroundColor: color}}>
{isSelected && (
<motion.div
layoutId="outline"
className="outline"
initial={false}
animate={{ borderColor: color }}
transition={spring}
/>
)}
</li>
);
}
const spring = {
type: "spring",
stiffness: 500,
damping: 30
};
Here is where I use the component:
<div className="product--Card--Layout">
<div className="product--Colors--Container">
<ParsedColors product={product}/>
</div>
...
I am trying to push the {database, id} to the end of the databaseChanges object which will be stored in a state variable as I want to access all of them. However I am getting undefined when I try to set it a new state variable (setDatabaseArr).
Here is my code:
const UnitTestsDatabaseView = props => {
const [databaseArr, setDatabaseArr] = useState('')
const addToProduction = test => () => {
const databaseChanges = props.unitTestsData.map(test => {
return {
"unit_test_id": test.id,
"databases": test.databases
}
})
const { databases, id } = test
console.log(databases, id)
databaseChanges.push(databases, id)
setDatabaseArr(databases, id)
console.log( setDatabaseArr(databases, id))
console.log( databaseChanges.push(databases, id))
}
return (
<div>
<div className='Card' style={{marginTop: '40px', overflow: 'hidden'}}>
<div className='TableTopbar UnitTestsGrid'>
<div>ID</div>
<div>Name</div>
<div>Database</div>
<div />
</div>
{props.unitTestsData && props.unitTestsData.map(test =>
<div key={test.id} className='Table UnitTestsGrid' style={{overflow: 'hidden'}}>
<div>{test.id}</div>
<div>{test.unit_test_name}</div>
<div>{test.databases}
<div>
<Checkbox
mainColor
changeHandler={addToProduction(test)}
data={{}}
id={test.id}
/>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default withRouter(UnitTestsDatabaseView)
I review your code, It seems there is a problem with the implementation on how to push a value to the state.
I tried to reproduce the problem and try to implement of which I think a solution.
And here is the code
import React, { useState, useEffect } from "react";
import { Checkbox } from "#material-ui/core";
// In order to reproduce the propblem
// Lets that these are the values of the unitTestsData props
// and instead of passing this as value of a props
// I defined it right here.
const unitTestsData = [
{ id: 1, unit_test_name: "Unit I", databases: "test1" },
{ id: 2, unit_test_name: "Unit II", databases: "test2" },
{ id: 3, unit_test_name: "Unit III", databases: "test3" }
];
const UnitTestsDatabaseView = () => {
const [databaseArr, setDatabaseArr] = useState([]);
// Maybe you want to push data if the checkbox is checked
// and pop the data if checkbox is unchecked :: Yes ???
// This is how you do it.
const addToProduction = ({ target }, { id, databases }) => {
setDatabaseArr((previousState) => {
let newState = [...previousState];
if (target.checked) {
newState = [
...newState,
{ unit_test_id: newState.length + 1, databases }
];
} else {
const i = newState.findIndex(({ unit_test_id }) => unit_test_id === id);
if (i !== -1) newState.splice(i, 1);
}
return newState;
});
};
useEffect(() => {
console.log("databaseArr", databaseArr);
}, [databaseArr]);
return (
<div>
<div className="Card" style={{ marginTop: "40px", overflow: "hidden" }}>
<div className="TableTopbar UnitTestsGrid">
<div>ID</div>
<div>Name</div>
<div>Database</div>
</div>
{unitTestsData.map((test) => {
const { id, unit_test_name, databases } = test;
return (
<div
key={id}
className="Table UnitTestsGrid"
style={{ overflow: "hidden" }}
>
<div>{id}</div>
<div>{unit_test_name}</div>
<div>
{databases}
<div>
<Checkbox
color="secondary"
onChange={(e) => addToProduction(e, test)}
data={{}}
id={id.toString()}
/>
</div>
</div>
</div>
);
})}
</div>
</div>
);
};
export default UnitTestsDatabaseView;
You may click the codesandbox link to see the demo
https://codesandbox.io/s/pushing-value-49f31