I have a form with controlled inputs managed via a Higher Order Component. The structure is like this:
Field Higher Order Component
function BaseField(WrappedComponent) {
class WrappedField extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
active: false,
}
}
setValue = (e) => {
this.setState({ value: e.target.value })
}
...
<WrappedComponent
{...this.props}
value={this.state.value}
set={this.setValue}
active={this.state.active}
/>
....
Field
import React from 'react';
import BaseField from './BaseField';
const TextField = (props) => {
return <input
value={props.value}
onChange={props.set}
name={props.name}
type={props.type}
/>
}
export default BaseField(TextField);
When using TextField this works well - however, I want to use this with more flexibility - for example, I'd like to be able to enhance the onChange functionality in some cases, always having it set state but also have it do other things based on state or functions in the component the TextField is used in.
Am I misunderstanding how HOCs work?
You can use something like createChainedFunction from react-bootstrap:
function createChainedFunction(...funcs) {
return funcs
.filter(f => f != null)
.reduce((acc, f) => {
if (typeof f !== 'function') {
throw new Error('Invalid Argument Type, must only provide functions, undefined, or null.');
}
if (acc === null) {
return f;
}
return function chainedFunction(...args) {
acc.apply(this, args);
f.apply(this, args);
};
}, null);
}
and something from my react utils:
export function copyPropsWithout(props, without) {
const propKeys = Object.keys(props);
const passProps = propKeys.reduce((obj, propKey) => {
if (without.indexOf(propKey) === -1) {
obj[propKey] = props[propKey];
}
return obj;
}, {});
return passProps;
}
I'd add these to your utils and then use them like:
...
<WrappedComponent
{...copyPropsWithout(this.props, ['onChange'])}
value={this.state.value}
set={createChainedFunction(this.setValue, this.props.onChange}}
active={this.state.active}
/>
....
Related
I have a picklist component that render some children based on the selected option. The problem comes when I render the same component in two options but with different props, because in that case, the component is not rerendered with the new props.
Let me clarify the problem: I have a picklist, I select option "A", then a text component is rendered below the picklist, I type "error" in that text field, then select option "B" in the picklist, then the other text field component disappear and another text field component is rendered just below the picklist. The last component should have been rendered empty, but the problem is that it contains the word "error".
Here's a minimized version of the code reproducing the error:
import React from "react";
class TextField extends React.Component {
constructor(props) {
super(props);
this.state = { value: props.value };
}
_onChange = (event) => {
this.setState({ value: event.currentTarget.value });
};
_handleBlur = (event) => {
this.setState({ value: event.currentTarget.value.trim() });
};
render() {
const { value } = this.state;
return (
<div>
<label>TextField</label>
<input
type="text"
onChange={this._onChange}
onBlur={this._handleBlur}
value={value}
/>
</div>
);
}
}
class Picklist extends React.Component {
constructor(props) {
super(props);
this.state = { selectedOption: "" };
this.options = ["Blank", "A", "B"];
}
// eslint-disable-next-line consistent-return
_handleSelectorCallback = (newOption) => {
this.setState({ selectedOption: newOption.currentTarget.value });
};
_renderChildren(selectedOption) {
if (!selectedOption) {
return null;
}
if (selectedOption === "A") {
return <TextField value="optionA"/>;
}
if (selectedOption === "B") {
return <TextField value="optionB"/>;
}
}
_renderOptions() {
const { selectedOption } = this.state;
return this.options.map((option) => (
<option value={option} selected={selectedOption === option}>
{option}
</option>
));
}
render() {
const { selectedOption } = this.state;
return (
<React.Fragment>
<div>
<label>Demo of the Error!</label>
<select
onChange={this._handleSelectorCallback}
value={selectedOption}
>
{this._renderOptions()}
</select>
</div>
{this._renderChildren(selectedOption)}
</React.Fragment>
);
}
}
export default Picklist;
Ignore how bad the component is written, is just to reproduce the error I'm having. Any idea why the component is not being rerendered with a new value?
I think what's happening is, when you switch from one <TextField> to the other, React is trying to be efficient by just passing different props to the same instance. You can tell React they are different and should be rerendered by adding a key:
_renderChildren(selectedOption) {
if (!selectedOption) {
return null;
}
if (selectedOption === "A") {
return <TextField key="A" value="optionA" />;
}
if (selectedOption === "B") {
return <TextField key="B" value="optionB" />;
}
}
alternatively, you could make your TextField a controlled component, which means it has no internal state, and the value/onChange fn are passed in as props. I edited your codesandbox to follow this pattern: https://codesandbox.io/s/empty-wind-43jq4?file=/src/App.js
I tried your sandbox. Your TextField component does not get unmounted so it's constructor gets called just once. Every change you make that has to do with that component, goes to it's componentDidUpdate hook.
So this is what you are missing:
componentDidUpdate() {
if (this.state.value !== this.props.value) {
this.setState({ value: this.props.value });
}
}
Ok, I'm new to react and mobx, and I'm experiencing some issues to manipulate the store.
When I'm typing at the input, the value gets overwritten for each char typed.
The component:
#withStore
#observer
class ConfigModel extends Component {
configModel;
constructor(props) {
super(props);
this.configModel = this.props.store.configModelStore;
}
render() {
const fieldsObj = this.configModel.modelConfig;
const fieldHelpers = this.configModel.helperModelStore.modelConfig;
const callbackOnChange = this.configModel;
const campos = merge(fieldHelpers, fieldsObj); // _.merge()
return (
<Form key={'configModelForm'}>
<>
{Object.entries(campos).map((campo) => {
if (campo[1].advanced) {
return;
}
if (campo[1].type === 'input') {
return (
<InputRender
key={campo[1].id}
field={campo[1]}
onChange={callbackOnChange.valueOnChange}
/>
);
}
})}
</>
</Form>
);
}
}
And my store define some observables (some options were omitted for simplicity, like the type evaluated at the component above):
#observable modelConfig = [{
id: 'postType',
value: '',
disabled: false,
advanced: false,
},
{
id: 'pluralName',
value: '',
disabled: false,
advanced: true,
},
...
]
And also define some actions:
#action valueOnChange = (e, {id, value}) => {
this.modelConfig.filter((config, index) => {
if (config.id === id) {
this.modelConfig[index].value = value;
console.log(this.modelConfig[index].value);
}
});
The console.log() above prints:
I truly believe that I'm forgetting some basic concept there, so can someone spot what am I doing wrong?
*EDIT:
I have another component and another store that is working correctly:
#observable name = '';
#action setName = (e) => {
this.name = e.target.value;
console.log(this.name);
}
So my question is:
Why the action that targets a specific value like this.name works fine and the action that targets a index generated value like this.modelConfig[index].value doesn't works?
The problem was at the <InputRender> component that was also receiving the #observable decorator. Just removed and it worked.
// #observer <---- REMOVED THIS
class InputRender extends Component {
render() {
const item = this.props.field;
return (
<InputField
id={item.id}
label={
<InfoLabel
label={item.label}
action={item.action}
content={item.popupContent}
/>
}
placeholder={item.placeholder}
onChange={this.props.onChange}
value={item.value}
disabled={item.disabled}
error={item.error}
throwError={item.throwError}
/>
);
}
}
I have built this site
https://supsurvey.herokuapp.com/surveycreate/
now I am trying to move the fronted to React so I can learn React in the process.
with vanila js it was much easier to create elements dynamically.
I just did createElement and after that when I clicked "submit" button
I loop throw all the elements of Options and take each target.value input.
so I loop only 1 time in the end when I click Submit and that's it I have now a list of all the inputs.
in react every change in each input field calls the "OnChange" method and bubbling the e.targe.value to the parent and in the parent I have to copy the current array of the options and rewrite it every change in every field.
is there other way? because it seems crazy to work like that.
Options.jsx
```import React, { Component } from "react";
class Option extends Component {
constructor(props) {
super(props);
this.state = { inputValue: "", index: props.index };
}
myChangeHandler = event => {
this.setState({ inputValue: event.target.value });
this.props.onChange(this.state.index, event.target.value);
};
render() {
return (
<input
className="survey-answer-group"
type="text"
placeholder="Add Option..."
onChange={this.myChangeHandler}
/>
);
}
}
export default Option;
______________________________________________________________________________
Options.jsx````
```import React, { Component } from "react";
import Option from "./option";
class Options extends Component {
render() {
console.log(this.props);
return <div>{this.createOptions()}</div>;
}
createOptions = () => {
let options = [];
for (let index = 0; index < this.props.numOfOptions; index++) {
options.push(
<Option key={index} onChange={this.props.onChange} index={index} />
);
}
return options;
};
}
export default Options;```
______________________________________________________________________________
App.jsx
```import React from "react";
import OptionList from "./components/Options";
import AddButton from "./components/add-button";
import "./App.css";
class App extends React.Component {
state = {
numOfOptions: 2,
options: [{ id: 0, value: "" }, { id: 1, value: "" }]
};
handleChange = (index, value) => {
const options = [...this.state.options];
console.log("from App", value);
options[index].value = value;
this.setState({
options: options
});
console.log(this.state);
};
addOption = () => {
const options = [...this.state.options];
options.push({ id: this.state.numOfOptions + 1, value: "" });
this.setState({
numOfOptions: this.state.numOfOptions + 1,
options: options
});
};
submitButton = () => {};
render() {
return (
<div className="poll-create-grid">
<div id="poll-create-options">
<OptionList
onChange={this.handleChange}
numOfOptions={this.state.numOfOptions}
/>
</div>
<button
className="surveyCreate-main-btn-group"
onClick={this.addOption}
>
Add
</button>
<button
className="surveyCreate-main-btn-group"
onClick={this.submitButton}
>
Submit
</button>
</div>
);
}
}
export default App;
```
So firstly,
The issue is with the way your OptionList component is defined.
Would be nice to pass in the options from the state into the component rather than the number of options
<OptionList
onChange={this.handleChange}
options={this.state.options}
/>
The you basically just render the options in the OptionsList component (I'm assuming it's same as the Options one here
class Options extends Component {
...
render() {
return
(<div>{Array.isArray(this.props.options) &&
this.props.options.map((option) => <Option
key={option.id}
index={option.id}
onChange={this.props.onChange}
value={option.value}
/>)}
</div>);
}
...
}
You would want to use the value in the Option component as well.
this.props.onChange(this.state.index, event.target.value); No need using the state here to be honest
this.props.onChange(this.props.index, event.target.value); is fine
So I have a component that adds a * at the beginning of typed in text that looks like this:
export default class MyTextField extends Component {
constructor() {
super();
this.state = {
value: '',
};
}
static propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
console.log(`I am the parent wit newValue ${newValue}.`);
this.setState({
value: this.addSymbol(newValue, '*')
});
}
render() {
const {
onChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.state.value}
onChange={this.change.bind(this)}
{...other } >
</TextField >
);
}
}
Now I use MyTextFieldin another component like this:
<MyTextField
style={styleTextFields}
floatingLabelText='Add text'
type='text'
/>
Now when I start typing everything works great.
If I change the code above and add value='test' prop on MyTextField everything breaks. It appears its because I added a prop to TextField which can not be overwritten by MyTextField value={this.state.value}.
So how can I add prop value and still add a * at the beginning of the typed text?
The other part is if the user adds their own onChange, how can I get both MyTextField and the component using MyTextField onChange events to get called?
=======Beginning of Edit 1===============
#Patrick - I have made tweaks based on your comment and the first part is now working. So I can type in <TextField /> and I see the changes.
The changes are below:
export default class MyTextField extends Component {
constructor(props) {
super();
this.state = {
value: props.value ? this.addSymbol(props.value.toString(), '*') : ''
};
}
static propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
console.log(`I am the parent wit newValue ${newValue}.`);
this.setState({
value: this.addSymbol(newValue, '*')
});
}
render() {
const {
value, // eslint-disable-line no-unused-vars
onChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.state.value}
onChange={this.change.bind(this)}
{...other } >
</TextField >
);
}
}
I am not sure how to make the onChange tweak because you are not supposed to modify component state?
I also did try this in the render() function which does not work:
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
console.log(`I am the parent wit newValue ${newValue}.`);
this.setState({
value: this.addSymbol(newValue, '*')
});
}
render() {
const {
value, // eslint-disable-line no-unused-vars
onChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.addSymbol(this.state.value, '*')}
{...other } >
</TextField >
);
}
Anyway, thank you for your help.
=======Ending of Edit 1===============
=======Beginning of Edit 2===============
I fixed the onChange event problem by adding my own event called onNewValueChange and calling it in the onChange. Here is the final code:
export default class MyTextField extends Component {
constructor() {
super();
this.state = {
value: props.value ? this.addSymbol(props.value.toString(), '*') : ''
};
}
static propTypes = {
onNewValueChange: PropTypes.func,
value: PropTypes.string
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
const value = value: props.value ? this.addSymbol(props.value.toString(), '*') : '';
this.setState({
value: value
});
if(this.props.onNewValueChange) {
this.props.onNewValueChange(value);
}
}
render() {
const {
onNewValueChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.state.value}
onChange={this.change.bind(this)}
{...other } >
</TextField >
);
}
}
Now I use MyTextField in another component like this:
<MyTextField
style={styleTextFields}
floatingLabelText='Add text'
type='text'
onNewValueChange={(newValue) => console.log(newValue) }
/>
#Patrick thank you for your help getting me in the right direction.
=======Ending of Edit 2===============
In general, I think this pattern of omitting props before passing everything else inside a component is confusing, at least for me. Being explicit as possible should always be a priority.
If I understood you correctly, the expected behavior is as follows:
You can pass a default text, this text should be prefixed with a * by your component.
You can modify the text, but no matter how you modify it, it should always be prefixed with a * - so an empty string should just be *.
I've created this fiddle which hopefully does what you meant : https://jsfiddle.net/69z2wepo/78041/
The key concept is to separate between props and state, a prop is passed only once, and should really be responsible for the initial state of your
Here's the two classes I created :
class Input extends React.Component{
render(){
return (<input onChange={this.props.onChange} value={this.props.value}/>)
}
}
class WrappedInput extends React.Component{
constructor(props){
super();
let val = props.value || '';
this.state = {
value : this.addSymbol(val,'*')
}
}
handleChange(e){
let newVal = e.target.value;
this.setState({value:this.addSymbol(newVal,'*')});
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
render(){
return (<Input onChange={this.handleChange.bind(this)} value={this.state.value}/>)
}
}
I have two react components which are ProgramSearchBox and DualBox which are generic and wrapper components of predefined npm packages AutoSuggest and DualListBox respectively.
My task to achieve is Based on the value from ProgramSearchBox, I have to list the set values in the DualListBox.
So, If user select a Program from ProgramSearchBox, then I will call API by passing the ProgramId and fetch the set of result values and have to bind them in the DualListBox.
I will get the user selected ProgramID from the ProgramSearchBox as a prop in DualBox component render method.
How to dispatch an action (call a function) from render function in DualBox component by passing the ProgramId?
If I call a method from render function in DualBox, that is becoming Infinite loop!
Here is DualBox component:
//DualBox.js
class DualBox extends React.Component {
constructor() {
super();
this.state = { selected: [] };
this.onChange = this.onChange.bind(this);
this.options = [ ];
}
onChange(selected) {
selected(selected);
}
updateOptions()
{
console.log("Update Option method called :" + this.props.traineesList );
this.options = [{ value: 'luna', label: 'Moon' }, { value: 'phobos', label: 'Phobos' }];
//this.options = this.props.traineeList.map( (value,id) => )
}
render() {
const {ProgramID} = this.props; // HERE I GET ProgramID AS PROP FROM AN ANOTHER COMPONENT
const {selected} = this.state;
if(ProgramID !== "") // BASED ON THIS ProgramID VALUE, I NEED TO DISPATCH AN ACTION.
{
{this.updateProgramId(ProgramID)} // THIS IS CAUSING INFINITE LOOP
{this.updateOptions}
console.log("Program Id came to dualbox:" +ProgramID);
return <DualListBox options={this.options} selected={selected} onChange={this.onChange}
canFilter
filterCallback={(option, filterInput) => {
if (filterInput === '') {
return true;
}
return (new RegExp(filterInput, 'i')).test(option.label);
}}
filterPlaceholder="Filter..."
/>;
}
else
{
console.log("Program Id didn't come to dualbox");
return <DualListBox options={this.options} selected={selected} onChange={this.onChange}
canFilter
filterCallback={(option, filterInput) => {
if (filterInput === '') {
return true;
}
return (new RegExp(filterInput, 'i')).test(option.label);
}}
filterPlaceholder="Filter..."
/>;
}
}
}
function mapStateToProps(state, ownProps) {
return {
traineesList: state.traineesList
};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
updateProgramId: bindActionCreators(( {ProgramID}) => dualBoxActions.getTraineesList(ProgramID), dispatch)
};
}
export default connect(mapStateToProps,mapDispatchToProps)(DualBox);
Here is the ProgramSearchBox component:
function renderSuggestion(suggestion) {
return (
<ul>
<li>{suggestion.Program}</li>
</ul>
);
}
class ProgramSearchBox extends React.Component {
constructor(props) {
super(props);
}
render() {
const { value, suggestions, onChange, onSuggestionSelected} = this.props;
const inputProps = {
placeholder: "Look Up",
value,
onChange: (event, { newValue, method }) => {
this.setState({
value: newValue
});
console.log("onChange: " + JSON.stringify(newValue) );
onChange(newValue);
}
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.props.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.props.onSuggestionsClearRequested}
onSuggestionSelected={
(event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
console.log("onSuggestionSelected: " + JSON.stringify(suggestion) );
onSuggestionSelected(suggestion);
}
}
getSuggestionValue={(suggestion) => suggestion.Program}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
theme={theme}
/>
);
}
}
function mapStateToProps(state, ownProps) {
return {
suggestions: state.results
};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onSuggestionsFetchRequested: bindActionCreators(({ value }) => searchActions.getProgramSuggestions(value), dispatch),
onSuggestionsClearRequested: bindActionCreators(() => searchActions.clearSuggestions(), dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ProgramSearchBox);
Don't call other functions in render() method. Render method is responsible only for rendering views, it can be called many times and it should be as pure as possible.
Updated answer (2019-11-21)
Use componentDidUpdate(prevProps) lifecycle function to react to prop changes.
It will look something like this:
componentDidUpdate(prevProps) {
if (this.props.ProgramID !== '' && prevProps.ProgramID !== this.props.ProgramID) {
this.updateProgramId(this.props.ProgramID)
}
}
Old answer
To do actions depending on props changing, use componentWillReceiveProps(nextProps) lifecycle function.
It will look something like this:
componentWillReceiveProps(nextProps) {
if (nextProps.ProgramID !== '' && this.props.ProgramID !== nextProps.ProgramID) {
this.updateProgramId(ProgramID)
}
}
After calling this.updateProgramId(ProgramID) props will update and render method will be called.
More info about ReactJS lifecycle:
https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops