Logic to keep only one section open at a time - reactjs

I'm struggling to come up with the implementation.
It's like a collapsible. I have a card component that has a local useState. Only one radio button should be active, but right now all of them can be active.
I know that using a local state, each card is going to have his own state, so they all can be active at the same time.
In this case, I'll need some kind of unified state or something like that, to handle the selection. But how can I do that?
This is my card component:
import * as S from './styled'
import Radio from '#material-ui/core/Radio'
export default function PackageCard({ packageInformations }) {
const [selectedValue, setSelectedValue] = React.useState(false)
const handleChange = () => {
setSelectedValue(!selectedValue)
}
return (
<>
<S.FormSectionContainer>
<S.PackageContainer>
<S.PackageSelectionContainer>
<S.PackageNameAndPrice>
<S.PackageName>{packageInformations.packageName}</S.PackageName>
<S.PackagePrice>{packageInformations.packagePrice}</S.PackagePrice>
</S.PackageNameAndPrice>
<Radio checked={selectedValue} onChange={handleChange} value={selectedValue} name="package" />
</S.PackageSelectionContainer>
{selectedValue && (
<S.PackageInformationContainer>
<S.PackageDescription>{packageInformations.description}</S.PackageDescription>
</S.PackageInformationContainer>
)}
</S.PackageContainer>
</S.FormSectionContainer>
</>
)
}
In my page, I just:
{packages.map(packageInfo => (
<PackageCard packageInformations={packageInfo}/>
))}
And this is what I have:

What about putting the state on the level upper?
const PackageCardsList = () => {
const [checkedIndex, setCheckedIndex] = useState();
return (
<>
{packages.map((packageInfo, index) => (
<PackageCard
checked={checkedIndex === index}
packageInformations={packageInfo}
onCheck={() => setCheckedIndex(index)}
/>
))}
<>
);
};
Then handle these two new props in the component PackageCard.
And it will be better if your cards have an ID so you will be able to store the ones instead of indexes. And don't forget about key props for rendering arrays components.

Related

'Maximum update depth exceeded' error when trying to use custom function passed as props in 'onClick' of button

I am trying to use a pass a function as props declared in App.js to handle a state variable in App.js in order to add items to a cart component but get this error as soon as I add the function to the onClick field of the "Add" button in my product component(at the end of post):
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
My App.js looks like this:
const App = () => {
const [cartItems, setCartItems] = useState([]);
const handleAddProduct = (product) => {
//some logic to add product to cartItems list here
}
return(
<Box className="App">
<AppRoutes handleAddProduct={handleAddProduct} cartItems={cartItems}/>
</Box>
);
}
Im passing the function and the state variable as props to my routes component so I can access it in my Products page:
const AppRoutes = ({ handleAddProduct, cartItems }) => {
return (
<Routes>
<Route exact path="/alldrip" element={<ProductsPage handleAddProduct={handleAddProduct} />} />
</Routes>
)}
And in my products page the function gets passed as props again to another component:
const ProductsPage = ({ handleAddProduct }) => {
return (
<AllProducts handleAddProduct={handleAddProduct} />
)}
And then I pass the function one last time in AllProducts to an individual Product component: ( I seperated the components this way so it is easier for me to read)
const AllProducts = ({ handleAddProduct }) => {
return (
{products.map(product => {
return (
<Product handleAddProduct={handleAddProduct} product={product} />
)
})}
)}
The products load fine but the app crashes with the error as soon as I add the function to the "Onclick" of the add to cart button:
const Product = ({ handleAddProduct, product }) => {
return (
<Heading>{product.name}</Heading>
<Text >{product.material}</Text>
<Text>{product.price} </Text>
<Button onClick={handleAddProduct(product)} >Add to Cart</Button>
)}
If I remove the function the app stays alive !
I'm not understanding why the error states setState is getting called repeatedly.
onClick={handleAddProduct(product)}
This should probably only be
onClick={() => handleAddProduct(product)}
otherwise you're calling the handleAddProduct method on render directly and not on click.
You call your handleAddProduct directly in your jsx that re-renders the component that directly call handleAddProduct and so on ...
You can try
onClick={() => handleAddProduct(product)}
A better approach is to avoid anonymous functions so in your Product component
const Product = ({ handleAddProduct, product }) => {
const onAddProduct = (product) => {
handleAddProduct(product)
}
return (
...
<Button onClick={onAddProduct}>Add to Cart</Button>
)
)}

