React useEffect acting strangely with checkboxes and previous data - javascript

So I am trying to pass in data previously selected in a form to an update version of the form. The update version of the form needs to display all previously selected data to the user and allow them to make any necessary changes.
Here is the problem I'm having. I am using a multi-select check box component. I am passing the previously selected data in to the component. When I set the selected property for component to the previously selected data using useEffect, It will let me submit the initial data or add new selections and everything functions correctly. It will not let me uncheck/remove selections. They get submitted even though they are unchecked.
If I don't use useEffect to set selected to previousData, I can not submit with the initial data but I can add and remove selections as intended. This is not acceptable though because users will most likely not make changes every time to the check boxes.
So to clarify, I can submit without making changes and add selections the first way and I can add and remove selections the second way but not submit without making changes the second way.
import { useState, useEffect } from 'react';
export default function UpdateMultiCheckBox({
title,
hint,
data,
previousData,
setSelectedData,
dotName,
}) {
const [selected, setSelected] = useState([]);
const handleChange = (event) => {
const { checked, value } = event.currentTarget;
setSelected((prev) =>
checked ? [...prev, value] : prev.filter((val) => val !== value),
);
};
{/* This is what is causing all the problems but it will not work without this without making a change to the selections */}
useEffect(() => {
let tempData = previousData?.map((a) => a.id);
setSelected(tempData);
}, [previousData]);
useEffect(() => {
setSelectedData(selected);
}, [selected]);
const difference = data?.filter(
(item) =>
!previousData?.some((itemToBeRemoved) => itemToBeRemoved.id === item.id),
);
return (
<fieldset className='space-y-5'>
<div>
<legend className='sr-only'>{title}</legend>
<label className='text-base font-medium text-brandText'>{title}</label>
<p className='text-sm leading-5 text-brandText'>
Please select all that apply.
</p>
</div>
{previousData?.map((item) => (
<div key={item.id} className='relative flex items-start'>
<div className='flex h-5 items-center'>
<input
id={item.id}
value={item.id}
defaultChecked={true}
type='checkbox'
onChange={handleChange}
className='h-4 w-4 rounded border-gray-300 text-brandPrimary focus:ring-brandPrimary'
/>
</div>
<div className='ml-3 text-sm'>
<label htmlFor={item.id} className='font-medium text-brandText'>
{item[dotName]}
</label>
<span id='comments-description' className='text-brandText'>
{hint}
</span>
</div>
</div>
))}
{difference?.map((item) => (
<div key={item.id} className='relative flex items-start'>
<div className='flex h-5 items-center'>
<input
id={item.id}
value={item.id}
type='checkbox'
onChange={handleChange}
className='h-4 w-4 rounded border-gray-300 text-brandPrimary focus:ring-brandPrimary'
/>
</div>
<div className='ml-3 text-sm'>
<label htmlFor={item.id} className='font-medium text-brandText'>
{item[dotName]}
</label>
<span id='comments-description' className='text-brandText'>
{hint}
</span>
</div>
</div>
))}
</fieldset>
);
}

If I understood correctly, the useEffect that you say is causing the problem, is setting the state with the previousData and loosing the selected state data.
If that is the case you could try the following:
useEffect(() => {
const tempData = previousData?.map((a) => a.id);
setSelected([...selected, ...tempData]);
}, [previousData]);
So now we are adding to the selected state the previousData, another way of doing it as seen in the wild:
useEffect(() => {
const tempData = previousData?.map((a) => a.id);
setSelected(prevState => [...prevState, ...tempData]);
}, [previousData]);
Which is effectively the same but in certain scenarios could be be useful when you only pass the state setter as a prop.

Related

Adding a default value to a Select list of options, where the options are read from a database

