Input value not resetting on re-render of React component - javascript

I have a React component which displays a modal. Displays fine the first time. But when I close and reopen it "holds" the input value: The code for the render method is:
render() {
return (
<>
<Button
type="primary"
onClick={() => {
this.setState({
topup: 0,
modalVisible: true
});
}}
>
Replace
</Button>
<Modal
visible={this.state.modalVisible}
onOk={this.onOk}
onCancel={() => {
this.setState({
topup: 0,
modalVisible: false
});
}}
>
<Form>
<Row gutter={24}>
<Col span={24}>
<Descriptions>
<Descriptions.Item label="Topup">
<Input
type="number"
defaultValue={this.state.topup}
onChange={value => {
console.log('value.target.value is ', value.target.value);
if (Number(value.target.value)) {
this.setState({
...this.state,
topup: Number(value.target.value)
});
}
}}
/>
</Descriptions.Item>
</Descriptions>
</Col>
</Row>
</Form>
</Modal>
</>
);
}
When it first displays, the value in the topup Input is 0, which is correct. If I change to 10, then close the modal, I reset the state so the topup is 0. When I click the button, somehow, the Input still has 10. I can see in the state the topup property is 0. What could possible be going on?

It might be because that Input is uncontrolled, as in you only provide default value, so maybe try using value instead of defaultValue if you want value to always come from state.
This is irrelevant to your problem, but if you're using this.setState method of class component, you don't need to spread this.state, because whatever you pass into this.setState will be merged with current state. Also using old state to create new state like that can lead to bugs, so if you ever need to use old state to generate new one (for example, you want to increment state variable), you should pass a function into this.setState:
this.setState((prevState)=>{
return {
value: prevState.value + 1
};
});
That way React will handle your setState properly and guarantee prevState is actual current state at the moment setState is running.

Related

React-hook-form working with multiple array data and conditional fields within a map array

