Material UI - Array is resetting to 0 elements after onChange event. (TextField component) - arrays

Im currently building a application where i want people to choose from a list of users who they want to create a group chatroom with. After that, i have a TextField that allows them to set a name to their group.
The problem: The onChange event for the textfield seems to reset the array to 0 elements when typing. See GIF below.
Have anyone encountered something like this? I will paste my relevant code snippet down below.
return (
<div>
<Dialog
open={MultiplelistClick}
keepMounted
className='grpchatDialog'
onClose={() => setMultiplelistClick(false)}
aria-describedby="alert-dialog-slide-description"
fullWidth
maxWidth="xl"
>
<DialogTitle className='dialogTitle'>{"Choose users"}</DialogTitle>
<DialogContent>
<DialogContentText className="dialog-information">
2 minimum users required here.
</DialogContentText>
</DialogContent>
{
userData && userData.filter((val: any) => {
return (searchTerm === '' && (val.id != myIdNumber) || (val.firstName.toLowerCase().includes(searchTerm.toLocaleLowerCase()) && (val.id != myIdNumber)));
}).map((user: UserModel) => {
return <li key={user.id}>
<img className='liAvatar' src={placeholderimg} alt='xx' />
<p className='nameDisplay'onClick={() => createChatRoom(user)}> {user.firstName} {user.lastName}</p>
<Checkbox value={user.id} onClick={() => CheckBoxEvent(user.id)} className='myCheckBox' />
</li>
})
}
<TextField
label='Give your group a name.'
variant='outlined'
rows={1}
className='grpNameInput'
onChange={(e) => setGroupName(e.target.value)}
/>
<DialogActions>
<Button onClick={() => CreateGroupChatRoom(GroupChatList)}>Skapa Chattrum</Button>
</DialogActions>
</Dialog>
</div>
UPDATE: I verified it is actually the onChange typing event in my TextField that resets my current array of user ids to 0. See GIF here: https://gyazo.com/f306b6907ca755ad07fa13b542b87d27
For anyone wondering how my event for pushing in the user ids into the array, heres the event for that:
const CheckBoxEvent = (userid: number) => {
if (!GroupChatList.includes(userid)) {
GroupChatList.push(userid); console.log(GroupChatList)
}
else {
var index = GroupChatList.indexOf(userid);
GroupChatList.splice(index, 1);
}
}

React re-renders components frequently. By re-render, especially with Functional Components, we mean: "run the Function"...which re-creates the declared variables and functions within each and every time. So when the component re-renders, GroupChatList is a new variable reset to whatever the code within the component says.
To maintain a value in a variable across renders, use the useState hook to put the variable into "state".
Also, when you use the "set state function" to update the value in the state variable, that is the trigger to React that it needs to re-render the component (because "information in state has changed").

Related

Is there a way to clear a MUI TextField Input from the form.restart() method in React-Final-Form and capture the event?

The form.restart() in my reset button resets all fields states and values as per my understanding of this Final-Form.
The method fires and resets all fields in my form and I can capture the event in the autocomplete, but I am unable to capture the clear event in the textfield - I have a state (not related to the value of the field) I need tor reset.
My form reset button
<Button
type={"button"}
disabled={submitting || pristine}
variant={"outlined"}
onClick={() => {
form.getRegisteredFields().forEach((field) => form.resetFieldState(field));
form.restart();
if (clearActionHandler) {
clearActionHandler();
}
setFormSubmittedOnce(false);
}}
>
Clear
</Button>;
My textfieldadapter
const [shrink, setShrink] = useState < boolean > false;
const countCharacters: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => boolean = (e) => {
setCount(e.target.value.length);
return maxCharacterCount === 0 || e.target.value.length < maxCharacterCount;
};
return (
<TextField
{...input}
{...rest}
onChange={(e) => {
if (countCharacters(e)) {
input.onChange(e);
}
}}
value={input.value}
onBlur={(e) => {
!input.value && input.onBlur(e);
!input.value && setShrink(false);
}}
error={meta.error && meta.touched}
helperText={
meta.touched ? (
<React.Fragment>
{maxCharacterCount > 0 ? (
<React.Fragment>
<Typography variant={"body1"} textAlign={"end"}>
{count}/{maxCharacterCount}
</Typography>
<br />
</React.Fragment>
) : null}{" "}
{meta.error}
</React.Fragment>
) : maxCharacterCount > 0 ? (
<React.Fragment>
<Typography variant={"body1"} textAlign={"end"}>
{count}/{maxCharacterCount}
</Typography>
<br />
</React.Fragment>
) : (
""
)
}
placeholder={placeholder}
fullWidth={true}
margin={"dense"}
multiline={multiline > 1}
rows={multiline}
inputProps={inputProps}
InputProps={{
startAdornment: (
<InputAdornment position={"start"} sx={{ width: "24px" }}>
{startAdornment}
</InputAdornment>
),
}}
InputLabelProps={{
shrink: shrink,
}}
onFocus={() => setShrink(true)}
sx={{
"& .MuiInputLabel-root:not(.MuiInputLabel-shrink)": {
transform: "translate(50px, 17px)",
},
}}
/>
);
Versions of packages:
"#mui/material": "^5.11.1",
"react": "^18.2.0",
"react-final-form": "^6.5.9"
I have tried to capture the onChange event with bluring all elements before the reset method is called, that doesn't call the textfield onblur method. I am just not sure how to clear it away.
Make sure that the field in question has an entry in initialValues and that that entry is initialised to an empty string. This is a long shot, so take everything I say as theory, but it looks like what would happen if the initial value was not defined or was explicitly set to undefined.
That would mean when you reset the form, the value prop of the TextField would be set to undefined. This is not a valid controlled value of a TextField, so the behavior of MUI could well be to switch to uncontrolled mode, where the value is kept internally and not actually controlled by the value prop anymore. It depends on the library what happens next as this is generally unexpected and leads to non-deterministic behavior, but it's likely that MUI just keeps the previous controlled value around internally, or it's sitting in a transient DOM state on the element itself since value is judged to be no longer the source of truth and so it was never written to the actual DOM element. When the user types again, onChange would be called, the state in your form set to a string, and it would become controlled again. Base react <input> gives warnings in the console when you transition like this but MUI might not.
This might explain why it does not capture the transition to the original form state.
Note null is usually not allowed either but there's some final form magic that protects you from that one.
I verified MUI does this on a codesandbox (excuse the old class style I just used an old TextField sandbox to build on). If my long shot is correct, the problem is more related to a core issue between your state consistency and the mui lib and less about final form.
The fix would be to make sure the field has an entry in initialValues set to a string. You could optionally put in guards to check for undefined and use '' instead when that's the case.
Take note that '' is actually still falsey so !input.value would evaluate to true when its empty string still. Not sure you care, but something to keep in mind.

ReactJS: Autosave on fields within an accordion

I am using MUI Accordions and am trying to auto save the fields when the accordions are open.
Currently I am using a button within each accordion (x5 - each button has a different class name):
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography className={classes.heading}>Heading</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
<form>
...
<Button
disableElevation
variant="outlined"
onClick={(e) => onSubmit(e)}
color="default"
size="large"
type="submit"
className="btn2"
startIcon={<SaveAltTwoToneIcon />}
>
Save
</Button>
</form>
</Typography>
</AccordionDetails>
</Accordion>
I am then using:
let intervalID2;
useEffect(() => {
if (value == 2) {
intervalID2 = setInterval(() => {
const formSubmitButton = document.querySelectorAll(".btn2, .btn3, .btn4, .btn5, .btn6");
if (formSubmitButton != null) {
formSubmitButton.forEach(el=>el.click())
}
}, 1000);
}
return () => clearInterval(intervalID2);
}, [value == 2]);
To loop through each accordion to simulate the button click. Not ideal, but it sort of works - but the performance is super slow and I cannot actually click on anything else on the page. These accordions are all within the same tab as well.
How can I trigger the autosaving of the fields only when the accordion is open?
Inspecting the Dom and extracting values isn't a "React" way to do things.
See this link, specifically the section on controlled components.
You want to have a state for each text field, where the state is linked to the current value of the field. So e.g.:
const [email, setEmail] = useState("")
return <Textfield value={EMAIL} onChange={e => setEmail(e.target.value}> // note value, not defaultValue
That way, the state is always "saved" in the component state. I'm not 100% sure what you meant by saved though. If you meant saved if they go to another route, you could save to Redux state. If you meant saved if they refresh the page, you could save to local storage. You could do these by having a useEffect that has the field state be in the dependency array, so it gets invoked whenever the field changes, and save the logic there.
If you meant save to the backend, and want to batch it so it only does that when the accordion is closed, you could have a useEffect that has the accordion state as a dependency, that gets called when true accordion changes. You could keep track of the last open accordion with a state or useRef, and upload all the state in the previously open accordion.
If I'm misunderstanding the question, let me know!

how can I get the current value when I map an array in react

I want to get the current value and save it in the state but I get too many renders...
I'm mapping through data then I want to get render the UI based on that but also get the current data in the display so I can use it in another function in the same component.
{DATA.map(({ title, fields }) => {
setCurrentFields(fields.map((field) => field.name));
//this above cause the error but I want find a way to save that part of the data somewhere
return (
<Step label={title} key={title}>
<Box
textAlign="center"
p="3"
overflowY="auto"
height="100%"
>
{fields.map(
({
name,
validation = { required: true },
placeholder,
type,
}) => {
console.log(fields.length);
return (
<FormField key={name} errors={errors} name={name}>
<Input
id={name}
name={name}
placeholder={placeholder}
type={type}
{...register(name, validation)}
/>
</FormField>
);
}
)}
</Box>
</Step>
You can't do state management inside a render because state update will trigger a rerender every time you update. What you should do is iterate this DATA array somewhere outside the render and update states there. Useeffect is probably what you are looking for.
Also, take into consideration that you are rewriting the state for each element on the data array, so in the end you will only have the state of the final element saved in you currentValues state.

React toggling between clickable words that pull up <Elements/>

Somewhat new to React.
I want to be able to toggle between React elements CreateUser, EditUser, and ListUser.
It would be great to have a clickable text that when selected pulls up that element and its
corresponding stuff. In the case of Create its a field with a save button.
What is the best way to have my CrudSelector class list all three texts, make them clickable and pull up the stuff inside toggling to the selected one at a time?
Welcome. Always a good idea to share code with your question.
You'll want to use some kind of state which tells your parent component which child component to show. Kind of like this in your React functional component:
const [ featureToShow, setFeatureToShow ] = useState('list')
return (
<>
{/* Show some buttons to change the state */}
<button onClick={() => setFeatureToShow('list')}>Show User List</button>
<button onClick={() => setFeatureToShow('create')}>Create User</button>
<button onClick={() => setFeatureToShow('edit')}>Edit User</button>
{/* Depending on what is in the state show that component */}
{ featureToShow === 'list' ? <ListUser /> : null }
{ featureToShow === 'create' ? <CreateUser /> : null }
{ featureToShow === 'edit' ? <EditUser /> : null }
</>
)
In reality you'll probably want to enable clicking on the user's name in ListUser to show the EditUser component.

How to render multiple objects based on a changing number

I've done this in Vue before with v-for. But in React I want to add a TextField based on a number in my state. I have a state:
constructor(...props){
super(...props)
this.state = {
questions: 1
}
}
My component will have a button which will add 1 to questions if pressed. And I want to render TextFields based on the number in the state (questions). By that same logic I also plan on doing a remove button which will remove one from the total, and will also remove one of the TextFields. I tried this:
render(){
let category_rows = []
for(var i = 0; i < this.state.questions; i++){
category_rows.push(<TextField variant='outlined' />)
}
return(
<Card>
<CardContent>
<Typography>
Add Question Categories
</Typography>
{category_rows}
</CardContent>
<Button onClick={() => {
this.state.questions += 1
console.log(this.state)
}}>Add</Button>
</Card>
)
}
}
And it renders the first time but when pressing my button and adding to the state, it doesn't add a new field like I want.
While I'm at it, if someone could tell me if it's possible to take the code on the Button's onClick and put it in a function, then call the function on the onClick. I tried it but when I log this it says it is undefined.
you are changing the state variable wrong
inside your Button instead of doing this
<Button onClick={() => {
this.state.questions += 1
console.log(this.state)
}}>Add</Button>
do this
<Button onClick={() => this.setState({questions : this.state.questions +1 })}>Add</Button>
Because in react we shouldn't update the state variable directly. otherwise it will not trigger re render and wont update.
You are updating the state wrong way. State should always be immutable and should only be updated by using setState function
<Button onClick={() => {
this.setState({questions: this.state.questions += 1})
console.log(this.state)
}}>Add</Button>

Resources