Trying to add multiple refs to dynamically generated element in ReactJs - reactjs

I am trying to add multiple refs to a set of dynamically generated checkboxes. At this point, the checkboxes work, but when I am trying to change the checked state of a particular checkbox, it seems the last one is the only one to have the ref and the state updates just for that particular checkbox.
So far I have this:
// Setting 6 as a number of states
new Array(states.length).fill(false)
);
// Adding refs to check values
const inputsRef = useRef([]);
const handleStateChecked = (position) => {
const updateCheckedState = isStateChecked.map((isChecked, index) =>
index === position ? !isChecked : isChecked
);
setStateIsChecked((prevState) => updateCheckedState);
};
return (
{ ... form ...}
{/* Generating state checkboxes dynamically */}
{states.map((state, index) => {
return (
{...div with styles}
<input
type="checkbox"
checked={isStateChecked[index]}
onChange={ () => handleStateChecked(index)}
value={state}
ref={inputsRef}
/>
{state}
</div>
);
})}
</div>
</div>
Could someone guide me in the right direction? Thanks!

Related

useRef is getting cleared when filtering array of objects

I've created one Demo Application where I've been able to add comments while clicking on the chat icon textarea will be expanded, currently, the functionality is I've created a reference using useRef of that particular text area using unique id, and I'm saving comments to that reference array, & rendering on UI using ref.current Method, everything is working as I expected but when I click on those filter buttons, the reference is getting null! my requirement is even though I do filter comments should be persisted!
Any suggestion, Any new Approach except using useRef would be Appreciated! Thanksyou!!
Here's my codesandbox link
https://codesandbox.io/s/proud-resonance-iryir7?file=/src/App.js
Your comment ref should only contains comments, not includes textarea element. So you should create a component to handle textarea value
const TextArea = ({ value, handleSaveComment }) => {
const ref = useRef(null);
return (
<>
<textarea
placeholder="Enter Here"
ref={ref}
defaultValue={value}
></textarea>
<div
className="save-button"
onClick={() => {
handleSaveComment(ref.current.value);
}}
>
Save
</div>
</>
);
};
and use it in table
const handleSaveComment = (fsValidationId, value) => {
comment.current[fsValidationId] = value;
setExpandedId((prev) => (prev === fsValidationId ? "0" : fsValidationId));
};
<AccordionDetails>
<TextArea
handleSaveComment={(value) =>
handleSaveComment(row.id, value)
}
value={comment.current[row.id]}
/>
</AccordionDetails>
You can check full code in my codesandbox. Hope it help!

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.

Is there any pitfall of using ref as conditional statement?

Context: I am trying to scroll view to props.toBeExpandItem item which keeps changing depending on click event in parent component. Every time a user clicks on some button in parent component I want to show them this list and scroll in the clicked item to view port. Also I am trying to avoid adding ref to all the list items.
I am using react ref and want to add it conditionally only once in my component. My code goes as below. In all cases the option.id === props.toBeExpandItem would be truthy only once in loop at any given point of time. I want to understand will it add any overhead if I am adding ref=null for rest of the loop elements?
export const MyComponent = (
props,
) => {
const rootRef = useRef(null);
useEffect(() => {
if (props.toBeExpandItem && rootRef.current) {
setTimeout(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}
}, [props.toBeExpandItem]);
return (
<>
{props.values.map((option) => (
<div
key={option.id}
ref={option.id === props.toBeExpandItem ? rootRef : null}
>
{option.id}
</div>
))}
</>
);
};
Depending upon your recent comment, you can get the target from your click handler event. Will this work according to your ui?
const handleClick = (e) => {
e.target.scrollIntoView()
}
return (
<ul>
<li onClick={handleClick}>Milk</li>
<li onclick={handleClick}>Cheese </li>
</ul>
)

How do I idiomatically turn a text component into an input component on click?

I am creating a todo app in React that is basically just a list of items, grouped by category. I want to add functionality such that when I click a single to do(which is a paragraph), it brings up an input with the current text that I can edit and save. How can I do that without manually editing the DOM?
Code:
A single todo item:
import React from 'react';
const Item = props => {
return (
<div
className={`item${props.item.purchased ? ' purchased' : ''}`}
onClick={() => props.toggleItem(props.item.id)}
>
<p>{props.item.name}</p>
</div>
);
};
export default Item;
I want to change the toggle to be a radio button and onClick to edit the todo.
sample image of todos
First of all, you will need a prop that updates item.name (this will be needed when you will edit the input)
You didn't explained well how you want it to work, so I made an example where you click on the text to edit it to a text input and also have a button to save the edit.
const Item = props => {
const [isEditing, setIsEditing] = useState(false);
return isEditing ? (
<div>
<input
value={props.item.name}
onChange={e => props.setItemName(e.target.value)}
/>
<button onClick={() => setIsEditing(false)}>Stop Editing</button>
</div>
) : (
<div
className={`item${props.item.purchased ? " purchased" : ""}`}
onClick={() => setIsEditing(true)}
>
<p>{props.item.name}</p>
</div>
);
};
I also created a working codesanbox with the behavior you want.
You will have to maintain a state to change between the TODO Item & TODO Input. Since you are using functional component, you can use useState hook from react to maintain the state as shown below
import React, { useState } from 'react';
const Item = props => {
const [isEditing, setIsEditing] = useState(false);
if (isEditing) {
return (
<div className={`item${props.item.purchased ? ' purchased' : ''}`}>
<input type="text/radio" value={props.item.name}>
</div>
);
} else {
return (
<div
className={`item${props.item.purchased ? ' purchased' : ''}`}
onClick={() => props.toggleItem(props.item.id)}
>
<p>{props.item.name}</p>
</div>
);
}
};
export default Item;
You might need to change the above a bit based on your application structure but this is what you need to follow.

Resources