Editing a record: conflict between props and state - javascript

When editing a record, the data being displayed on the component belongs to the props. I get an error
Warning: Failed form propType: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly. Check the render method of TerritoryDetail.
I have a feeling I implemented my edit record component the wrong way based on what the docs say involving controlled components.
When editing a record, should you not use props for the field values? If that is the case, I have values of the record in my application state, but how do I sync my application state to my component state without using props?
In addition, the props say what value the select option should be on edit. But component state is used to monitor changes in the select option. How would component state update the props of the record, when the props are being set by application state and not component state?
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTerritory, getTerritoryMetaData, updateTerritory, modal } from '../actions/index';
import { Link } from 'react-router';
import { reduxForm } from 'redux-form';
import TerritoryTabs from './territory-tabs';
class TerritoryDetail extends Component {
constructor(props) {
super(props);
this.openSearchUserQueueModal = this.openSearchUserQueueModal.bind(this);
this.setAssignedToType = this.setAssignedToType.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
componentWillMount() {
// console.log(this.props);
this.props.getTerritory(this.props.params.id);
this.props.getTerritoryMetaData();
}
renderTerritoryPickList(fieldName) {
return this.props.territoryFields.map((territoryField) => {
const shouldRender = territoryField.name === fieldName;
if (shouldRender) {
return territoryField.picklistValues.map((option) => {
return<option value={option.value}>{option.label}</option>;
});
}
});
}
setAssignedToType(event) {
this.setState({ assignedToType : event.target.value });
}
openSearchUserQueueModal(searchType) {
this.props.modal({
type: 'SHOW_MODAL',
modalType: 'USER_QUEUE_SEARCH',
modalProps: {searchType}
})
}
onSubmit() {
console.log('Update button being clicked');
this.props.updateTerritory({
Name: this.refs[ `Name`].value,
tpslead__Type__c: this.refs[ `tpslead__Type__c`].value,
tpslead__Assigned_To_Type__c: this.refs[ `tpslead__Assigned_To_Type__c`].value,
tpslead__Assigned_To__c: this.refs['tpslead__Assigned_To__c'].value,
tpslead__Assigned_To_ID__c: this.refs['tpslead__Assigned_To_ID__c'].value
}, this.props.params.id);
}
onChangeTerritoryName(event) {
this.props.
}
render() {
if(!this.props.territory) {
return <div>Loading...</div>;
}
return(
<TerritoryTabs id={this.props.params.id} listTab="detail">
<div className="slds-form">
<div className="slds-form-element">
<div className="slds-form-element__label">
<label className="slds-align-middle" htmlFor="input1">Lead Territory Name</label>
</div>
<div className="slds-form-element__control">
<input type="text" ref="Name" className="slds-input" value={this.props.territory.Name}/>
</div>
</div>
<div className="slds-form-element">
<label className="slds-form-element__label" htmlFor="input2">Type</label>
<div className="slds-form-element__control">
<div className="slds-select_container">
<select ref="tpslead__Type__c" className="slds-select" value={this.props.territory.tpslead__Type__c}>
<option></option>
{this.renderTerritoryPickList('tpslead__Type__c')}
</select>
</div>
</div>
</div>
<div className="slds-form-element">
<label className="slds-form-element__label" htmlFor="input3">Assigned to Type</label>
<div className="slds-form-element__control">
<div className="slds-select_container">
<select ref="tpslead__Assigned_To_Type__c" onChange={ this.setAssignedToType } className="slds-select" value={this.props.territory.tpslead__Assigned_To_Type__c}>
<option></option>
{this.renderTerritoryPickList('tpslead__Assigned_To_Type__c')}
</select>
</div>
</div>
</div>
<div className="slds-form-element">
<label className="slds-form-element__label">Assigned To</label>
<div className="slds-form-element__control">
<section className="slds-clearfix">
<input ref="tpslead__Assigned_To__c" value={this.props.territory.tpslead__Assigned_To__c} className="slds-input slds-float--left" style={{maxWidth: '95%'}} disabled/>
<input ref="tpslead__Assigned_To_ID__c" value={this.props.territory.tpslead__Assigned_To_ID__c} type="hidden" />
<button onClick={this.openSearchUserQueueModal.bind(this, this.props.territory.tpslead__Assigned_To_Type__c)} className="slds-button slds-button--icon-border slds-float--right" aria-live="assertive" style={{display: 'inline'}}>
<svg className="slds-button__icon" aria-hidden="true">
<use xlinkHref={searchIcon}></use>
</svg>
</button>
</section>
</div>
</div>
<div className="slds-form-element slds-p-top--small">
<Link to="/" className="slds-button slds-button--neutral">
Cancel
</Link>
<button type="button" onClick={this.onSubmit} className="slds-button slds-button--brand">Update</button>
</div>
</div>
</TerritoryTabs>
);
}
}
function mapStateToProps(state) {
console.log(state);
return { territory: state.territories.single,
territoryFields: state.territories.fields
};
}
export default connect(mapStateToProps, { getTerritoryMetaData, getTerritory, updateTerritory, modal })(TerritoryDetail);

A controlled component means that you've provided both a value and an onChange handler. You have to have both, or React will complain. This is also true if you pass a null or undefined value, so you'll want to default to an empty string in those cases. Example:
export function TerritorySelect({ territory = '', options, onChange }) {
const choices = options.map((o, i) => (
<option key={i} value={o.value}>{o.label}</option>
));
const update = e => onChange(e.target.value);
return (
<select value={territory} onChange={update}>
{choices}
</select>
);
}
export default connect(
state => ({ territory: state.territory.get('territory') }),
{ onChange: actions.updateTerritory }
)(TerritorySelect)

Golden rule is that if the data will be changed by user's input then use the state, otherwise use props. To avoid confusion between application state and component state, I will call application state Redux store.
You can pass whatever is in your Redux store, with the function you already have mapStateToProps and on the constructor of your component simply setting them to the state. But the props are always needed, just not in the way you are using it. To do that on the constructor method, just add the following:
this.state = {
// whatever you want to define goes here
// if you want to pass props you don't need to call this, just props.foo
}
That means that you can handle the state of your select and the state of your inputs without an issue.
Your props will be updated whenever they receive new props as long as they are not received by user interaction. That means that you can use your Redux store to dispatch actions, what will fire an update to your props.

Related

Props turns out to be undefined in Reactjs

Hi I was trying to setState for my app based on the values of Props I was relying on values of prop for my state , But when I passed the function to setState the props I received were undefined , don't know why , I followed this document for reference. You can read the section that says State Updates May Be Asynchronous
Below is my code (code snippet from File Form.js)
this.props.setTodos(function (_, props) {
console.log("this is props:" + props);
return [
...props.todos,
{ text: props.inputText, completed: false, id: Math.random() * 1000 },
];
});
Code for App.js File where I pass props
import "./App.css";
import Form from "./components/Form";
import TodoList from "./components/TodoList";
import React, { useState } from "react";
function App() {
const [inputText, setText] = useState("");
const [todos, setTodos] = useState([]);
return (
<div className="App">
<header>
<h1>My Todolist </h1>
</header>
<Form
todos={todos}
setTodos={setTodos}
setText={setText}
inputText={inputText}
/>
<TodoList />
</div>
);
}
export default App;
Full code for Form.js file
import React from "react";
class Form extends React.Component {
constructor(props) {
super(props);
}
handler = (e) => {
// console.log(e.target.value);
this.props.setText(e.target.value);
};
submitTodoHandler = (e) => {
e.preventDefault();
this.props.setTodos(function (_, props) {
console.log("this is props:" + props);
return [
...props.todos,
{ text: props.inputText, completed: false, id: Math.random() * 1000 },
];
});
};
render() {
return (
<div>
<form>
<input onChange={this.handler} type="text" className="todo-input" />
<button
onClick={this.submitTodoHandler}
className="todo-button"
type="submit"
>
<i className="fas fa-plus-square"></i>
</button>
<div className="select">
<select name="todos" className="filter-todo">
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="uncompleted">Uncompleted</option>
</select>
</div>
</form>
</div>
);
}
}
export default Form;
I don't know why my props are undefined ? Thanks
What you need to do is to change submitTodoHandler function in your <Form> component:
submitTodoHandler = (e) => {
e.preventDefault();
console.log(this.props.inputText) // outputs what you typed in the input
this.props.setTodos(this.props.inputText);
};
You define setState in functional component and pass it as a prop to class component. setState hook in functional components behaves slightly different from setState in class components, which you are referencing to.
In functional components setState (or setTodos in your case) changes the state of the variable simply by using setState(newVariableValue) and accepts as a parameter previous state. As newVariableValue is passed with a prop (inputText) from <App> component to <Form> component, you can directly access it with this.props.inputText.
Using the State Hook

Updating the forms in react js doesn't keep track of the state of all the props

I am facing issues while updating the values. Initially I am taking the values from the parent class to put into the text box, and then if I want to update the values into the form through the child component it should basically set the state in child component and pass the updated values to the API. But now when I try to change the values in the text box, it only changes one character and doesn't keep track of the state of all the props. How can I solve this? I have tried using the defaultValue it does change the values but it cannot keep track of the state change.
PS: The updateToApi is just a sample function that is using post to update values into the api
my sample project is here
https://codesandbox.io/s/sad-perlman-ukb68?file=/src/parent.js
#class Parent#
import React from "react";
import "./styles.css";
import Child from "./child";
class Parent extends React.Component {
constructor() {
super();
this.state = {
data: {
username: ["mar"],
name: [null]
}
};
}
updateToApi(data) {
var username: data.username;
var name: data.name;
}
render() {
return (
<Child data={this.state.data} updateToApi={this.updateToApi.bind(this)} />
);
}
}
export default Parent;
##class Child##
import React from "react";
import "./styles.css";
import { Button } from "react-bootstrap";
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
username: "",
name: ""
};
}
handleSubmit = e => {
e.preventDefault();
};
handleChange = e => {
const data = { ...this.state };
data[e.currentTarget.name] = e.currentTarget.value;
this.setState({ data });
};
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<label>
Username:
<input
type="text"
name="username"
value={
this.props.data.username !== "undefined"
? this.props.data.username
: this.state.username
}
onChange={this.handleChange}
/>
</label>
<b />
<label>
Name:
<input
type="text"
name="Name"
value={
this.props.data.name !== "undefined"
? this.props.data.name
: this.state.name
}
onChange={this.handleChange}
/>
</label>
<br />
<Button variant="primary" onClick={this.props.updateToApi} />
</form>
</>
);
}
}
export default Child;
Why do you have 2 separate states? You should get rid of the state in your Child component entirely and only work with the Parent's state. Put HandleChange function in your Parent component also and pass it down through props.
UPD
Well, if you want for changes in your inputs to be visible, you could change the onchange handler in your Child coponent to
handleChange = e => {
this.setState({
[e.currentTarget.name] : e.currentTarget.value });
};
and the Input value just to this.state.username
Though i'm still having hard time to grasp what you are trying to accomplish here. Having 2 separate conditional states for the input fields is just too complicated. Imagine if your app would be a bit more complex? You'd lost yourself to debugging this stuff:
value={ this.props.data.username !== "undefined"
? this.state.username
: this.state.username
}
So here i highly recommend you to reevaluate all your data strucuture and data flow within the app. You should have the least amount of sources of truth within your app. Ideally one. So just use the main state in the Parent component and pass down the props that are required.

