Send argument to setState besides prevState and props? - javascript

I have a component that calls a function addOption that is defined on its parent component. The function has a parameter 'option' that is from a form field. The addOption function takes the argument and concats it with an array of options in setState. All this is working fine but, I really want the function called by setState outside of the component so it can be tested and to match other functions in the class, but I can't figure out how to pass the 'option' argument to setState.
class AddOption extends React.Component<IProps> {
onFormSubmit = (e: any) => {
e.preventDefault();
const option = e.target.elements.option.value.trim();
const error = this.props.addOption(option);
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<input type="text" name="option" />
<button>Add Option</button>
</form>
</div>
);
}
}
Parent
const deleteOptions = () => ({ options: [] as string[] })
class IndecisionApp extends React.Component<object, State> {
readonly state: State = initialState;
render() {
const { title, subtitle, options } = this.state;
return (
<div>
<Header title={title} subtitle={subtitle} />
<Action
hasOptions={options.length > 0}
onClick={this.handlePickOption}
/>
<Options
onClick={this.handleDeleteOptions}
options={options} />
<AddOption
addOption={this.handleAddOption}
/>
</div>
);
}
handleDeleteOptions = () => this.setState(deleteOptions);
handlePickOption = () => {
const randomNum = Math.floor(this.state.options.length * Math.random());
const option = this.state.options[randomNum];
alert(option);
}
handleAddOption = (option: string) => {
if (!option) {
return "Enter a valid option";
} else if (this.state.options.indexOf(option) > -1) {
return "Enter a unique option";
}
this.setState((prevState: State) => {
return {
options: prevState.options.concat(option)
}
});
}
}
I really want to have handleAddOption more closely resemble deleteOption where its declared outside of the IndecisionApp component, but I don't know how to pass 'option' to setState from handleAddOption.
GitHub - index.tsx

I figured it out.
Inside the component
handleAddOption = (option: string) => {
if (!option) {
return "Enter a valid option";
} else if (this.state.options.indexOf(option) > -1) {
return "Enter a unique option";
}
this.setState(prevState => appOption(prevState, option));
}
Outside the component.
const appOption = (prevState: State, option: string) => {
return {
options: prevState.options.concat(option)
}
}

Related

I want to control value of input in parent component (Child is PureComponent)

Before question, sorry for massy code because I'm newbie at code.
I made Input component by PureComponent.
But I have no idea how to control(like change state) & submit its value in Parent component.
this is Input PureComponent I made:
index.jsx
class Input extends PureComponent {
constructor(props) {
super(props);
this.state = {
value: props.value,
name: props.value,
};
this.setRef = this.setRef.bind(this);
this.handleChange = this.handleChange.bind(this);
}
setRef(ref) {
this.ref = ref;
}
handleChange(e) {
const { name, onChange, type } = this.props;
onChange(name, e.target.value);
this.setState({ isInputError: checkError, value: e.target.value });
}
render() {
const { label, name, type} = this.props;
return (
<div className="inputBox">
<input
id={name}
value={this.state.value}
type={type}
ref={this.setRef}
onChange={this.handleChange}
className="BlueInput"
/>
<label>{label}</label>
</div>
);
}
}
Input.propTypes = {
label: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
type: PropTypes.oneOf(["text", "password", "number", "price", "tel"]),
onChange: PropTypes.func,
}
Input.defaultProps = {
value: "",
type: "text",
onChange: () => { },
}
export default Input;
And this is Parent component where I want to control value
Join.js
function Join() {
const [idMessage, setIdMessage] = useState("");
const [isId, setIsId] = useState(false);
const onChangeId = (name, value) => {
const currentId = value;
const idRegExp = /^[a-zA-z0-9]{4,12}$/;
if (!idRegExp.test(currentId)) {
setIdMessage("Wrong format");
setIsId(false);
} else {
setIdMessage("You can use this");
setIsId(true);
}
}
const handleSubmit = (e) => {
e.preventDefault();
let formData = new FormData();
console.log(formData);
alert("Join completed.");
}
return (
<div className="join-wrap">
<form encType="multipart/form-data" onSubmit={handleSubmit}>
<Input name="id" label="ID" onChange={onChangeId} />
<p className={isId ? 'possible' : 'impossible'}>{idMessage}</p>
<button type="submit">Join</button>
</form>
</div>
)
}
export default Join;
How can I control or submit value?
For anyone landing here with a similar question, I would advise you to use functional components. For the custom input component, you do not really need to define any state in the custom component. You can pass the state (value of input) as well as the change handler as prop to the child component. In case you need refs, use forwardRef.
Intentionally, I am not spoon-feeding here. I am just giving food for thought and some keyword to do a quick research and enhance your skills.
index.js
handleChange(e, fieldName) {
this.props.setState({...this.props.state, [fieldName]:e.target.value})
}
-----------------------
<input
id={this.props.name}
value={this.props.state[name]}
type={type} // you can also add type in state
onChange={(e)=>this.handleChange(e, name)}
className="BlueInput"
/>
join.js
const [state,setState] = useState({fName:'',lNAme:''})
{Object.keys(state).map((value)=>{
return <Input name={value} label="ID" state={state} setState = {setState}/>
})}

