React selectively not passing new props to rendered object - javascript

I'm working on a multi-stage form that gets some intermediate data via AJAX based on this guide. I'm having an odd issue where React isn't passing new props to a component.
// MyForm.js.jsx
var MyForm = React.createClass({
render: function() {
switch(this.state.stage) {
case 1:
return <InitialFields
nextStage={this.nextStage}
save={this.save}
/>
case 2:
return <ChoiceFields
title="Choose first thing:"
field="first_id"
options={this.state.firstChoices}
nextStage={this.nextStage}
save={this.save}
/>
case 3:
return <ChoiceFields
title="Choose second thing:"
field="second_id"
options={this.state.secondChoices}
nextStage={this.nextStage}
save={this.save}
/>
}
}
// etc ...
});
ChoiceFields.js.jsx:
var ChoiceFields = React.createClass({
render: function() {
console.log(this.state);
var options = this.setOptions();
return (
<div className="choiceFields">
<h1>{this.props.title}</h1>
<SearchBar onChange={this.onSearch} />
<div className="btn-group">{options}</div>
<NextButton next={this.saveAndContinue} text="Set Default Values" />
</div>
);
},
setOptions: function() {
var buttons = this.state.options;
return buttons.map(function(choice) {
return (
<ChoiceButton key={choice.id} title={choice.name}
description={choice.description} id={choice.id}
makeSelection={this.selectButton} selected={choice.selected}
/>
);
}.bind(this));
}
});
When the state advances from 1 to 2, it renders the ChoiceFields without issue. When the state advances from 2 to 3, it renders the new title, but the options prop remains unchanged despite giving it a different object.
Is there some way to force React to update the prop, or otherwise rerender the ChoiceFields object?
--UPDATE--
I was copying this.props.options into this.state.options, and using state to keep track of whether or not an option was selected. Per #superfell's recommendation, I kept the object array in props and calculated which one was selected in the render method. This fixed it the issue.

Based on the comments, you are copying the props to state in your ChoiceFields component in getInitialState. getInitialState doesn't get called again when the props are updated, and so you're left looking at stale state. You can add a componentWillReceiveProps function to ChoiceFields that can update state from the new props. Or you can refactor to not copy props to state at all, as that is a specific anti-pattern called out by React.

Another option available to you is to give your ChoiceField variants different keys, so React will know they're different instances and they'll each get the full component lifecycle when you swap between them on subsequent renders:
case 2:
return <ChoiceFields
key="first"
title="Choose first thing:"
field="first_id"
options={this.state.firstChoices}
nextStage={this.nextStage}
save={this.save}
/>
case 3:
return <ChoiceFields
key="second"
title="Choose second thing:"
field="second_id"
options={this.state.secondChoices}
nextStage={this.nextStage}
save={this.save}
/>
React.js and Dynamic Children - Why the Keys are Important has a good explaination of what's happening and links to the relevant docs.

Related

Cannot update a component (`App`) while rendering a different component