Redux component not updating on state change?

Update: Apparently the bug is fixed. I never pushed a solution, so I'm still not sure what the problem/solution.
Essentially what's going on is that I have a child component that is being passed state from the main application component. I know that works fine, as I see the default value of the state showing up properly.
When the child mounts, it fires an ajax call to fetch some data, and then fires an action to update the state value accordingly (Other packages use this fetch call and it works fine as well). I can see all of this is working as expected by taking a look at the Redux chrome devtool. It shows the action being fired, and that the state has changed from the default value to the value it fetched.
The problem is that the page still shows that default value and not the new state value. So I'm wondering if there's an issue with calling that fetch request/state update and then expecting the component to properly update. Should I pass the state as a prop one level lower and have a component that only focuses on displaying that value? It's clear that everything is working as expected, the page is just not updating when the new state value is set.
Here's the code for child component that is not updating (had to modify for privacy purposes)
import { bindActionCreators, Component, connect, createElement, PropTypes } from 'somePackage';
import { getStatus } from 'somedirectory';
class ChildComponent extends Component {
constructor(props) {
super(props);
this.state = {
irrelevantState: false,
};
}
componentDidMount() {
this.fetchMyData();
}
fetchMyData() {
const {
boundNavActions,
} = this.props;
boundNavActions.getStatus();
}
render() {
const {
**stateImLookingAt**,
irrelevantString,
irrelevantString,
} = this.props;
return (
<div>
<div styleName="irrelevantString">
<div styleName="irrelevantString">
<a
href={ irrelevantString }
aria-label={ irrelevantString }
>
<div
spriteSheetType="irrelevantString"
name={ irrelevantString }
/>
//Would making this it's own component help?
<div styleName="thisDoesntUpdate">
{ **stateImLookingAt** }
</div>
//Would making this it's own component help?
</a>
</div>
</div>
</div>
);
}
}
ChildComponent.propTypes = {
boundNavActions: PropTypes.object,
cartCount: PropTypes.number,
};
const mapDispatchToProps = dispatch => ({
boundNavActions: bindActionCreators({
getStatus,
}, dispatch),
});
export default connect(null, mapDispatchToProps)(ChildComponent);
There's not a lot going on pertaining to this state in the parent but here's a snippet
import { connect, createElement, PropTypes } from 'somedirectory';
import ChildComponent from 'ChildComponentPackage';
import './app.css';
const AppContainer = (props) => {
const {
**stateImLookingAt**,
} = props;
return (
<div styleName="root">
<ChildComponent
**stateImLookingAt**={ **stateImLookingAt** }
/>
</div>
);
};
const mapStateToProps = state => ({
**stateImLookingAt**: state.moo.cow.**stateImLookingAt**,
});
AppContainer.propTypes = {
**stateImLookingAt**: PropTypes.number.isRequired,
};
export default connect(mapStateToProps)(AppContainer);

