Set input values on receiving changed prop from redux - javascript

I am trying to set the change the input value to its previous redux state value. For that, I am storing each redux state as history node. When a user presses ctrl+z in the form, my action creator should fetch the previous history node and find the input value(which is being done) and set the input value. I have cross-checked whether the input component is being rerendered or not with props being changed.
*My history node is double linked list if that helps
Although the props are being changed, the input value is not getting updated.
So, I have thought of updating the input field with componentWillRecieveProps life cycle where I set the ref to input component and by accessing the ref in life cycle hook, the input value gets updated.
For basic inputs like a single field in the form, it is easy to access the ref. I have a field which is an array of objects consisting of 3 inputs fields. Now, if I want to access these refs in componentWillRecieveProps life cycle hook, I should set the ref for each input box which is quite complicated since the array is dynamic.
Consider the array structure as
persons:[
{
firstName: 'a',
middleName: 'b',
lastName: 'c'
},
{
firstName: 'd',
middleName: 'e',
lastName: 'f'
}
.
.
.
]
**MainComponent**
class MainComponent extends Component{
componentWillReceiveProps = (nextProps) => {
const { title, subtitle, sponsor, email, subject } = this.props;
if (nextProps.title !== title) {
this.uncontrolledInputRefs.title.current.value = nextProps.title;
}
}
uncontrolledInputRefs = {
title: React.creatRef()
}
onKeyDown = e => {
if(e.key == 'z' && e.ctrlKey){
e.preventDefault();
this.props.undoHistory()
}
}
render(){
return(
<Form onKeyDown={onKeyDown}>
<Title
value={title}
handleTitleChange={(e) => {
const value = e.target.value;
this.props.handleTitleChange(value);
}}
refer={this.uncontrolledInputRefs.title}
/>
<div>
{persons.map((person,id)=>{
<input value={person.firstName} handlePersonChange={e=>{
this.props.handlePersonChange('firstName',e.target.value,id)
}}/>
<input value={person.middleName} handlePersonChange={e=>{
this.props.handlePersonChange('middleName',e.target.value,id)
}}/>
<input value={person.lastName} handlePersonChange={e=>{
this.props.handlePersonChange('lastName',e.target.value,id)
}}/>
})}
<button onClick={this.addPerson}>AddPerson</button>
</div>
</Form>
);
}
}
mapStateToProps = state => {
title: state.reducer.title,
persons: state.reducer.persons
}
mapDispatchToProps = dispatch => {
handleTitleChange: value => dispatch(actions.handleTitleChange(value)),
handlePersonChange: (field,value) =>
dispatch(actions.handlePersonChange(field,value)),
undoHistory: _ => dispatch(reducer.undoHistory())
}
initialState
initialState = {
title: null,
persons: [{firstName: null, middleName:null, lastName:null}]
}
reducer
const reducer = (state=initialState,action) => {
switch(action.type){
case actionTypes.CHANGE_TITLE:
return{
...state,
title: action.payload.title
}
case actionTypes.CHANGE_PERSON:
return{
...state,
[action.payload.persons[action.id].[fieldToModify]:
action.payload.value
}
}
}
If the problem gets resolved at the root which is without using any life cycle hook and updating the input value, the life cycle update can be skipped.

Related

Cannot read property React function component issue

I'm having this issue with a function component that it's giving me:
TypeError: Cannot read property 'name' of undefined
These are the parts that implements the 'name' input:
const initialState = { user: { name: '', email: '' }, list: []}
...
export default (props) =>
const [state, setState] = useState(initialState);
const user = state.user;
...
function handleChange (event){
setState({...state.user,
[event.target.name]: event.target.value})
}
<TextField
className="form-control"
name="name"
placeholder="Digite o nome..."
type="text"
value={state.user.name}
onChange={(e) => handleChange(e)}
required={true}
/>
I know I could easly do this with a Class Component, but i would like to do with a functional one, is it possible?
Issue
I think the issue is one, or both, of the following, both in the handleChange callback function.
You don't shallow copy the nested state correctly. {...state.user spreads the user values up a level to the root of the state object.
The onChange event has been nullified by the time the state update is processed.
Solution
useState state updater doesn't merge state updates, you need to shallow copy all parts of state that are being updated, and in this case that is state and state.user. I suggest also using a functional state update since you are updating from the previous state.
The onChange event is possibly nullified so you should destructure/save the input's name and value properties before enqueueing the state update.
Code:
function handleChange(event) {
const { name, value } = event.target; // <-- save name and value
setState(prevState => ({
...prevState, // <-- shallow copy previous state
user: {
...prevState.user, // <-- shallow copy previous state.user
[name]: value, // update property
},
}));
}

React state not changing

I have a react component like this:
const students: [
{id: '', name: '', age: '', attendance: ''}
//... 1000 entries
]
class Students extends React.Component {
constructor (props) {
super(props)
this.state = {
studentID: 1
}
}
createMonthlyChart = () => {
const { studentID } = this.state
let computeScore = students.attendance.map (
item => {
//... get attendance by id
}
)
return chartData
}
handleOnChange = value => {
console.log(value) // student key
this.setState({
studentID: value
})
this.createMonthlyChart() // not working
}
render () {
return (
<div>
// ant design component
<Select
defaultValue={type}
onChange={this.handleOnChange}
>
students.map((student, key) => (
<Option key={student.id}> {student.name} </Option>
))
</Select>
</div>
)
}
}
That is the just idea
I am not sure if I am using setState wrongly but somehow I get unexpected value
For example, the first time I click on a student, I don't get any chart visualization even though the data is there, I have to press it second time to get any chart.
And If I click on student without any attendance, I get empty chart after that for all students. I have to refresh the page to restart
To me it seems like you don't need the studentID state at all, you could directly call this.createMonthlyChart passing the value as a parameter to the function.
But if you need it for some other reason you can invoke that function as a callback to the setState like this:
this.setState({
studentID: value
}, this.createMonthlyChart)
I see a couple of things
The option should have the value
<option key={student.id} value={student.id}> {student.name}</option>
createMonthlyChart, should be called after updating the state (second parameter)
And you should use event.target.value
handleOnChange = event => {
this.setState({
studentID: event.target.value,
}, this.createMonthlyChart);
};
And for the first time, you can use componentDidMount
componentDidMount() {
this.createMonthlyChart();
}
And don't forget to initialize the state with the first student, like this
this.state = {
studentID: students[0].id,
};

Best approach for using same component for editing and adding data. Mixing component state with redux store

I'm building web app in React with Redux. It is simple device manager. I'm using the same component for adding and updating device in database. I'm not sure, if my approach is correct. Here you can find parts of my solution:
UPDATE MODE:
In componentDidMount I'm checking, if deviceId was passed in url (edit mode). If so, I'm calling redux action to get retrieve data from database. I'm using connect function, so when response arrives, It will be mapped to component props.
Here is my mapStateToProps (probably I should map only specific property but it does not matter in this case)
const mapStateToProps = state => ({
...state
})
and componentDidMount:
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
Next, componentWillReceiveProps will be fired and I will be able to call setState in order to populate inputs in my form.
componentWillReceiveProps(nextProps) {
if (nextProps.devices.item) {
this.setState({
id: nextProps.devices.item.id,
name: nextProps.devices.item.name,
description: nextProps.devices.item.description
});
}
}
ADD MODE:
Add mode is even simpler - I'm just calling setState on each input change.
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
That's how my inputs looks:
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={this.state.description}
/>
I don't like this approach because I have to call setState() after receiving data from backend. I'm also using componentWillReceiveProps which is bad practice.
Are there any better approaches? I can use for example only redux store instead of component state (but I don't need inputs data in redux store). Maybe I can use React ref field and get rid of component state?
Additional question - should I really call setState on each input onChange?
To avoid using componentWillReceiveProps, and because you are using redux, you can do:
class YourComponent extends React.Component {
state = {
// ...
description: undefined,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.description === undefined && nextProps.description) {
return { description: nextProps.description };
}
return null;
}
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
// ...
render() {
let { description } = this.state;
description = description || ''; // use this value in your `TextField`.
// ...
return (
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={description}
/>
);
}
}
const mapStateToProps = (state) => {
let props = { ...state };
const { devices } = state;
if (devices && devices.item) {
props = {
...props,
id: devices.item.id,
name: devices.item.name,
description: devices.item.description,
};
}
return props;
};
export default connect(
mapStateToProps,
)(YourComponent);
You can then access id, name, and description thought this.props instead of this.state. It works because mapStateToProps will be evaluated every time you update the redux store. Also, you will be able to access description through this.state and leave your TextField as is. You can read more about getDerivedStateFromProps here.
As for your second question, calling setState every time the input changes is totally fine; that's what's called a controlled component, and the react team (nor me) encourage its use. See here.

Serialize dynamic form into array of objects

Here is my initial state in redux
grains: [{grainId: "", amount: "0.0"}],
Im trying to get my form to serialize to something similar but I cant find a way to have grains be the outer object. Im wondering if its even possible or if I'll have to recreate the object manually. Or just get the state from the store.
My form looks like this:
<select
name='grainId'
value={this.props.value}
>
...options
</select>
<input
type="text"
name='amount'
value={this.props.value}
/>
Then theres an add grain button which will add another object to the array in the state which will then render another section of the form on the page. Is there anyway to wrap each "Grain" section in a form element so that is serializes nicely?
If not can I just post the state from the store on form submit or is that bad practice?
Since you are already loading your initial form data from the redux state, I think it would make sense to have the form update the Redux state as it changes. With this approach every input chnage would trigger an update of the Redux state, which would then be passed down to the component (through mapStateToProps), and cause a re-render showing the new value. That way you can make sure, in your reducer, that the state always has the shape you prefer ( {grains: [{grainId: "", amount: "0.0"}, etc... ] ).
Like you hinted, that would mean that when you finally submit, you are basically submitting the Redux form state (or at least the props passed down from it).
That could look something like this (runnable JSFiddle demo here):
class App extends React.Component {
renderGrains() {
const {grains, changeGrain} = this.props;
return grains.map((grain, index) => (
<div key={ index }>
<input
type="text"
value={grain.grainId}
onChange={({target:{value}}) => changeGrain(index, 'grainId', value)}
/>
<input key={ grain.grainId + 'amount' }
type="number"
value={grain.amount}
onChange={({target:{value}}) => changeGrain(index, 'amount', value)}
/>
<br />
</div>
));
}
render() {
const {addEmptyGrain, grains} = this.props;
const onSubmit = (e) => {
e.preventDefault();
alert('submitting: \n' + JSON.stringify({grains}, null, 2));
};
return (
<form>
{ this.renderGrains() }
<button onClick={ (e) => { e.preventDefault(); addEmptyGrain();} }>Add grain</button><br />
<button onClick={onSubmit}>Submit</button>
</form>
);
}
}
where the reducer would look something like this:
const r1_initialState = { grains: [{grainId: "", amount: 0.0}] };
const reducer1 = (state = r1_initialState, action) => {
switch(action.type) {
case CHANGE_GRAIN:
const {index, key, newValue} = action;
const grainsCopy = [...state.grains];
grainsCopy[index][key] = newValue;
return {...state, grains: grainsCopy};
case ADD_EMPTY_GRAIN: {
return {...state, grains: [...state.grains, {grainId: '', amount: 0.0}]}
}
default:
return state;
}
};
If this seems like too much work, but you still want to keep the form data in the Redux state, there are libraries like redux-form, which handles the repetitive handling of onChange etc. for your forms.
Another option would be to load the initial state from Redux, but just use it to set the internal state of the component. Then handle all the form changes as a change to the component state. The same logic as the one in the example above, could be used for rendering the fields from an array.

How to properly manage forms in React/Redux app

I am following some tutorials about React/Redux.
The part that is not clear to me is input forms.
I mean how to handle changes and where to manage state
This is a very simple form with just one field.
export default class UserAdd extends React.Component {
static propTypes = {
onUserSubmit: React.PropTypes.func.isRequired
}
constructor (props, context) {
super(props, context);
this.state = {
name: this.props.name
};
}
render () {
return (
<form
onSubmit={e => {
e.preventDefault()
this.handleSubmit()
}}
>
<input
placeholder="Name"
onChange={this.handleChange.bind(this)}
/>
<input type="submit" value="Add" />
</form>
);
}
handleChange (e) {
this.setState({ name: e.target.value });
}
handleSubmit () {
this.props.onUserSubmit(this.state.name);
this.setState({ name: '' });
}
}
I feel this like breaking Redux philosophy, because a presentation component is updating the state, am I right?
This is the connected component to be coupled with the presentation component.
const mapDispatchToProps = (dispatch) => {
return {
onUserSubmit: (name) => {
dispatch(addUser(name))
}
}
}
const UserAddContainer = connect(
undefined,
mapDispatchToProps
)(UserAdd)
Is this the correct way to follow, or am i mixing things up?
Is correct to call setState in UserAdd component and updating state on every key pressed (handleChange) ?
Thank you
There is a nice library Redux Form for handling forms by updating global store in a Redux way. With it's help you shouldn't have to set up actions for each input, just the whole form and its state. Check it out.
The main principle of this library, consists in updating inputs value by dispatching redux actions, not using setState stuff. For every form in the app, there is a separate property in the global state. Every blur, onChange, submit events dispatches an action that mutates the state. Action creators are common for all the forms, no need to declare them for every form apart, just pass form id or name in payload to the reducer, so it could know which form`s property should be updated.
For example. There should be set a property form as a plain object in the app state. Each new form in the application, should store it's state in it. Let's give your form a name attribute, so it should serve us as the identificator.
render () {
return (
<form
name="AForm"
onSubmit={e => {
e.preventDefault()
this.handleSubmit()
}}
>
<input
name="name"
placeholder="Name"
onChange={this.handleChange.bind(this)}
/>
<input type="submit" value="Add" />
</form>
);
}
Since it has just one property Name, form state should now have a structure like:
form: {
AForm: {
Name: {
value: '',
error: ''
}
}
}
Also, there should be an action creator:
export function onFormFieldChange(field) {
return {
type: "redux-form/CHANGE"
field: field.name
value: field.value,
form: field.form
}
}
All needed data should be passed as the pay load so, the reducer will know now what form and what field to update.
Now, when the form component is being connected, this action creator should be set as a property:
import { onFormFieldChange } from `path-to-file-wit-actions`
const mapStateToProps = (state, ownProps) => {
const { AForm } = state.form
return {
name: AForm.name
}
}
const mapDispatchToProps = (dispatch) => {
return {
onChange: (e) => {
dispatch(onFormFieldChange({
field: 'name',
value: e.target.value,
form: 'AForm'
}))
},
onUserSubmit: (name) => {
dispatch(addUser(name))
}
}
}
const UserAddContainer = connect(
undefined,
mapDispatchToProps
)(UserAdd)
In the component, field value and onChange event handler should now be taken from props:
<input placeholder="Name" name="this.props.name.value" onChange={this.props.handleChange.bind(this)} />
So, form is being handled in a "Redux" way. On every key press, global state will be updated and input will be rerendered with it's new value. Similar thing should be done with other events, like onBLur, onFocus, onSubmit etc. Since it's a lot work to do, it's much more comfrotable to use Redux Form.
It's a very rough example. Nearly each line of code could be enhanced, hope you'll understand what was meant.
I usually store my form state inside a form component using this.setState() and only fire an action with the complete form object, which gets passed to some sort of POST ajax call.

Categories

Resources