Parent scope not triggering child rerender in React

i have a prent comp and a child cmponent. as follows
parent
export class ExpressionMenu extends Component {
constructor(props) {
super(props)
}
state = {
apiArray: [],
}
updateStateFromChild = (arrayType, propertyType, value) => {
let { apiArray } = this.state
let currentArray = []
let idx = apiArray.findIndex((q) => q.id === id)
currentArray = apiArray
switch(propertyType) {
case 'updateObject': {
currentArray = value
break;
}
}
this.setState({
apiArray: currentArray
})
}
render () {
const {
apiArray
} = this.state
return (
<React.Fragment>
<div >
<div>
<ApiPanel
apiArray={apiArray}
updateStateFromChild={this.updateStateFromChild}
/>
</div>
</div>
</React.Fragment>
)
}
}
ExpressionMenu.propTypes = {
styleOverride: PropTypes.object,
eventHandler: PropTypes.func,
};
export default ExpressionMenu;
child
export class ApiPanel extends Component {
constructor(props) {
super(props),
}
removeApi = (id) => {
let { apiArray } = this.props
apiArray = apiArray.filter((q) => q.id !== id);
this.props.updateStateFromChild('api', 'updateObject', apiArray)
};
addApi = () => {
let { apiArray } = this.props
const id = uniqid();
let obj = {}
obj.id = id
apiArray.push(obj)
this.props.updateStateFromChild('api', 'updateObject', apiArray)
};
render() {
const { apiArray } = this.props
return (
<React.Fragment>
{
apiArray.map((apiObj, i) =>
<div key={i} >
<span onClick={() => this.removeApi(apiObj.id) } className={[classes.deleteRow,'material-icons'].join(' ')}>
close
</span>
<div>
<label><b>Hi</b></label>
</div>
<div onClick={this.addApi}>+Add Api</div>
}
</React.Fragment>
)
}
}
ApiPanel.propTypes = {
apiArray: PropTypes.array,
updateStateFromChild: PropTypes.func
}
export default ApiPanel
Now when i call addApi(), it updates the parent but doesnt rerenders the child.
But when i call removeApi() , it updates parent as well as rerenders the child component properly.
in the first case when i manually reload the componnt i can see the change.
Dont understand why this is happening
Try to change your addApi function.
addApi = () => {
let { apiArray } = this.props
this.props.updateStateFromChild('api', 'updateObject', [...apiArray, {id : uniqid()} ])
};
You need to return an enriched copy of your array
Whenever we are updating the stating using arrays, objects. We need to always create a new array [...array], a new object {...obj}. Because if we update value in the array or obj without creating it won't change the reference value hence it assumes the state is not update and won't re-render.

Input doesn't work when used with debounce, event.persist() and storing value at parent component

I need an input field with debounced search and value should be passed from parent component. But it doesn't work when value passed from parent component. What is the right way to implement it?
Codesandbox example https://codesandbox.io/embed/debounce-input-owdwj
Text field with debouncing
class MyTextField extends Component {
search = _.debounce(event => {
this.props.onChange(event.target.value);
}, 300);
handleChangeInput = event => {
event.persist();
this.search(event);
};
render() {
return (
<TextField
value={this.props.value}
placeholder="type"
onChange={this.handleChangeInput}
/>
);
}
}
Parent component storing the value of text field
class Form extends Component {
state = {
value: ""
};
handleChangeInput = value => {
this.setState({
value
});
};
render() {
return (
<div>
<h2>{this.state.value}</h2>
<MyTextField
value={this.state.value}
onChange={this.handleChangeInput}
/>
</div>
);
}
}
The problem here is that you are updating the component only after 300 seconds which will not update the input box also. first, you need to update the search box component whenever there is a keyup and later parent can be informed about the changed after 300 seconds
I have updated your code reference please check it out https://codesandbox.io/embed/debounce-input-gm50t
Declare your debounce function in componentDidMount is everything will be fine.
1) Without state
class MyTextField extends Component {
handleChangeInput = e => {
this.search(e.target.value)
};
componentDidMount() {
this.search =_.debounce((value) => {
this.props.onChange(value);
}, 300)
}
render() {
return (
<TextField
value={this.props.value}
placeholder="type"
onChange={this.handleChangeInput}
/>
);
}
}
export default MyTextField;
2) With state:
class MyTextField extends Component {
state = {
textValue: ''
}
handleChangeInput = e => {
this.setState({
textValue: e.target.value
}, () => { this.search()})
};
componentDidMount() {
this.search =_.debounce(() => {
this.props.onChange(this.state.textValue);
}, 300)
}
render() {
return (
<TextField
value={this.props.value}
placeholder="type"
onChange={this.handleChangeInput}
/>
);
}
}
export default MyTextField;
Hope that helps!!!