Accessing a component state from a sibling button

I'm building a page that will render a dynamic number of expandable rows based on data from a query.
Each expandable row contains a grid as well as a button which should add a new row to said grid.
The button needs to access and update the state of the grid.
My problem is that I don't see any way to do this from the onClick handler of a button.
Additionally, you'll see the ExpandableRow component is cloning the children (button and grid) defined in SomePage, which further complicates my issue.
Can anyone suggest a workaround that might help me accomplish my goal?
const SomePage = (props) => {
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { /* Need to access MyGrid state */ }} />
Add Row
</button>
<MyGrid>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
const ExpandableRowsComponent = (props) => {
const data = [{ id: 1 }, { id: 2 }, { id: 3 }];
return (
<>
{data.map((dataItem) => (
<ExpandableRow id={dataItem.id} />
))}
</>
);
};
const ExpandableRow = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<div className="row-item">
<div className="row-item-header">
<img
className="collapse-icon"
onClick={() => setExpanded(!expanded)}
/>
</div>
{expanded && (
<div className="row-item-content">
{React.Children.map(props.children, (child => cloneElement(child, { id: props.id })))}
</div>
)}
</div>
);
};
There are two main ways to achieve this
Hoist the state to common ancestors
Using ref (sibling communication based on this tweet)
const SomePage = (props) => {
const ref = useRef({})
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { console.log(ref.current.state) }} />
Add Row
</button>
<MyGrid ref={ref}>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
Steps required for seconds step if you want to not only access state but also update state
You must define a forwardRef component
Update ref in useEffect or pass your API object via useImerativeHandle
You can also use or get inspired by react-aptor.
⭐ If you are only concerned about the UI part (the placement of button element)
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
(Mentioned point by #Sanira Nimantha)

How can I keep input values of React Material UI multiple select after refreshing/reloading the page?

I have a react material ui multi-select that shows the list of all my venues and I can multi-select venues from it. Right now when I refresh the page after multi select, all my selections disappear and the selected list shows nothing; but I want it to keep the selected values after page refresh/reload. Here is my code:
const addEventContent = () => {
const venueNames = venues.map((item) => item.name);
const venueIds = venues.map((item) => item.id);
const names = {};
venueIds.forEach((key, i) => (names[key] = venueNames[i]));
return(
<>
.
.
.
<FormControl className={classes.formControl}>
<InputLabel id='demo-mutiple-checkbox-label'>Venues</InputLabel>
<Select
labelId='demo-mutiple-chip-label'
id='demo-mutiple-chip'
multiple
value={venueId}
onChange={handleChange}
input={<Input id='select-multiple-chip' />}
renderValue={(selected) => (
<div className={classes.chips}>
{selected.map((value) => (
<Chip
key={value}
label={names[value]}
className={classes.chip}
/>
))}
</div>
)}
>
{Object.keys(names).map((id) => (
<MenuItem key={id} value={id}>
{names[id]}
</MenuItem>
))}
</Select>
</FormControl>
.
.
.
<>
}
const handleChange = (event) => setVenueId(event.target.value);
I would appreciate any help or hint!
You can always use localStorage to store such values that persist over refresh. Other than using the browser provided storage, I am not sure there is a way to persist such data over the client side after reloads.
You can add another function as follows:
function getVenueId() {
const localStorage = window.localStorage;
let venueId;
if(localStorage) {
localStorage.getItem('venue-id-selected-value');
}
return venueId;
}
When you initialize venueId using useState, you can call this function as follows:
let [venueId, setVenueId] = useState(getVenueId());
And finally, in your handleChange function, you can just set the item in localStorage to the selected value as follows:
const handleChange = (event) => {
const localStorage = window.localStorage;
localStorage.setItem('venue-if-selected-value',`${event.target.value}`);
setVenueId(event.target.value);
}
Note the caveats of this approach:
=> You can only store string values in localStorage.
=> localStorage data is specific to the protocol of the webpage.
Edit: If you are looking for a solution that does not have to persist over a long period of time, such as only until the tab is closed, the sessionStorage may be a better option for you. The solution would still be the same mostly. You would just have to replace localStorage with sessionStorage in the code.

How can i get multiple recoil atoms when using components multiple?

In some components i am using recoil atoms to manage my states. One example is my modal component. It look something like this:
export const modalState = atom({
key: "modalState",
default: false
})
export const useToggleModalState = () => {
const setModalState = useSetRecoilState(modalState)
return (state, callback) => {
setModalState(state)
if (callback) {
callback()
}
}
}
export const Modal = (props) => {
<Transition show={modalState}>
<Dialog>
<Dialog.Title>My Modal Headline</Dialog.title>
<Dialog.Description>My Modal Description</Dialog.Description>
</Dialog>
</Transition>
}
and i am using this modal like this:
const toggleModalState = useToggleModalState();
return (
<Modal />
<Button text="Close Modal" onClick={() => toggleModalState(false)} />
)
however, if I use the modal multiple times, the modal is automatically duplicated, but I still use only one state for all modals. Of course, I don't want that. If I use a component multiple times, I want the state to be created multiple times, so that I can change the state of each component individually.
I have read that there are also atomFamilys. Could I use these at this point? What should my code look like then? Can multiple atoms also be created automatically if I use a component multiple times?
Why do you want to use recoil for that? The state of the modal is tied to the modal itself, it doesn't need to access global state.
You can just use useState to determine if you want to show a modal within a component:
export const Modal = (props) => {
<Transition show={props.show}>
<Dialog>
<Dialog.Title>My Modal Headline</Dialog.title>
<Dialog.Description>My Modal Description</Dialog.Description>
</Dialog>
</Transition>
}
export const ComponentWithModal = () => {
const [showModal, setShowModal] = useState(false);
return (
<>
<Modal show={showModal}/>
<Button text="Open Modal" onClick={() => setShowModal(true)} />
<Button text="Close Modal" onClick={() => setShowModal(false)} />
</>
)
}

How to pass state from parent to child in react?

How do I pass a state attribute from parent to child? In the following implementation, the Dropdown component has a state "isActive" and I want to access it in the Button component to attach propper styling to it. The Dropdown has to generic as it is supposed to take different sorts of buttons.
<Dropdown items="...">
<Button active ="false" />
</Dropdown>
Dropdwon.js
...
constructor(props){
super(props)
this.state = {
isActive: true,
}
}
render (){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children} /* want to set active prop for the child button here */
</div>
);
}
...
You have two possibilities:
Lift your Dropdown state and keep it in its parent component;
Use useContext hook;
The first approach would be better, but it may not be good for your application (I cannot know that). Let me make an example for both cases.
This is an example where I've lifted the isActive state to the parent component.
const ParentComponent = () => {
const [isActive, setIsActive] = useState(false);
handleIsActiveChange = (newValue) => {
setIsActive(newValue);
}
<Dropdown isActive={isActive} setIsActive={handleIsActiveChange}>
<Button isActive={isActive} />
</Dropdown>
}
const Dropdown = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
// You can use `props.handleIsActiveChange` to update the `isActive` state.
}
const Button = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
}
Instead, this exploits the useContext API:
const dropdownContext = React.createContext(null);
const Dropdown = props => {
const [isActive, setIsActive] = useState(false);
return (
<dropdownContext.Provider value={{ isActive }}>
{props.children}
</dropdownContext.Provider>
);
}
const Button = props => {
const dropdownCtx = React.useContext(dropdownContext);
// You can use `dropdownCtx.isActive` to know whether the dropdown is active or not.
}
Aside from the answer I linked, there might be another way of achieving this that I didn't see mentioned.
You can send a function as a children element of your dropdown which will take isActive as a variable :
<Dropdown items="...">
{isActive => <Button active={isActive} />}
</Dropdown>
Then, is the render function, simply call the function and send your state value as a parameter :
render(){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children(this.state.isActive)}
</div>
);
}
<Dropdown >
<Button isActive={this.state.isActive} />
</Dropdown>
In your button get it with this.props.isActive

Resources