Component is not receiving current, but previous input values

I have two components to represent a list of articles and a filtering form. Every time any form field is changed, I need to send a HTTP request including the selected filters.
I have the following code for the SearchForm:
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const SearchForm = ({ onFormChange }) => (
<form>
<Field component='select' name='status' onChange={onFormChange}>
<option>All</option>
<option value='published'>Published</option>
<option value='draft'>Draft</option>
</Field>
<Field
component='input'
type='text'
placeholder='Containing'
onChange={onFormChange}
/>
</form>
);
export default reduxForm({ form: 'myCustomForm' })(SearchForm);
And the following for the PostsList:
import React, { Component } from 'react';
import SearchForm from './SearchForm';
import { dispatch } from 'redux';
class PostsList extends Component {
constructor(props) {
super();
this.onFormChange = this.onFormChange.bind(this);
}
onFormChange() {
// Here I need to make the HTTP Call.
console.info(this.props.myCustomForm.values);
}
componentWillMount() {
this.props.actions.fetchArticles();
}
render() {
return (
<div>
<SearchForm onFormChange={this.onFormChange} />
<ul>
{ this.props.articles.map((article) => (<li>{article.title}</li>)) }
</ul>
</div>
);
}
}
const mapStateToProps = (state) => ({
myCustomForm: state.form.myCustomForm
});
const mapDispatchToProps = (dispatch) => ({
actions: {
fetchArticles: dispatch({ type: 'FETCH_ARTICLES' })
}
});
export default connect(mapStateToProps, mapDispatchToProps)(PostsList);
Though there is nothing going wrong with the rendering itself, something very awkful is happending with the myCustomForm.values prop when I change the form.
When I do that for the first time, the console.log(this.props.myCustomForm.values) call returns undefined, and the next calls return the previous value.
For example:
I load the page and select the draft option. undefined is printed.
I select published. { status: 'draft' } is printed.
I select draft again... { status: 'published' } is printed.
I inspected the redux store and the componend props. Both change according to the form interaction. But my function is returning the previous, not the new value sent by onChange.
This is clearly a problem with my code, most probably with the way I'm passing the function from parent to child component.
What am I doing wrong?
There is nothing wrong with your function. What I think is happening is that first time you select the option your callback is fired and is console logging current state for myCustomForm.values which haven't been yet changed by redux-form. So when the select changes:
your callback is fired...
...then redux-form is updating the state.
So. when your callback is making console.log it's printing not yet updated store.
do this, and you will see it's true:
onFormChange(e) {
// Here I need to make the HTTP Call.
console.info(e.currentTarget.value);
}
EDIT
My first question would be, do you really need to store this value in redux and use redux-form? It's a simple case, and you get current value in a way I showed you above.
However, if that's not the case, the callback is not required here, you just need to detect in your connected component (PostsList) that values have been changed in a form. You can achieve it with componentWillReceiveProps hook.
class PostsList extends Component {
constructor(props) {
super(props); // you should pass props to parent constructor
this.onFormChange = this.onFormChange.bind(this);
}
componentWillReceiveProps(nextProps) {
if(this.props.myCustomForm.values !== nextProps.myCustomForm.values) {
// do your ajax here....
}
}
componentWillMount(nextProps) {
this.props.actions.fetchArticles();
}
render() {
return (
<div>
<SearchForm />
<ul>
{ this.props.articles.map((article) => (<li>{article.title}</li>)) }
</ul>
</div>
);
}
}