There are a bunch of similar questions on so, but I can't see one that matches my conundrum.
I have a react component (a radial knob control - kinda like a slider).
I want to achieve two outcomes:
Twiddle the knob and pass the knob value up to the parent for further actions.
Receive a target knob value from the parent and update the knob accordingly.
All without going into an endless loop!
I have pulled my hair out - but have a working solution that seems to violate react principles.
I have knob.js as a react component that wraps around the third party knob component and I have app.js as the parent.
In knob.js, we have:
export default class MyKnob extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
size: props.size || 100,
radius: (props.value/2).toString(),
fontSize: (props.size * .2)
}
if (props.value){
console.log("setting value prop", props.value)
this.state.value = props.value
} else {
this.state.value = 25 // any old default value
}
}
To handle updates from the parent (app.js) I have this in knob.js:
// this is required to allow changes at the parent to update the knob
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
this.setState({value: this.props.value})
}
console.log("updating knob from parent", value)
}
and then to pass changes in knob value back to the parent, I have:
handleOnChange = (e)=>{
//this.setState({value: e}) <--used to be required until line below inserted.
this.props.handleChangePan(e)
}
This also works but triggers a warning:
Cannot update a component (App) while rendering a different component (Knob)
render(){
return (
<Styles font-size={this.state.fontSize}>
<Knob size={this.state.size}
angleOffset={220}
angleRange={280}
steps={10}
min={0}
max={100}
value={this.state.value}
ref={this.ref}
onChange={value => this.handleOnChange(value)}
>
...
Now over to app.js:
function App() {
const [panLevel, setPanLevel] = useState(50);
// called by the child knob component. works -- but creates the warning
function handleChangePan(e){
setPanLevel(e)
}
// helper function for testing
function changePan(e){
if (panLevel + 10>100){
setPanLevel(0)
} else {
setPanLevel(panLevel+10)
}
}
return (
<div className="App">
....
<div className='mixer'>
<div key={1} className='vStrip'>
<Knob size={150} value={panLevel} handleChangePan = {(e) => handleChangePan(e)}/>
</div>
<button onClick={(e) => changePan(e)}>CLICK ME TO INCREMENT BY 10</button>
...
</div>
So - it works -- but I am violating react principles -- I haven't found another way to keep the external "knob value" and the internal "knob value" in sync.
Just to mess with my head further, if I remove the bubbling to parent in 'handleOnChange' - which presumably then triggers a change in prop-->state cascading back down - I not only have a lack of sync with the parent -- but I also need to reinstate the setState below, in order to get the knob to work via twiddling (mouse etc.._)! This creates another warning:
Update during an existing state transition...
So stuck. Advice requested and gratefully received. Apols for the long post.
handleOnChange = (e)=>{
//this.setState({value: e})
**this.props.handleChangePan(e)**
}
It has been suggested on another post, that one should wrap the setState into a useEffect - but I can't figure out how to do that - let alone whether it's the right approach.
The error message will be displayed if parent (App) states are set while rendering children (Knob).
In your case, while App is rendering, Knob'sonChange() is triggered when loaded, which then calls this.handleOnChange() and then this.props.handleChangePan() having App'ssetPanLevel().
To fix using useEffect():
In knob.js, you can store panLevel as state first just like in App, instead of direct calling this.props.handleChangePan() to call App'ssetPanLevel().
Then, use useEffect(_=>props.handleChangePan(panLevel),[panLevel]) to call App'ssetPanLevel() via useEffect().
Your knob.js will look like this:
function Knob(props){
let [panLevel, setPanLevel] = useState(50);
useEffect(_=>{
props.handleChangePan(panLevel);
}, [panLevel]);
return *** Knob that does not call props.handleChangePan(), but call setPanLevel() instead ***;
}
setState() called inside useEffect() will be effective after the render is done.
In short, you cannot call parent'ssetState() outside useEffect() while in first rendering, or the error message will come up.

Why isn't my input value updating with React?

I have the following code in my component. It will get called when I update certain things, thereby replacing a bunch of things in the UI. Everything is updating EXCEPT the value of the input as seen by the user.
let input = {
id: 'discount-' + geo + '-' + segment,
value: percentage,
disabled: applyToAll,
placeholder: '0.00'
};
cells.push(
<td key={'cell-' + geo + '-' + segment} className="discount-segment cfix">
<Text {...input} />
</td>
);
This is what <Text> returns, with things removed for clarity
return (
<div className={containerClasses.join(' ')} id={'field-container-' + id}>
{label}
<input
autoComplete="off"
type="text"
id={id}
ref="input"
{...extraProps}
name={id}
className={classes.join(' ')}
defaultValue={this.props.value}
onChange={this.props.onChange}
/>
</div>
);
Everything renders fine. Let's say the percentage value is 5 on start, it will show 5 in the field. I then do something else that updates percentage to 50. (A console log will show the right number on re-render). However the value is only showing 5 in the UI still. I am using defaultValue on the input field, but I figure that should be changing as the whole thing re-renders from parent.
Edit
Updated <Text> to set value instead of defaultValue. However then I need to use state to update the value when user types. Then when I re-render, I'm sending in new props with proper value, but of course props isn't updated. Catch-22 for me.
You need to perform a couple of steps:
Your input needs to only use the value and onChange props, do not use defaultValue
Initialize your state using your props to set your default value
Update your state when your props change
So, for example:
const MyComponent = React.createClass({
propTypes: {
defaultInputValue: React.PropTypes.string
},
getInitialState() {
return {
inputValue: this.props.defaultInputValue
};
},
componentWillReceiveProps(nextProps) {
if (nextProps.defaultInputValue !== this.props.inputValue) {
//Forcibly overwrite input value to new default if the default ever changes
this.setState({inputValue: nextProps.defaultInputValue});
}
},
render() {
return <input type="text"
value={this.state.inputValue}
onChange={e => this.setState({inputValue: e.target.value})} />;
}
});
In general initializing state off of props is a no-no. I would probably cringe a little bit if I saw this come across in a code review as there is probably some behavior that can be simplified.
You can also do:
<input value={this.state.inputValue || this.props.defaultInputValue} />
Such that the value of the input reverts to the prop value if you ever clear out the input. In this case you wouldn't have to forcibly overwrite the state with the new props.
componentWillReceiveProps is deprecated now, and even its successor getDerivedStateFromProps is recommended against. To square the circle of "I need to manage the state of a text input (state), but I also need it to update its initial value when I re-render (props)", the React blog recommended using a key in the props. When the key changes, the component remounts and props can initialize state anew.
To aggressively force the component to refresh whenever anything about the input changes, I would use
<Text {...input} key={JSON.stringify(input)}/>
Then you can use value={this.state.value} like a normal controlled form, but you can set this.state.value = props.value and it will change every time.
We can do it as this.We Must have to use onChnage() Event.
<input placeholder="as_you_want"
value={this.state.as_your_state}
onChange={e => {
this.setState({ as_your_state: e.target.value });
this.value = this.state.as_your_state;
}}
/>
Hope This Works.

React - change input defaultValue by passing props

Consider this example:
var Field = React.createClass({
render: function () {
// never renders new value...
return (
<div>
<input type="text" defaultValue={this.props.value || ''} />
</div>
);
}
});
var App = React.createClass({
getInitialState: function () {
return {value: 'Hello!'};
},
changeTo: function (str) {
this.setState({value: str});
},
render: function () {
return (
<div>
<Field value={this.state.value} />
<button onClick={this.changeTo.bind(null, 'Whyyyy?')}>Change to "Whyyyy?"</button>
<button onClick={this.changeTo.bind(null, void 0)}>Change to undefined</button>
</div>
);
}
});
React.render(
<App />,
document.getElementById('app')
);
I want to pass value into defaultValue as prop of dumb input component. However it never re-renders it.
As a previous answer mentioned, defaultValue only gets set on initial load for a form. After that, it won't get "naturally" updated because the intent was only to set an initial default value.
You can get around this if you need to by passing a key to the wrapper component, like on your Field or App component, though in more practical circumstances, it would probably be a form component. A good key would be a unique value for the resource being passed to the form - like the id stored in the database, for example.
In your simplified case, you could do this in your Field render:
<div key={this.props.value}>
<input type="text" defaultValue={this.props.value || ''} />
</div>
In a more complex form case, something like this might get what you want if for example, your onSubmit action submitted to an API but stayed on the same page:
const Form = ({item, onSubmit}) => {
return (
<form onSubmit={onSubmit} key={item.id}>
<label>
First Name
<input type="text" name="firstName" defaultValue={item.firstName} />
</label>
<label>
Last Name
<input type="text" name="lastName" defaultValue={item.lastName} />
</label>
<button>Submit!</button>
</form>
)
}
Form.defaultProps = {
item: {}
}
Form.propTypes = {
item: PropTypes.object,
onSubmit: PropTypes.func.isRequired
}
When using uncontrolled form inputs, we generally don't care about the values until after they are submitted, so that's why it's more ideal to only force a re-render when you really want to update the defaultValues (after submit, not on every change of the individual input).
If you're also editing the same form and fear the API response could come back with different values, you could provide a combined key of something like id plus timestamp.
defaultValue only works for the initial load. After that, it won't get updated. You need to maintain the state for you Field component:
var Field = React.createClass({
//transfer props to state on load
getInitialState: function () {
return {value: this.props.value};
},
//if the parent component updates the prop, force re-render
componentWillReceiveProps: function(nextProps) {
this.setState({value: nextProps.value});
},
//re-render when input changes
_handleChange: function (e){
this.setState({value: e.target.value});
},
render: function () {
// render based on state
return (
<div>
<input type="text" onChange={this._handleChange}
value={this.state.value || ''} />
</div>
);
}
});
I'm fairly certain this has to do with Controlled vs. Uncontrolled inputs.
If I understand correctly, since your <input> is Uncontrolled (doesn't define a value attribute), then the value will always resolve to the value that it is initialized with. In this case Hello!.
In order to overcome this issue, you can add a value attribute and set it during the onChange:
var Field = React.createClass({
render: function () {
// never renders new value...
return (
<div>
<input type="text" defaultValue={this.props.default || ''} value={this.props.value} />
</div>
);
}
});
Here is a plunker showing the change.
You can make the input conditionally and then every time you want to force an update of the defaultValue you just need to unmount the input and then immediately render it again.
The issue is here:
onClick={this.changeTo.bind(null, 'Whyyyy?')}
I'm curious why you bind to null.
You want to bind to 'this', so that changeTo will setState in THIS object.
Try this
<button onClick={this.changeTo.bind(this, 'Whyyyy?')}>Change to "Whyyyy?"</button>
<button onClick={this.changeTo.bind(this, void 0)}>Change to undefined</button>
In Javascript, when a function is called, its called in the scope where it was called from, not where it was written (I know, seems counter intuitive). To ensure it is called in the context you write it, you need to '.bind(this)'.
To learn more about binding and function scope, there are lots of online tutes, (some much better than others) - you might like this one: http://ryanmorr.com/understanding-scope-and-context-in-javascript/
I also recommend using the React Dev tools if you are using firefox or chrome, this way you would have been able to see that state.message was not changing:
https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html
Use conditional rendering, then the component will load correct initial value. Something like in this module:
class MenuHeaderInput extends React.Component{
constructor(props){
super(props);
this.handleBlur = this.handleBlur.bind (this);
}
handleBlur (e) {
this.props.menuHeaderUpdate(e.target.value);
}
render(){
if (this.props.menuHeader) {
return (
<div className="w3-row w3-margin" onClick = {() => this.props.handleTitleClick (10)}>
<div className="w3-third" ><pre></pre></div>
<input
className = {"w3-third w3-input w3-jumbo " + EDIT_COLOR}
type = "text"
defaultValue = {this.props.menuHeader}
onBlur = {this.handleBlur}
/>
<div className="w3-third" ><pre></pre></div>
</div>
)
}
else {
return null;
}
}
}
Related to Sia's excellent answer above: https://stackoverflow.com/a/41962233/4142459.
For my case I had a few ways in which a form could be updated:
users could input values into form fields
An API request allowed users to restore from previous versions
Users could navigate to a filled out form (using queryParams of the URL)
clearing the form fields.
Etc more ways of allowing all the fields or just a single change to happen from user action or websockets.
I found that the easiest way to make sure the state of the form is reflected in its inputs is indeed:
To provide a manually-controlled key prop on the top level of the form or parent element to the form (as long as it is above the inputs in the DOM tree.
When users are typing a key update does not need to happen.
I made the key be a simple formHistoricalVersion and as certain updates external to a user typing/selecting/etc interacting with the form field's values happened I incremented the formHistoricalVersion.
This made sure that the state of the form whether by user action or by API request was in-sync--I had complete control over it.
Other solutions I tried:
While making the API request make the whole form disappear (when loading change to a loading spinner instead of the form). Disadvantage to performance and for clearForm it was a bit crazy to do, but possible with setImmediate to convert the form to a loading spinner when they first clear it, then setting isLoading back to false in the setImmediate.
Adding a key on each input: this worked amazingly, but it had a weird blip whenever users would type so I had to get rid of it.
Putting a static key for the form (field.id) (as suggested by above answer) didn't cover all the use cases I had.
In conclusion, it worked pretty easily to set the key of the form with react/redux, I just would add the equivalent of:
return {
...state,
formFieldState: payload.formFields,
historicalFormVersion: state.historicalFormVersion + 1
}
This was necessary because I was using some 3rd party libraries and my own Numeric Input that took in value as a prop but used value as a defaultValue:
const NumberDisplay: FunctionComponent = ({ value, setValue }) => (
<input
defaultValue={convertToSpecialNumberDisplay(value)}
onBlur={(e) => convertToSpecialNumberDisplay(e.target.value)}
onFocus={(e) => convertToNumberFromDisplay(e.target.value)}
onChange={(e) => setValue(e.target.value)}
/>
)
Approximate Redux of overall Form:
const FullForm: FunctionComponent = () => {
const dispatch = useDispatch();
const formState = useState((state) => state.formState);
const formHistoricalVersion = useState((state) => state.formHistoricalVersion);
return (
<form key={formHistoricalVersion}>
{renderFormFields(formState, dispatch)}
</form>
)
}
I also face this problem, what I did was to manually update the input value when the props has change. Add this to your Field react class:
componentWillReceiveProps(nextProps){
if(nextProps.value != this.props.value) {
document.getElementById(<element_id>).value = nextProps.value
}
}
You just need to add an id attribute to your element so that it can be located.

React.js: How do you change the position of a Component item

How do you change the position of a Component item in React?
Unless I've misunderstood it, React orders list items by key, which is represented in the DOM by data-reactid, but I don't know how to modify the key of components on the page.
i.e. How do you grab the component, change it's key, and then fire a render so that the reordered list renders in the order you've set?
e.g. in the following code example, when the Click me link is clicked, the first list item would be swapped with the last list item.
Ideally, this functionality would allow you to dynamically reorder/relocate any component on the page without changing the order of components in the render method.
Here is a link to the repo where the full project is located: https://github.com/bengrunfeld/gae-react-flux-todos
var TodoBox = React.createClass({
render: function(){
return (
<div className="todo-container">
<h4>GAE React Flux Todos</h4>
<TodoList data={this.state.data} />
</div>
)
}
});
var TodoList = React.createClass({
changePosition: function(e){
// Change position of list item (e.g. to top/certain position/end of list)
},
render:function(){
var todoNodes = this.props.data.map(function(todo) {
return (
<Todo key={todo.id} id={todo.id}>
{todo.todoText}
</Todo>
);
});
return (
<form className="todoList">
{todoNodes}
<a onClick={this.changePosition}>Click me</a>
</form>
)
}
});
var Todo = React.createClass({
render:function(){
return (
<div className="todoItem">
<input type="text" className={this.props.id} onChange={this.checkInput} defaultValue={this.props.children} ref="todoItem"/>
</div>
)
}
});
The key prop is not used to order the element, but to reconciliate it between different render calls. Elements with the same key will not be re-rendered but rather diffed against each other in order to update the DOM optimally. See Reconciliation
If you want to reorder elements, you need to change their position in your JSX or in the element array you pass as children in your render method (todoNodes).
In your case, you could make a copy of this.props.data in the TodoList component state, then update that copy in your changePosition method with something like this.setState({data: reorderedData}). A good place to make that copy would be in getInitialState.
The render method of your TodoList would then be called again, and you would map over your newly reordered this.state.data to create an array of Todo elements ordered to your liking.
However, be aware that props in getInitialState is an anti-pattern. Since your data lives in the state of your TodoBox component, a way to avoid this would be to have your TodoList component call this.props.onReorder(reorderedData) in its changePosition method. Your TodoBox component could then pass an event handler to its TodoList child, and update its state with the new data whenever this handler is called.
var TodoBox = React.createClass({
handleReorder: function(reorderedData) {
this.setState({data: reorderedData});
},
render: function(){
return (
<div className="todo-container">
<h4>GAE React Flux Todos</h4>
<TodoList data={this.state.data} onReorder={this.handleReorder} />
</div>
)
}
});
var TodoList = React.createClass({
changePosition: function(e){
// Change position of list item (e.g. to top/certain position/end of list)
// Create a copy of this.props.data and reorder it, then call
// this.props.onReorder to signal to the parent component that
// the data has been reordered
this.props.onReorder(reorderedData);
},
render:function() {
var todoNodes = this.props.data.map(function(todo) {
return (
<Todo key={todo.id} id={todo.id}>
{todo.todoText}
</Todo>
);
});
return (
<form className="todoList">
{todoNodes}
<a onClick={this.changePosition}>Click me</a>
</form>
)
}
});
Keys are used for something else, not for sorting. React uses keys to optimize its internal Virtual DOM operations. It means you tell React that "no matter the order of these siblings, the individual sibling is still identified by this key". That's how React knows whether it should prepend, insert, delete, or append new siblings by reusing the old, without throwing stuff away unnecessarily.
As for your sorting question: To change the order of the siblings, just sort the JavaScript array this.props.data.

setState start to work recursively in React.js

I start to work with react.js and create a component Box, state = {pageNumber: 1, dataForTable:''}. I insert two components in it - Pagination and Table. When click to Pagination it give number of page to Box. Box state change then it render and then Pagination render too. Then I set ajax to server, get new data for table and then Box render for second time to render Tables.
In which function should I put ajax logic? When i put it in componentDidUpdate setState start to work recursively.
In future will be more components in <Box/ > which will change <Tables />.
From what I understand, this is your setup:
var React = require('react');
var ComponentBox = React.createClass({
render: function() {
return (
<div>
<Table />
<Pagination />
</div>
);
}
});
module.exports = ComponentBox;
Your table component should never handle data, you should "feed" data to your component. So you should have a prop receiving data in your table component.
Pagination component should pass an event to ComponentBox telling ComponentBox to get the data - so ajax should happen in your ComponentBox (read more on flux if you wish)
Here's the suggested solution for you
var React = require('react');
var ComponentBox = React.createClass({
handlePageChange: function(startIndex, size) {
// do your ajax here
// set your data to state which causes re-render
},
render: function() {
return (
<div>
<Table data={this.state.tableData} />
<Pagination onPageChange={this.handlePageChange} />
</div>
);
}
});
module.exports = ComponentBox;
inside your pagination component, remember to pass the information out through props onPageChange =)
Hope this is clear enough for you.

Categories

Resources