Setting State with Objects from Firebase

I'm having trouble setting the state of a component in React. The component is called "Search" and uses react-select. The full component is here:
class Search extends React.Component {
constructor(props){
super(props);
let options = [];
for (var x in props.vals){
options.push({ value: props.vals[x], label: props.vals[x], searchId: x });
};
this.state = {
inputValue: '',
value: options
};
}
handleChange = (value: any, actionMeta: any) => {
if(actionMeta.action == "remove-value"){
this.props.onRemoveSearch({ searchId: actionMeta.removedValue.searchId })
}
this.setState({ value });
};
handleInputChange = (inputValue: string) => {
this.setState({ inputValue });
};
handleSearch = ({ value, inputValue }) => {
this.setState({
inputValue: '',
value: [...value, createOption(inputValue)], // Eventually like to take this out...
});
this.props.onSearch({ inputValue });
}
handleKeyDown = (event: SyntheticKeyboardEvent<HTMLElement>) => {
const { inputValue, value } = this.state;
if (!inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
this.handleSearch({
value,
inputValue
});
event.preventDefault();
}
};
render() {
const { inputValue, value } = this.state;
return (
<div className="search">
<div className="search__title">Search</div>
<Tooltip
content={this.props.tooltipContent}
direction="up"
arrow={true}
hoverDelay={400}
distance={12}
padding={"5px"}
>
<CreatableSelect
className={"tags"}
components={components}
inputValue={inputValue}
isMulti
menuIsOpen={false}
onChange={this.handleChange}
onInputChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
placeholder="Add filters here..."
value={value}
/>
</Tooltip>
</div>
);
}
}
module.exports = Search;
You've probably noticed the strange thing that I'm doing in the constructor function. That's because I need to use data from my firebase database, which is in object form, but react-select expects an array of objects
with a "value" and "label" property. Here's what my data looks like:
To bridge the gap, I wrote a for-in loop which creates the array (called options) and passes that to state.value.
The problem: Because I'm using this "for in" loop, React doesn't recognize when the props have been changed. Thus, the react-select component doesn't re-render. How do I pass down these props (either modifying them inside the parent component or within the Search component) so that the Search component will re-render?
I would suggest not using the value state. What you do is simply copying props into your state. You can use props in render() method directly.
I reckon you use the value state because you need to update it based on user actions. In this case, you could lift this state up into the parent component.
class Parent extends React.Component {
constructor() {
this.state = { value: //structure should be the same as props.vals in ur code };
}
render() {
return (
<Search vals={this.state.value}/>
);
}
}
class Search extends React.Component {
constructor(props){
super(props);
this.state = {
inputValue: '',
};
}
render() {
const { inputValue } = this.state;
const { vals } = this.props;
let options = [];
for (var x in vals){
options.push({ value: vals[x], label: vals[x], searchId: x });
};
return (
<div className="search">
<div className="search__title">Search</div>
<Tooltip
content={this.props.tooltipContent}
direction="up"
arrow={true}
hoverDelay={400}
distance={12}
padding={"5px"}
>
<CreatableSelect
value={options}
/>
</Tooltip>
</div>
);
}
}
module.exports = Search;

add and remove dynamic input field

I want to have a dynamic number of input field and also able to remove it. I can do it if this is only client side but I am getting the data from server and the issue is that I am not getting the id from the server, and that I need to be able to change the number of fields on the fly in the UI.
since I am also getting some fields from the server the manually added id was not longer in sync with the # of items.
How do i achieve this?
Here is my code
let _id = 0;
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4();
}
class Rules extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.state = {
rules: {
rule_regulations: {}
},
numberOfInput: []
};
}
componentDidMount() {
this.props.loadRules();
}
componentWillReceiveProps(nextProps) {
if (nextProps.rules.size && nextProps.rules !== this.props.rules) {
nextProps.rules
.entrySeq()
.map(([key, value]) => {
this.setState(state => ({
rules: {
...state.rules,
rule_regulations: {
...state.rules.rule_regulations,
[key]: value
}
},
numberOfInput: [...state.numberOfInput, guid()]
}));
})
.toArray();
}
}
handleChange = e => {
const { value, name } = e.target;
this.setState({
rules: {
...this.state.rules,
rule_regulations: {
...this.state.rules.rule_regulations,
[name]: value
}
}
});
};
handleAddRules = e => {
this.setState({
numberOfInput: [...this.state.numberOfInput, guid()]
});
};
handleRemoveRules = (e, num) => {
e.preventDefault();
this.setState({
numberOfInput: this.state.numberOfInput.filter(input => input !== num)
});
};
handleSave = e => {
e.preventDefault();
const obj = {
rule_regulations: Object.values(this.state.rules.rule_regulations)
};
this.props.postRules(obj);
};
render() {
const { numberOfInput } = this.state;
return (
<div className="basic-property">
<span className="circleInputUi" onClick={this.handleAddRules}>
+
</span>
<RulesInputContainer
numberOfInput={numberOfInput}
handleChange={this.handleChange}
handleRemoveRules={this.handleRemoveRules}
handleSave={this.handleSave}
value={this.state.rules.rule_regulations}
/>
</div>
);
}
}
const RulesInputContainer = props => {
return (
<div className="rules-input">
{props.value &&
Object.keys(props.value).map(key =>
<RulesInput
key={key}
num={key}
value={props.value}
handleChange={props.handleChange}
handleRemoveRules={props.handleRemoveRules}
/>
)}
<button className="button" onClick={props.handleSave}>
Save
</button>
</div>
);
};
export default RulesInputContainer;
const RulesInput = props => {
return (
<form className="form">
<InputField
label={`Rule ${props.num + 1}`}
type="text"
name={`${props.num}`}
value={props.value[props.num] || ""}
onChange={props.handleChange}
/>
<Button onClick={e => props.handleRemoveRules(e, props.num)}>
Remove
</Button>
</form>
)
}
I want the synchronization with server data and client
You can store returned data from your server into your component state.
componentDidMount() {
this.loadRules();
}
loadRules() {
let rules = this.props.loadRules();
this.setState({rules});
}
You can keep the synchronization by setting your state with data you received from your server.
In addition, it's not related to your question. Since RulesInputContainer and RulesInput are presentational components I think you can combine those into 1 component.

Categories

Resources