ReactiveVar not re-rendering React component

I'm quite new to React in Meteor.
TL;DR : In my data container, changing the value of my ReactiveVar do not rerender my view.
I've got this code :
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
// Mail composer - interface for generating enews
class MailComposer extends Component {
constructor(props) {
super(props);
}
handleSubmit( event ) {
event.preventDefault();
const text = this.refs.mailText.value.trim(),
title = this.refs.mailTitle.value.trim(),
rcpts = this.refs.mailRecpts.value.trim();
console.log(text, title, rcpts);
if ( text && title && rcpts ) {
// ...
this.props.hasTestedIt.set( true );
} else {
// ...
}
}
componentDidMount () {
console.log(this);
$( this.refs.textArea ).autosize();
}
getBtnText () {
return ( this.props.hasTestedIt.get() ? "Send it" : "Test it" );
}
render() {
let self = this;
Tracker.autorun(function(){
console.log(self.props.hasTestedIt.get());
});
return (
<div className="panel panel-default panel-primary">
<div className="panel-heading">
<h3 className="panel-title">Mail composer</h3>
</div>
<div className="panel-body">
<form className="form-group" onSubmit={this.handleSubmit.bind(this)}>
<div className="input-group">
<span className="input-group-addon" id="basic-addon1">Title</span>
<input type="text" className="form-control" ref="mailTitle" />
</div>
<br/>
<label htmlFor="js-recipients">Recipients:</label>
<select className="form-control" width="width: initial;" id="js-recipients" ref="mailRecpts">
<option>Admins</option>
<option>All</option>
</select>
<br/>
<label htmlFor="comment">Text:</label>
<textarea className="form-control" rows="5" ref="mailText"></textarea>
<br/>
<button className="btn btn-primary" type="submit">
{this.getBtnText()}
</button>
</form>
</div>
</div>
);
}
}
MailComposer.propTypes = {
hasTestedIt: PropTypes.object.isRequired
};
export default createContainer( () => {
return {
hasTestedIt: new ReactiveVar( false )
};
}, MailComposer);`
But when I set my ReactiveVar prop in my submit handler, the text returned by the getBtnText method in the button do not change. I've tried to put the ternary directly inside the HTML, but I got the same result.
The var is correctly setted to true, as the autorun correctly log me the change.
In another component, from which this one has been copied, I do correctly rerender the component, but using a .fetch() on a find to map the returned array in a renderTasks method which render a list of new components.
What am I missing please ? And how could I fix it ?
Thanks a lot !
The component doesn't update because the passed hasTestedIt prop is not changed itself. It's the value it holds that changed.
Watch the value you are actually interested in instead.
// Declare the reactive source somewhere appropriately.
const hasTestedIt = new ReactiveVar( false );
// Watch its value instead.
export default createContainer( () => {
return {
hasTestedIt: hasTestedIt.get()
};
}, MailComposer);`
Note that it is now the value passed to MailComposer, not a ReactiveVar that can be referenced to update the value anymore. How do we update it in this case?
One simplest approach is to pass the hasTestedIt as well as before, though this wouldn't be my personal recommendation.
// Watch its value instead.
export default createContainer( () => {
return {
// Reactive source that triggers re-rendering.
valueOfHasTestedIt: hasTestedIt.get(),
// Provided for the contained component to reference to set new values.
// Leaving this alone doesn't trigger re-rendering!
hasTested
};
}, MailComposer);
It's not elegant IMO. Another one is to pass a callback function to MailComposer which can be used to update the value.
const updateHasTestedIt = ( value ) => hasTestedIt.set( value );
// Watch its value instead.
export default createContainer( () => {
return {
hasTestedIt: hasTestedIt.get(),
updateHasTestedIt,
};
}, MailComposer);
class MailComposer extends Component {
...
handleSubmit( event ) {
...
this.props.updateHasTestedIt( true );
...
}
...
};
It's better this time.
It is possible to develop more, but it really depends on your preference for your very application. You and only you can make the design decision.

Categories