Dynamic, unique values for DOM elements - reactjs

I want advice on if my solution is conventional and/or if there are better, more common approaches using React for this situation:
I have a comments section. I retrieve a list of comments from the server as a prop to my functional component. Within the JSX, I map through each comment and, for each, return a TextArea to display the comment. One of the functionalities is that if you created the comment an edit button will appear, rendering the textarea editable.
This means that I need a unique identifier for each textarea, because once the user edits the comment's content and clicks the save button, I need to post to my server with the correct _id of the comment in the DB, along with the updated content.
My most idiomatic solution thus far is to use refs, code below:
I create a ref containing an array, a function to pass objects mapping the comment._id to the element, and add that to the commentRefs array whenever the function is called. The function to add to the ref array is the ref proeprty's value for the textarea:
const addCommentRef = _id => el => {
if(el && !commentRefs.current.includes(el) && el) {
const commentRef = {
_id,
el,
content: '',
}
commentRefs.current.push(commentRef);
}
}
<textarea ref={addCommentRef(comment._id)}
On the textarea onChange, I call a function called editCommentRef, which maps through the commentRefs, compares the ID's passed as param with each _id value in the array and if found, updates the content to what has been typed into the textarea:
const editCommentViaRef = _id => event => {
for(let i = 0; i < commentRefs.current.length; i++) {
const commentRef = commentRefs.current[i];
if(_id == commentRef._id) {
commentRefs.current[i] = {
...commentRefs.current[i],
content: event.target.value
}
}
}
}
<textarea onChange= {editCommentRefs(comment._id)}...
Lastly, once the save button is clicked, its onClick handler calls a 3rd function named "saveCommentEdit", where it passes the _id of the comment. We again traverse through the commentRefs array and find the _id param matching the _id of the saved object. Once found, I have what I need to post to the server/db and update the comment:
const saveCommentEdit = _id => {
commentRefs.current.forEach((comment) => {
if(_id == comment._id) {
console.log('found comment to save to db ', comment._id + ' saving content - \n' + comment.content);
}
});
}
<button onSubmit={saveCommentEdit(comment._id)} ...
This works, but is this too complicated, and are there any better and common solutions? Other solutions I have tried/considered:
States - I imagine I would have the same issue with states as the state names need to be referred to in code, so I'd need a different variable name for each state. Also, states would require the entire component to re-render with every character typed into the textarea which seems more costly than refs.
Document selectors - The simplest solution is to call a function which targets the DOM for the elements using "findElementById" and etc.. in which case I provide a unique identifier by making the ID of each textarea contain the comment._id. The obvious issue here is that targeting the DOM directly in a React app is discouraged, and fundamentally contradicting to the React framework.
Thanks in advance!

You don't need to use Ref - it's meant for something else (to communicate with the DOM) and your task is different. You can store the edit state and record id from the database in each component. You can store the author's id to know exactly who can edit. On the server, update the record, provided that the id and author are the same. Also, you do not need to do any loops, a comment component can take care of sending data itself according to OOP.
For example you may have a table comments in your DB:
id
post_id
author_id
comment
1
1000
user100
cool
2
1001
user42
hello
Then get all comments by post_id if current user_id equal author_id you can pass to component property editable for show button edit/save (caption depends of mode)

Related

How to correctly synchronize the internal state of the input field with the React Hook Form value

I am using the MUI component library with RHF.
For quick form creation, I want to create my collection of form fields to be controlled by RHF.
But I'm facing a problem synchronizing the internal state of some complex fields, like DatePicker, with the state of the form.
For simple input components like TextField I don't create an additional internal state because I don't need to transform the value entered by the user before sending it to the form.
However, components like the DatePicker work with complex objects like the Moment object. I want to get a formatted value like "2022-02-02" after submitting the form, so I create an internal state that contains the Moment value and call RHF onChange with the formatted value I want.
const [momentValue, setMomentValue] = useState(() => getMomentDateTime(value));
const handleChange = useCallback(
(newValue) => {
setMomentValue(newValue);
onChange(
newValue && newValue.isValid()
? newValue.format(format)
: '',
);
},
[format, onChange],
);
This approach works fine until the external action is on the field.
For example, I have two date input fields at the level above. When I change the first one, I should automatically set the second one to "date of the first field + 1 day". To do this, I use setValue. This method will update the form's value but not the internal state's value. Therefore, the UI will not be updated.
I have to use useEffect, keep track of the form value change, compare it to the internal value, and update as needed.
useEffect(() => {
if (value !== momentValue.format(format)) {
setMomentValue(getMomentDateTime(value));
}
}, [format, momentValue, value]);
I wouldn't say I like this approach. A similar check for MultipleSelect looks even more complicated.
How can this problem be solved correctly? Thanks.

Too show different things depending on how many rows are selected on a table React js

I am using this libray https://material-table.com/#/ to make my table with react js.
In my table I use selection :
And actions as well :
What happens :
I have the same action(DELETE) when selecting a row and my positioning actions (SAVE, DELETE).
When clicking the selection action I want to show a dialog which title will be users if I select more than 1 row. When clicking my right action I want to show the same dialog but with the title user.
I am thinking in using ternary operator to make the condition like the following:
<DialogTitle>
rowsCount > 1 ? (
'Users'
) : (
'User'
)
</DialogTitle>
Question:
How can I know how many rows are selected and pass that data to my dialog in the const RowsCount?.
UPDATE :
Following the first answer given , to figure out if the table it's selected or not.. I've found a prop that my parent component(ModuleLayout) has : tableCanSelect which is boolean. The thing is that I don't know how to pass it to the child , which is my current table.
I suppose selected is a const .
I tried to do it like the following :
const MYTABLE : FC = () => {..
.. some other constants ..
const selected = useState<typeof ModuleLayout | null(ModuleLayout.tableCanSelect);
}
But it throws me this error :
Property 'tableCanSelect' does not exist on type 'FC<Props>'.
Which is not true because the prop tableCanSelect is indeed a prop of my parent component ModuleLayout.
Note : My dialog is a different component than my table which means I import my dialog to show it in my table.
I don't know anything about the table you're using, but I quickly looked up the documentation. You can use actions on your rows.
actions={[
{
icon: 'save',
tooltip: 'Save User',
onClick: (event, rowData) => alert("You saved " + rowData.name)
}
]}
The onClick part is what you need to count the amount of users. So you'd have something like this:
...
const Component = () => {
const [total, setTotal] = useState(0)
...
// table things here
data={[
// data here
]}
actions={[
{
icon: 'selection',
tooltip: 'Select',
onClick: (event, rowData) => selected ? setTotal(total + 1) : setTotal(total - 1),
]}
...
}
The code above is not completely correct, you'll have to figure out how to see if it's selected or unselected. But using that you can set the state and increase or decrease the total. Then you can use that total in your dialogue by passing it as a prop to change the title.
EDIT: Update to your update.
You need:
options={{
selection: true
}}
On your table according to https://material-table.com/#/docs/features/selection.
You need to store the state for your rows in the component that's importing the table. You can do this using:
const example = useState(true)
// or const example = useState(rowsSelected)
// depending on what your table needs
Doing:
const selected = useState<typeof ModuleLayout | null(ModuleLayout.tableCanSelect);
Doesn't really make sense, the reason why you're getting your error is likely because it needs to be useState<Props>. It will get the type of your component and try store that.
You also shouldn't store this state in the table itself, you should store it where you want to use it and pass it to the table as a prop. In this case you want to store it in the parent so that you can also use it in the dialogue, so you'll have to pass down the props to both components.
That being said I don't see any way to count all the selected rows in the documentation, nor do I see any way to count/select individual rows, so I'd really suggest that you either find a different table to use (chakra/semantic ui/something else) or better yet try and do it yourself without using 3rd party library stuff. Using 3rd party library stuff is nice until the point where you want to add custom things then it becomes really complicated.
As a tip, I can also see that you're trying to do a lot of things at once and getting confused between a lot of it, the table, the types, the parent/child component relationship and state updates, it's too much to try and learn and do all at once. Instead choose one thing at a time and make sure you understand it. Focus on learning just state, how props work, then focus on creating a child component and trying to use parent state in there, then learn about types and after that try and create a table in the way you want to. At that point rather create your own table without using a 3rd library so that you can customize it in the way you want and also so you'll learn a lot more.

React, conditional setState with the click of a button

So, I don't know if how I want this to work is possible at all, but in my head I think it could I just can't seem to get it to work..
Basically I have an empty array of objects:
this.state = {
orders: [{}]
}
Then when I click on a button I want an order to be added (custom event because cart has no parent-child relations to the button clicked, for reasons):
pubSub.on("addToCart", (data) =>
this.setState((prevState => ({
orders: [...prevState.orders, data.order]
})
);
This works. I end up with:
orders: [
{[sku]: sku, quantity: quantity}
]
Now the problem I am stuck with is that if an order is to be added with an SKU that is already in the array, I want just the quantity to be updated.
If the SKU isn't already found in the array, I want a new order added.
So I want to check the array for the existence of a key (key SKU is the same as the value of key SKU).
So, to check an array for the existence of a key I would use:
this.state.orders.map(obj =>{
if (!(data.sku in obj)){
**setState here with order added**
}
})
I know how the conditional should look like if it finds that the key exists, and then only update the quantity.
the problem I face is that it iterates over all the orders in the array, and every time the key doesn't exist, it adds a new order.
How could I make this, so it checks for the existence of a key, and only adds a new order 1 time if that's the case, and not every time?
I want actually to first check all the keys, and then decide what to do.
With this approach, I have to decide every round of the map what should be done.
Also keep in mind that we are in the scope of a custom event handler, where I want to use data.
inside this handler, I cannot assign variables outside the scope of the map function. So basically it's or I use just setState, or I use the map function and inside there the setState. I can't use them both next to each other as data is only available in the first method call.
This is also the first time I try to use this custom event handler thing, and the scoping has tied my hands a bit.
If this makes sense, and someone can point me in the right direction, that would be greatly appreciated.
Thanks in advance!
You can pass function in setState. This function will have current state and, if you use classes, current props. In this function you create state change - part of state object that you want to update. In your case it might look like this (it's barebones and dirty, to show how you can do it):
// data is here, somwhere
this.setState(function(state, props) {
let index = state.orders.findIndex(obj => data.sku in obj);
if (index > -1) {
// update existing
return {orders: state.orders.slice(0, index).concat(
{...state.orders[index], quantity: state.orders[index].quantitiy + 1},
state.orders.slice(index+1))}
} else {
// create new
return {orders: state.orders.concat(data)}
}
})

React storing Ref elements in Redux

How would you go about storing Ref elements in Redux and would you do it at all?
I have a component containing some form elements where I need to store the state of which field in the form the user had selected if they leave the page and come back.
I tried registering each input field in Redux like so (I'm using the <InputGroup> component from Blueprint.js):
<InputGroup
inputRef={(ref) => { dispatch(addRefToState(ref)) }}
...more props...
/>
That resulted in a circular JSON reference error since Redux is serializing the ref element to JSON in order to save it to localStorage. I then tried "safe" stringifying the object with a snippet I found here on Stackoverflow, removing any circular references before turning the object into JSON. That sort of works, but the Ref elements are still so huge that 3-5 refs stored in state turns into a 3MB localStorage and my browser starts being painfully slow. Further, I'm concerned whether I can actually use that Ref object to reference my components, know that I essentially modified the Ref object. I've not tried yet, because performance is so poor with the stringified objects.
I'm contemplating abandoning "the React way" and just adding unique IDs on each component, storing those in Redux and iterating over the DOM with document.querySelector to focus the right element when the page is loaded. But it feels like a hack. How would you go about doing this?
I am not sure if I would use them for that purpose but It would not be among the first ways to do that.
It is perfectly fine to have a React state to store a unique identifier of focused form element. Every form element, or any element in general, can have a unique identifier which can just be a string. You can keep them in your app's redux store in any persistence like web storage.
While you navigate away you can commit that state to your redux store or to persistence, by using a React effect.
const [lastFocusedElementId, setLastFocusedElementId] = useState();
useEffect(() => {
// here you can get last focused element id from props, or from directly storage etc, previously if any
if(props.lastFocusedElID) {
setLastFocusedElementId(props.lastFocusedElID);
}
// here in return you commit last focused id
return saveLastFocusedElementID(lastFocusedElementId) // an action creator that saves to the redux store before every rerender of component and before unmount
}, [props.lastFocusedElID]);
Alternatively
const [lastFocusedElementId, setLastFocusedElementId] = useState();
useEffect(() => {
const lastFocusedElID = window.localStorage.getItem(lastFocusedElementId);
if (lastFocusedElID) {
setLastFocusedElementId(lastFocusedElID);
}
return window.localStorage.setItem('lastFocusedElementId', lastFocusedElementId);
}, []);
Not to mention you need to use onFocus on form elements you want to set the last focused element ID. Id can be on an attribute of your choice, it can be id attribute, or you can employ data-* attributes. I showed id and data-id here.
<input onFocus={e => setLastFocusedElementId(e.target.id)} />
<input onFocus={e => setLastFocusedElementId(e.dataset.id)} />
Also needed a way to focus the last focused element with the data from your choice of source when you reopen the page with that form elements. You can add autoFocus attribute every element like
<input autoFocus={"elementID"===lastFocusedElementId} />
Lastly if your user leave the form page without focusing any element you might like to set the id to a base value like empty string or so. In that case you need to use blur events with onBlur handler(s).

Store checkbox values as array in React

I have created the following demo to help me describe my question: https://codesandbox.io/s/dazzling-https-6ztj2
I have a form where I submit information and store it in a database. On another page, I retrieve this data, and set the checked property for the checkbox accordingly. This part works, in the demo this is represented by the dataFromAPI variable.
Now, the problem is that when I'd like to update the checkboxes, I get all sorts of errors and I don't know how to solve this. The ultimate goal is that I modify the form (either uncheck a checked box or vice versa) and send it off to the database - essentially this is an UPDATE operation, but again that's not visible in the demo.
Any suggestions?
Also note that I have simplified the demo, in the real app I'm working on I have multiple form elements and multiple values in the state.
I recommend you to work with an array of all the id's or whatever you want it to be your list's keys and "map" on the array like here https://reactjs.org/docs/lists-and-keys.html.
It also helps you to control each checkbox element as an item.
Neither your add or delete will work as it is.
Array.push returns the length of the new array, not the new array.
Array.splice returns a new array of the deleted items. And it mutates the original which you shouldn't do. We'll use filter instead.
Change your state setter to this:
// Since we are using the updater form of setState now, we need to persist the event.
e.persist();
setQuestion(prev => ({
...prev,
[e.target.name]: prev.topics.includes(e.target.value)
// Return false to remove the part of the array we don't want anymore
? prev.topics.filter((value) => value != e.target.value)
// Create a new array instead of mutating state
: [...prev.topics, e.target.value]
}));
As regard your example in the codesandbox you can get the expected result using the following snippet
//the idea here is if it exists then remove it otherwise add it to the array.
const handleChange = e => {
let x = data.topics.includes(e.target.value) ? data.topics.filter(item => item !== e.target.value): [...data.topics, e.target.value]
setQuestion({topics:x})
};
So you can get the idea and implement it in your actual application.
I noticed the problem with your code was that you changed the nature of question stored in state which makes it difficult to get the attribute topics when next react re-renders Also you were directly mutating the state. its best to alway use functional array manipulating methods are free from side effects like map, filter and reduce where possible.

Resources