I'm lost in the weeds on this, but I think the issues are straightforward enough, and its just my lack of understanding as to why I cant get this working right. I have a form using react-hook-form that is part of a scheduling/ documentation feature. The initial data is pulled from 1 api endpoint which sets the initial info in the parent level of the form- the standard date/time info and the subsequent conditional goal info if the event has already been interacted with- as an 'event' prop. For the child component within the form (GoalInput), the goal titles are pulled from a separate api endpoint to ensure the available goal fields match the current report. Since the first time a user will interact with any given event, the goal fields should be un-toggled and have no associated user information, however, if they are returning to an event previously interacted with, I want the previously set information (contained in the event initial data mentioned earlier) displayed as the default.
Heres the parent form
/.../
const { register, unregister, handleSubmit, watch, control, setValue, formState: { errors } } = useForm({
defaultValues: {
visitStart: event?.visitStart,
visitEnd: event?.visitEnd,
location: event?.location,
goals: [{
title: '',
marked: false,
note: ''
}]
},
shouldUnregister: true
});
const onSubmit= async (data) => {
/.../
}
return (
<div>
<Button color='primary' variant='contained' onClick={handleClickOpen}>
Edit Visit
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Edit Visit</DialogTitle>
<DialogContent>
<DialogContentText>
Visit for {event.client.fullName}
</DialogContentText>
<form id="editVisit"
onSubmit={(e) =>
handleSubmit(onSubmit, onError)(e).catch((e) => {
console.log("e", e);
})}>
<br></br>
<section>
/... initial fields .../
</section>
{goals && goals.map((goal, index) => (
<GoalInput
key={goal._id}
goal={goal}
index={index}
register={register}
control={control}
errors={errors}
visitGoals={event.goals}
setValue={setValue}
unregister={unregister}
/>
))}
/... end of form/ action buttons .../
And the child component:
function GoalInput({ goal, index, register, unregister, setValue, control, errors, visitGoals }) {
const [toggle, setToggle] = useState(false)
console.log("goal: ", goal)
console.log("visitGoals: ", visitGoals)
const goalData = visitGoals?.filter((value)=> {
if ( value.marked === true) {
return value
}
})
console.log("goalData: ", goalData)
useEffect(() => {
if(!toggle) {
unregister(`goals.${index}.title`)
unregister(`goals.${index}.marked`)
unregister(`goals.${index}.note`)
}
}, [unregister, toggle])
return (
<>
<FormControlLabel
{...register(`goals.${index}.title`, goal.title)}
value={goal.title}
name={`goals.${index}.title`}
control={
<Switch
key={index}
{...register(`goals.${index}.marked`)}
checked={goalData.marked || toggle}
// checked={toggle}
name={`goals.${index}.marked`}
value={goalData.marked || toggle}
onClick={console.log("marked? ", goalData.marked, "toggle ", toggle)}
// value={toggle}
onChange={
() => {
setToggle(!toggle);
setValue(`goals.${index}.title`, goal.title)
}}
/>
}
label={goal.title}
/>
<br />
{toggle ? (
<>
<Controller
control={control}
name={`goals.${index}.note`}
id={`goals.${index}.note`}
render={({field}) => (
<TextField
index={index}
error={!!errors.note}
value={goalData.note || field.value}
// value={field.value}
onChange={(e)=>field.onChange(e)}
label="Progress Note"
/>
)}
/>
<br />
</>
) : <></>}
</>
)
}
The visitGoals prop is passing down the event information if it already contains existing goals. Currently the log is showing that the component is correctly filtering out if the goals had been marked: true previously, however, the actual Switch component is not registering the goalData value. I tried setting it as state and having a useEffect set the state, but I was getting just an empty array. I'm sure theres something simple I'm missing to get the input fields to recognize the values, but I cant figure it.
As an extra question if I may, I'd also like to unregister any fields if the Switch input is not toggled, so that its false, so that I'm not storing a bunch of empty objects. Following the docs and video, I thought I've set it up correctly, even trying shouldUnregister: true in the parent form, but I can't seem to navigate that either. The submission data shows the fields are being registered fine by RHF, so I figured the unregister by the same syntax should have worked.
React Hook Form's unregister docs: https://react-hook-form.com/api/useform/unregister
Any direction or guidance would be greatly appreciated.

Asynchronous State affecting conditional display of Element

I have a screen (parent) where a FlatList resides in, and the renderItem shows a Child element.
In this Child element, I have a Pressable, and when the user clicks on it, it shows a Checked Icon and changes its background colour.
This happens dynamically based off a state Array in the Parent where in each renderItem of the Child I pass the state Array.
And in the Child component I check if the ID of this Child element is present, if it is, a Checked Icon is shown and the background changes colour.
I know that states in React is asynchronous but I'm always having problems working through such scenarios.
I have tried checking in the Parent screen where the FlatList resides at, to instead pass a Boolean prop to the Child on whether to show the Checked Icon.
E.g. (Sorry always having trouble formatting code in SO)
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
isSelected={selectedIds?.includes(item)}
// selectedIds={selectedIds}
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
instead of
// In Parent Screen
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
selectedIds={selectedIds} // here
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
// In Child element
const Child = ({
id,
selectedIds,
selectedHandler
}) => {
return (
<Pressable
style={[
styles.checkContainer,
selectedIds?.includes(id) && { backgroundColor: '#3D9A12' }
]}
onPress={onPressHandler}
>
{selectedIds?.includes(id) && <CheckIcon />} {/* Problem lies here. Not showing Checked Icon */}
</Pressable>
);
};
I won't dump any code here as I have made a snack of the reproduction of my problem.
I appreciate any help please. Thank you so much
Unchecked:
Checked:
The problem is in the selectedHandler function.
You are storing the reference of your state in this variable.
let selectedArr = selectedIds;
and later directly modifying the state itself by doing so:
selectedArr.push(id);
This is why the state updation is not firing the re-render of your component.
Instead, what you need to do is:
let selectedArr = [...selectedIds];
By spreading it, you will be storing a copy of your array and not a reference to it. Now if you modify selectedArr, you won't modifying your state.
I made the changes in the snack provided by you and it now works fine.
The updated selectedHandler function:
const selectedHandler = id => {
let selectedArr = [...selectedIds];
console.log('before selectedArr', selectedArr);
if (selectedArr.includes(id)) {
selectedArr = selectedArr.filter(userId => userId !== id);
setSelectedIds(selectedArr);
console.log('after selectedArr', selectedArr);
return;
}
if (selectedArr.length > 2) {
selectedArr.shift();
}
selectedArr.push(id);
console.log('after selectedArr', selectedArr);
setSelectedIds(selectedArr);
};

React onclick function produces increments state instead of changing it

I am trying to implement a function that sets a property of the state, "changedMarkup" on a button click event.
Constructor
constructor() {
super();
this.state = {
value: 0,
changedMarkup: 0
};
}
Render
render() {
const { classes } = this.props;
return (
<Paper className={styles.root}>
<Tabs
value={this.state.value}
onChange={this.handleChange}
variant="fullWidth"
indicatorColor="primary"
textColor="primary"
aria-label="icon label tabs example"
>
<Tab onClick={() => this.changeMarkup(0)} icon={<TrendingUpIcon />} label="TRENDING" />
<Tab onClick={() => this.changeMarkup(1)} icon={<ScheduleIcon />} label="NEW" />
<Tab onClick={() => this.changeMarkup(2)} icon={<WhatshotIcon />} label="HOT" />
</Tabs>
</Paper>
);
}
changeMarkup function
changeMarkup = (markup) => {
this.setState({
changedMarkup: markup
})
console.log("markup", this.state.changedMarkup);
}
Expected behavior
Log statement when the first tab is clicked: markup 0
Log statement when the second tab is clicked: markup 1
Log statement when the third tab is clicked: markup 2
Resulting behaviour
The "changeMarkup" property produces unexpected values. I can't seem to find an exact common pattern but it seems to be increment from 0 to 2 and decrements back to 0 with continuous clicks irrespective of the tab clicked
Any help is appreciated.
See: https://reactjs.org/docs/react-component.html#setstate
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState is an async operation, it won't be complete at the time you do your console logging. You can supply a callback after it has been updated:
this.setState({changedMarkup: markup}, () => {
// Do your logging here!
});
Because this.setState({}) is async operation so if you want updated value log than you can do it in two ways.
1. use callback function like this
this.setState({
//set your state
}, () => {
console.log('your updated state value')
})
2. in render function like this
render(){
console.log('your state')
return()
}

React Native not rendering on prop change

I have created the following component:
type ToggleButtonProps = { title: string, selected: boolean }
export default class ToggleButton extends Component<ToggleButtonProps>{
render(){
return (
<TouchableWithoutFeedback {...this.props}>
<View style={[style.button, this.props.selected ? style.buttonSelected : style.buttonDeselected]}>
<Text style={[style.buttonText, this.props.selected ? style.buttonTextSelected : style.buttonTextDeselected]}>{this.props.title}</Text>
</View>
</TouchableWithoutFeedback>
);
}
}
The styles are simple color definitions that would visually indicate whether a button is selected or not. From the parent component I call (item is my object):
item.props.selected = true;
I've put a breakpoint and I verify that it gets hit, item.props is indeed my item's props with a selected property, and it really changes from false to true.
However, nothing changes visually, neither do I get render() or componentDidUpdate called on the child.
What should I do to make the child render when its props change? (I am on React Native 0.59.3)
You can't update the child component by literally assigning to props like this:
item.props.selected = true;
However, there are many ways to re-render the child components. But I think the solution below would be the easiest one.
You want to have a container or smart component which will keep the states or data of each toggle buttons in one place. Because mostly likely, this component will potentially need to call an api to send or process that data.
If the number of toggle buttons is fixed you can simply have the state like so:
state = {
buttonOne: {
id: `buttonOneId`,
selected: false,
title: 'title1'
},
buttonTwo: {
id: `buttonTwoId`,
selected: false,
title: 'title2'
},
}
Then create a method in the parent which will be called by each child components action onPress:
onButtonPress = (buttonId) => {
this.setState({
[buttonId]: !this.state[buttonId].selected // toggles the value
}); // calls re-render of each child
}
pass the corresponding values to each child as their props in the render method:
render() {
return (
<View>
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonOne} />
<ToggleButton onPressFromParent={this.onButtonPress} dataFromParent={this.state.buttonTwo} />
...
finally each child can use the props:
...
<TouchableWithoutFeedback onPress={() => this.props.onPressFromParent(this.props.dataFromParent.id)}>
<View style={[style.button, this.props.dataFromParent.selected ? style.buttonSelected : style.buttonDeselected]}>
...
I left the title field intentionally for you to try and implement.
P.S: You should be able to follow the code as these are just JS or JSX.
I hope this helps :)
Because children do not rerender if the props of the parent change, but if its STATE changes :)
Update child to have attribute 'key' equal to "selected" (example based on reactjs tho')
Child {
render() {
return <div key={this.props.selected}></div>
}
}

Passing dynamic onChange listener to children

I have a stateful component that holds some state
state={
name:'',
age:'',
occupation:''
}
And a function to update the state onChange listener
onValueChange = (key, event) => {
this.setState({ [key]: event.target.value });
};
I pass the state and function down to child as props
<ComponentA {...this.state} changed={this.onValueChange}>
Inside my component B which is a child of A I want to programatically create inputs based on given props and change the state by invoking that function every time user types in input.
<ComponentB>
{ Object.entries(this.props)
.filter(
prop =>
prop[0] !== 'changed'
)
.map(propName => (
<Input
key={propName}
label={propName[0]}
value={propName[1]}
onValueChange={this.props.changed(propName[0])}
/>
))}
</ComponentB>
My Input component just renders the following
<input
onChange={this.props.onValueChange}
value={this.props.value}
type={this.props.type}
placeholder=" "
/>
Can't make it work for some reason.
Thanks for any help!
You are currently invoking this.props.changed straight away on render by writing onValueChange={this.props.changed(propName[0])}. Instead of invoking it on render you should give it a function to call when onValueChange occurs instead.
You also want to give the Input a unique key prop that will not change between state updates, so that React doesn't create an entirely new component every time and you e.g. lose focus of the input. You can use propName[0] instead, which will be unique.
<Input
key={propName[0]}
label={propName[0]}
value={propName[1]}
onValueChange={event => this.props.changed(propName[0], event)}
/>

Categories

Resources