I have a Formik form which contains a field which uses the HTML Select tag to create a drop-down list. The data in this select drop-down comes from an array of data which is read from a MySQL database table by way of a useEffect hook. The fields in that array are staff_id and full_name. The user should see and select a full_name in the drop-down, then when the form is saved to the database, it should save the corresponding staff_id. This functionality is working in the code below.
My problem is that the first row of data from the array is displayed as the default value in the drop-down - e.g. the first name might be Joe Bloggs, and that's what appears when a user first opens the form. If the user tries to save the form at that point, without doing anything to the select drop-down, the form's save button does nothing - I presume because no option has actually been selected in the drop-down, so the 'value' is 'undefined'.
If the user wanted to select Joe Bloggs, they would need to ignore that Joe Bloggs was the displayed default option, select another option from the list, then go back and select Joe Bloggs again.
To prevent this situation, I've seen examples where, when the data is not sourced from a database, but instead an array of options is hardcoded, people add another key:value pair in the list of options, calling it something like "Please choose an option..." and a value of null or zero. That option becomes the default value which displays when the user first opens the form, in turn forcing the user to select a different option.
How would I achieve that same kind of functionality I've seen hardcoded, whilst still populating the options array from the database? Should I be amending another key/value pair to the top of the array that has been returned from the database, containing an option like "Please select from below and value='0'? Or is there some property that I can use to set a default value for the select drop-down list? Or some other way to achieve this that I've not considered?
Code:
import React, { useState, useEffect } from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup'; //yup does form validation
import axios from 'axios';
import { useMutation } from '#tanstack/react-query';
import { useRecoilState } from 'recoil'
import { eventButtonClickedState } from '../Atoms/atoms'
import Button from '#mui/material/Button'
// import Select from 'react-select'
//react-query useMutation code
const useEventsCreateMutation = () => {
return useMutation((formPayload) => {
return axios.post('http://localhost:3001/events', formPayload);
});
};
//Variable to store Tailwind css for 'Field' elements of Formik function
const formikField =
'my-px block px-2.5 pb-2.5 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border border-gray-400 appearance-none focus:outline-none focus:ring-0 focus:border-blue-600 peer';
//Variable to store Tailwind css for 'Label' elements of Formik function
const formikLabel =
'absolute text-base text-gray-500 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-gray-900 px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-6 peer-focus:top-1 peer-focus:scale-75 peer-focus:-translate-y-4 left-1';
//Main function - creates Formik form
function EventsCreate() {
const { mutate } = useEventsCreateMutation();
//Formik initial values (not actually used here)
const initialValues = {
event_name: '',
staff_id: '',
};
// Yup field validation
const validationSchema = Yup.object().shape({
event_name: Yup.string()
.required('*Event Name is required')
.max(35, 'Event Name can be a maximum of 35 characters'),
staff_id: Yup.number()
.required('*Event Leader is required'),
});
// State used to display success/error message
const [createMsg, setCreateMsg] = useState('');
// console.log(createMsg);
// Recoil global state to trigger data table refresh after event edit button is clicked
const [buttonisClicked, setButtonIsClicked] = useRecoilState(eventButtonClickedState)
// State for staff data to populate Event Leader dropdown
const[staff, setStaff] = useState([])
// console.log(staff)
// Gets array of staff ids/names from staff table
useEffect(() => {
axios.get('http://localhost:3001/staff/staffdropdown')
.then((res) => res.data)
.then(data => setStaff(data))
}, [])
return (
<div className="createEventPage px-5">
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values, formik) => {
mutate(values, {
onSuccess: () => {
setCreateMsg('New Event Created!')
setButtonIsClicked(buttonisClicked +1) //updates Recoil global state, to trigger data-table refetch of data
formik.resetForm();
},
onError: (response) => {
setCreateMsg('Error: Event not created - Keep Calm and Call Jonathan');
console.log(response);
},
});
}}
>
<Form className="formContainer">
<h1 className="pb-3 text-xl font-semibold">General Information</h1>
<div className="pb-2 relative">
<Field
className={formikField}
autoComplete="off"
id="inputCreateEvent"
name="event_name"
placeholder=" " />
<label className={formikLabel}>Event Name</label>
<ErrorMessage
name="event_name"
component="span"
className="text-red-600" />
</div>
<div className="pb-2 relative">
<Field
className={formikField}
as="select"
name="staff_id"
id="inputCreateEvent"
>
{staff.map(staff => {
return(
<option key={staff.staff_id} value={staff.staff_id}>{staff.full_name}</option>
)
})}
</Field>
<label className={formikLabel}>Drop Down</label>
</div>
<div className="flex flex-col items-center">
<Button variant="contained" size="large"
/* className="text-base text-white bg-blue-500 border hover:bg-blue-600 hover:text-gray-100 p-2 px-20 rounded-lg mt-5" */
type="submit"
>
Create Event
</Button>
</div>
<br></br>
<h1 className= {(createMsg ==="") ? "" :
((createMsg ==="New Event Created!") ? "text-xl text-blue-600 font-bold p-2 border border-blue-600 text-center":"text-xl text-red-600 font-bold p-2 border border-red-600 text-center")}> {/* This code only formats the class, hence shows the border, when a message is being displayed */}
{createMsg}
</h1>
</Form>
</Formik>
</div>
);
}
export default EventsCreate;
Per Randy's comment, the solution was to simply add another option above the {staff.map} array deconstruction. So as follows:
<div className="pb-2 relative">
<Field
className={formikField}
as="select"
name="staff_id"
id="inputCreateEvent"
>
<option>Select from the options...</option>
{staff.map(staff => {
return(
<option key={staff.staff_id} value={staff.staff_id}>{staff.full_name}</option>
)
})}
</Field>
<label className={formikLabel}>Drop Down</label>
</div>

Checkbox clicked style appears for all the checkboxes when i press dropdown option through useState in a different page in React/Next JS

So I have these sets of checkboxes for 3 different categories. 1 is "types", 1 is "Price" and the other is "Categories". they work perfectly. I have created a separate checkbox component and data is passed there without any problem.
The problem is when I click the checkbox and data is rendered but when I click the dropdown in the index page once and then press it again the data is the same as expected but the clicked style by that I mean"checked" is applied to all the checkboxes. is there any way to overcome this?
this is the separate checkbox component
import React, { useState } from "react";
interface Props {
isChecked?: boolean;
handleClick?: any;
label: string;
className?: string;
labelClassName: string;
handleChange?: any
}
const Checkbox = ({isChecked,handleClick,label,labelClassName,handleChange}: Props) => {
const [checking,setChecking]=useState(isChecked)
return (
<div>
<input
type="checkbox"
id={label}
checked={checking}
onClick={()=>{handleClick();setChecking(!checking)}}
onChange={handleChange}
className={`space-x-3 text-gray-400 bg-gray-800 border rounded border-stone-200 $`}
/>
<label className={labelClassName} htmlFor={label}>{label}</label>
</div>
);
};
export default Checkbox;
Main Index Page
// these states are for dropdown/dropup styling
const [categoryOpts, setCategoryOpts]= useState(true);
const [typeOpts, setTypeOpts]= useState(true);
const [priceOpts, setPriceOpts]= useState(true);
const [checking,setChecking]=useState(false)
const handleCheckChange=()=>{
setChecking(!checking)
}
index (){
<div>
// this is for showing/hiding price option
<div className='flex justify-between cursor-pointer' onClick={()=>setPriceOpts(!priceOpts)}>
<Text as='h5' tag='h2'>
Price
</Text>
{priceOpts?
<dropdown icon up/>
</svg> :
<dropdown icon down>
</svg>
}
</div>
// price option mapping that comes from api source.
{ priceOpts?
<div className='flex flex-col gap-2'>
{checkboxes.map(({ label, onClick, key }) =>
<div key={key}>
<Checkbox
labelClassName='text-sm text-gray-400 ml-3'
label={label}
handleClick={onClick}
isChecked={checking}
handleChange={handleCheckChange}
/>
</div>
)}
</div>: ''
}
</div>
}
<Checkbox
labelClassName="text-sm text-gray-400 ml-3"
label={label}
handleClick={onClick}
isChecked={checking} // this is the problem
handleChange={handleCheckChange}
/>;
You're setting checking prop using the same value for all your checkboxes in the main index page.
You need to have a checking parameter for each checkbox that you're rendering
{checkboxes.map(({ label, onClick, key }) right here. It should contain the state on whether the checkbox is checked or not. Like this
{checkboxes.map(({ label, onClick, key, checking })
You have to rework your checkboxes structure and checkbox even handling

Trying to filter an array using checkboxes and useState, checkbox doesn't stay checked

i have an array, called reportsData, then i need to filter it, generating some checkboxes with each of them having a label based on each name that comes from another array (emittersData), so basically i set it like this:
const [searchUser, setSearchUser] = useState<string[]>([])
const mappedAndFiltered = reportsData
.filter((value: any) =>
searchUser.length > 0 ? searchUser.includes(value.user.name) : true
)
Then i render my checkboxes like this:
function EmittersCheckboxes () {
const [checkedState, setCheckedState] = useState(
new Array(emittersData.length).fill(false)
)
const handleOnChange = (position: any, label: any) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
)
setSearchUser((prev) =>
prev.some((item) => item === label)
? prev.filter((item) => item !== label)
: [...prev, label]
)
setCheckedState(updatedCheckedState)
};
return (
<div className="App">
{emittersData.map((value: any, index: any) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
className="h-4 w-4 focus:bg-indigo border-2 border-gray-300 rounded"
type="checkbox"
id={`custom-checkbox-${index}`}
name={value.Attributes[2].Value}
value={value.Attributes[2].Value}
checked={checkedState[index]}
onChange={() => handleOnChange(index, value.Attributes[2].Value)}
/>
<label className="ml-3 font-medium text-sm text-gray-700 dark:text-primary" htmlFor={`custom-checkbox-${index}`}>{value.Attributes[2].Value}</label>
</div>
</div>
</li>
);
})}
</div>
)
}
And on the react component i am rendering each checkbox, that is a li, like:
<ul><EmittersCheckboxes /></ul>
And i render the mappedAndFiltered on the end.
Then it is fine, when i click each generated checkbox, it filters the array setting the state in setSearch user and the array is filtered.
You can check it here: streamable. com /v6bpk6
See that the filter is working, the total number of items in the array is changing based on the checkbox selected (one or more).
But the thing is that each checkbox does not become 'checked', it remains blank (untoggled).
What am i doing wrong, why doesnt it check itself?
You've defined your EmittersCheckboxes component inside another component. and every time that the parent component renders (by state change) your internal component is redefined, again and again causing it to lose it's internal state that React holds for you.
Here's a simplified example:
import React, { useState } from "react";
function CheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>CheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
export default function App() {
const [counter, setCounter] = useState(1);
function InternalCheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>InternalCheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
return (
<>
<InternalCheckboxeComponent />
<CheckboxeComponent />
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
</>
);
}
There's the App (parent component) with its own state (counter), with a button to change this state, clicking this button will increase the counter, causing a re-render of App. This re-render redefines a new Component named InternalCheckboxeComponent every render.
The InternalCheckboxeComponent also has an internal state (checkedState).
And there's an externally defined functional component named CheckboxeComponent, with this component React is able to hold its own state, because it's not redefined (It's the same function)
If you set the state of each to be "checked" and click the button, this will cause a re-render of App, this will redefine the InternalCheckboxeComponent function, causing React to lose its state. and the CheckboxeComponent state remains in React as it's the same function.

Remove dynamic rendered element from dom in ReactJS

Currently I've got a react component that looks like this:
const GeraCard = (cards, cart = false) => {
return cards.map((v, i) => {
return (
<div key={i} className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div>
);
});
};
Based on the cards array, it renders something like this:
Rendered Component
Whenever I click the trash button, I make a request to my backend, edit the list on my database and rerender the component based on the now updated "cards". The problem is that this takes sometime to happen, so i wanted a way to remove it from the dom instantly while my backend does it's job.
somehting like
{show ? renderCompoennt : null}
I've tried using vanilla javascript to grab the parent from the trash can, which would be the card i want to remove, but the results are unpredictable and it's quite slow as well.
My latest try was this:
const GeraCard = (cards, cart = false) => {
return cards.map((v, i) => {
const [show, setShow] = useState(true);
return (
<div key={i}>
{show ?
<div className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
setShow(false);
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div> :
null
}
</div>
);
});
};
but react won't let me do this. Even tho its fast, everytime one item gets deleted, react complains that "less hooks were rendered" and crashes the app.
You are attempting to do some Optimistic UI, in which you assume that your action will succeed, and reflect the expected/assumed state instantly, before the request to the backend completes. This would be in lieu of showing some progress/busy indicator, like a spinner, until the action completes with the server.
The first problem and immediate problem in your code-- it violates the rules of hooks, which state that hooks may only be used at the top-level (never inside loops, conditionals, etc).
The second problem is that you are leveraging vanilla JS to manipulate the DOM directly; this generally an antipattern in MV* frameworks, and very much so here. Instead, I would suggest doing managing it in your data model; something like this:
Rewrite your .map handler to return null if the card has a deleted property.
When the user clicks the trash button, do two things:
Make the request to the backend to delete it
Use a setState to add a deleted: true property to the clicked card
Now you will get a rerender that will omit the deleted card, and also make the request to the backend, all inside the React data model. Make sure that you handle complexity for:
How to handle the response
How to handle an error if the deletion fails at the backend
How to manage if a user quickly clicks many cards for deletion before any of the requests can complete.
The problem is that in the first render you have {cards.length} calls to hook "useState" within GeraCard, but after deletion of one card, you will have {cards.length-1} calls to hook "useState". As the React docs state:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function. By
following this rule, you ensure that Hooks are called in the same
order each time a component renders. That’s what allows React to
correctly preserve the state of Hooks between multiple useState and
useEffect calls.
You should extract the content of map callback into separate a component.
const GeraCards = (cards, cart = false) => {
return cards.map((v, i) =>
<GeraCard card={v} index={i} cart={cart} />
);
};
const GeraCard = ({ card, index, cart }) => {
const [show, setShow] = useState(true);
const v = card;
return (
<div key={index}>
{show ?
<div className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
setShow(false);
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div> :
null
}
</div>
);
}

How can I move my child component to new parent in React?

I'm rendering a list of child components which contain a checkbox, and when that checkbox is clicked, I want to move that child component inside another div element.
Here's an image of what my app looks nice. I'd like to check the student names and move them up, under the "Present" sub-heading..
let ClassComp = (props) => {
const { teacher, subject, students, day } = props.classOf
const renderStudents = (students) => {
if (students && students.length > 0) {
return (
<div>
{students.map((student, index) =>
<StudentCheckbox key={index} student={student} handleCheckboxClick={handleCheckboxClick} />
)}
</div>
)
} else {
return <p style={{ margin: '10px' }} >No students registered.</p>
}
}
const handleCheckboxClick = (elId) => {
const presentStudentEl = document.getElementById('present-students')
// move StudentCheckbox element inside this element ^
}
return (
<div className="ui segment" style={segmentStyle} >
<div className="content">
<div className="ui medium header">{teacher} - {subject}</div>
<div className="ui divider"></div>
<div className="ui sub header">Students</div>
<div className="ui tiny header">Present:
<div id="present-students"></div>
</div>
<div className="ui tiny header">Absent:
<div id="absent-students">
{renderStudents(students)}
</div>
</div>
<div style={{ marginBottom: '30px' }}>
<button className="mini compact ui negative right floated button"
onClick={() => setModalVisible(true)}>Delete Class
</button>
<Link to={`/todaysclass/edit/${props.classId}`} className="mini compact ui right floated button">Edit Class</Link>
</div>
</div>
</div >
)
}
const mapStateToProps = (state, ownProps) => {
return { classOf: state.classes[ownProps.classId] }
}
export default connect(mapStateToProps, { deleteClass })(ClassComp)
and here's my child component:
const StudentCheckbox = (props) => {
const uniqId = idGenerator()
return (
<div className="field" style={{ margin: '5px' }}>
<div className="ui checkbox">
<input type="checkbox" id={uniqId} onChange={() => props.handleCheckboxClick(uniqId)} />
<label htmlFor={uniqId}>{props.student}</label>
</div>
</div>
)
}
In this case, you'll need a state for your component. Take a look in the docs:
https://reactjs.org/docs/state-and-lifecycle.html
So basically, besides props (which are "fixed"), you'll have a state, which will change when you check the items.
Your render method will use the state to place the items either in one div, or the other. So all you have to do is use setState to change the state and the render method will redraw the new one.
You're using redux to manage state. That's good. It helps properly manage/manipulate data.
In this case you're trying to decorate a view without data changes [in redux store] - that's not good, it doesn't even make sense in react.
Rendered components/view is only a [derived] View from a Model(state) - in MVC. Moving some element from one div to another in DOM (if you implement this) doesn't change the [base] state - after rerendering you'll loose these kind of changes.
UPDATE:
You should keep students' presence in the store (default false). You need a separate action (with params/payload: classId, studentId), call API (in action creator to save attendence) and reducer for change 'flag' in redux store.
Each student will have isPresent property. You can simply change your renderStudents to render both divs (additional boolean argument and apriopriate filtering at the beginning).

Categories