Reputation: 71
I tried to make this code working as functional component, but got stuck.
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import './index.css';
import { Tag, Input, Tooltip } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
class EditableTagGroup extends React.Component {
state = {
tags: ['Unremovable', 'Tag 2', 'Tag 3'],
inputVisible: false,
inputValue: '',
editInputIndex: -1,
editInputValue: '',
};
handleClose = removedTag => {
const tags = this.state.tags.filter(tag => tag !== removedTag);
console.log(tags);
this.setState({ tags });
};
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
};
handleInputChange = e => {
this.setState({ inputValue: e.target.value });
};
handleInputConfirm = () => {
const { inputValue } = this.state;
let { tags } = this.state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
console.log(tags);
this.setState({
tags,
inputVisible: false,
inputValue: '',
});
};
handleEditInputChange = e => {
this.setState({ editInputValue: e.target.value });
};
handleEditInputConfirm = () => {
this.setState(({ tags, editInputIndex, editInputValue }) => {
const newTags = [...tags];
newTags[editInputIndex] = editInputValue;
return {
tags: newTags,
editInputIndex: -1,
editInputValue: '',
};
});
};
saveInputRef = input => {
this.input = input;
};
saveEditInputRef = input => {
this.editInput = input;
};
render() {
const { tags, inputVisible, inputValue, editInputIndex, editInputValue } = this.state;
return (
<>
{tags.map((tag, index) => {
if (editInputIndex === index) {
return (
<Input
ref={this.saveEditInputRef}
key={tag}
size="small"
className="tag-input"
value={editInputValue}
onChange={this.handleEditInputChange}
onBlur={this.handleEditInputConfirm}
onPressEnter={this.handleEditInputConfirm}
/>
);
}
const isLongTag = tag.length > 20;
const tagElem = (
<Tag
className="edit-tag"
key={tag}
closable={index !== 0}
onClose={() => this.handleClose(tag)}
>
<span
onDoubleClick={e => {
if (index !== 0) {
this.setState({ editInputIndex: index, editInputValue: tag }, () => {
this.editInput.focus();
});
e.preventDefault();
}
}}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
className="tag-input"
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag className="site-tag-plus" onClick={this.showInput}>
<PlusOutlined /> New Tag
</Tag>
)}
</>
);
}
}
ReactDOM.render(<EditableTagGroup />, document.getElementById('container'));
My code throws an error when I press "add tag" button: Unhandled Runtime Error TypeError: Cannot read properties of undefined (reading 'map'). Plus setState expects 1 argument, but gets 2(2nd one is a function). I might have problems with understanding of hooks. Here is where I got stuck:
import { PlusOutlined } from '@ant-design/icons';
import { useRef, useState } from 'react';
const Tags = () => {
const [state, setState] = useState({
tags: ['Unremovable', 'Tag1', 'Tag2', 'Tag3'],
inputVisible: false,
inputValue: '',
editInputIndex: -1,
editInputValue: '',
});
const handleClose = removedTag => {
const tags = state.tags.filter(tag => tag !== removedTag);
console.log(tags);
setState({ tags });
};
const showInput = () => {
setState({ inputVisible: true }, () => input.focus());
};
const handleInputChange = e => {
setState({ inputValue: e.target.value });
};
const handleInputConfirm = () => {
const { inputValue } = state;
let { tags } = state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
console.log(tags);
setState({
tags,
inputVisible: false,
inputValue: '',
});
};
const handleEditInputChange = e => {
setState({ editInputValue: e.target.value });
};
const handleEditInputConfirm = () => {
setState(({ tags, editInputIndex, editInputValue }) => {
const newTags = [...tags];
newTags[editInputIndex] = editInputValue;
return {
tags: newTags,
editInputIndex: -1,
editInputValue: '',
};
});
};
const inputRef = useRef('')
const saveInputRef = input => {
inputRef.current = input;
};
const editInput = useRef('')
const saveEditInputRef = input => {
editInput.current = input;
};
const { tags, inputVisible, inputValue, editInputIndex, editInputValue } =
state;
return (
<>
{tags.map((tag, index) => {
if (editInputIndex === index) {
return (
<Input
ref={saveEditInputRef}
key={tag}
size="small"
className="tag-input"
value={editInputValue}
onChange={handleEditInputChange}
onBlur={handleEditInputConfirm}
onPressEnter={handleEditInputConfirm}
/>
);
}
const isLongTag = tag.length > 20;
const tagElem = (
<Tag
className="edit-tag"
key={tag}
closable={index !== 0}
onClose={() => handleClose(tag)}
>
<span
onDoubleClick={e => {
if (index !== 0) {
setState(
{ editInputIndex: index, editInputValue: tag },
() => {
editInput.focus();
});
e.preventDefault();
}
}}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible && (
<Input
ref={saveInputRef}
type="text"
size="small"
className="tag-input"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag className="site-tag-plus" onClick={showInput}>
<PlusOutlined /> add tag
</Tag>
)}
</>
)
};
export default Tags;
Upvotes: 0
Views: 851
Reputation: 71
Here is working piece of the code for anyone who needs!
import { PlusOutlined } from '@ant-design/icons';
import { useEffect, useRef, useState } from 'react';
const Tags = () => {
const [state, setState] = useState({
tags: ['Unremovable', 'Tag 1', 'Tag 2', 'Tag 3'],
inputVisible: false,
inputValue: '',
editInputIndex: -1,
editInputValue: '',
});
const handleClose = removedTag => {
const tags = state.tags.filter(tag => tag !== removedTag);
console.log(tags);
setState(state => ({ ...state, tags }));
};
const showInput = () => {
setState(state => ({ ...state, inputVisible: true }));
};
const handleInputChange = e => {
setState({ ...state, inputValue: e.target.value });
};
const handleInputConfirm = () => {
const { inputValue } = state;
let { tags } = state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
console.log(tags);
setState({
...state,
tags,
inputVisible: false,
inputValue: '',
});
};
const handleEditInputChange = e => {
setState(state => ({ ...state, editInputValue: e.target.value }));
};
const handleEditInputConfirm = () => {
setState(({ tags, editInputIndex, editInputValue, ...state }) => {
const newTags = [...tags];
newTags[editInputIndex] = editInputValue;
return {
...state,
tags: newTags,
editInputIndex: -1,
editInputValue: '',
};
});
};
const inputRef = useRef(null);
const saveInputRef = input => {
inputRef.current = input;
};
const editInput = useRef(null);
const saveEditInputRef = input => {
editInput.current = input;
};
const { tags, inputVisible, inputValue, editInputIndex, editInputValue } =
state;
useEffect(() => {
if (state.inputVisible) {
inputRef.current.focus();
}
}, [state, saveInputRef]);
return (
<>
{tags.map((tag, index) => {
if (editInputIndex === index) {
return (
<Input
ref={saveEditInputRef}
key={tag}
size="small"
className="tag-input"
value={editInputValue}
onChange={handleEditInputChange}
onBlur={handleEditInputConfirm}
onPressEnter={handleEditInputConfirm}
/>
);
}
const isLongTag = tag.length > 20;
const tagElem = (
<Tag
className="edit-tag"
key={tag}
closable={index !== 0}
onClose={() => handleClose(tag)}
>
<span
onDoubleClick={e => {
if (index !== 0) {
setState(state => ({
...state,
editInputIndex: index,
editInputValue: tag,
}));
e.preventDefault();
}
}}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible && (
<Input
ref={saveInputRef}
type="text"
size="small"
className="tag-input"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag className="site-tag-plus" onClick={showInput}>
<PlusOutlined /> add tag
</Tag>
)}
</>
);
};
export default Tags;
Upvotes: 0
Reputation: 165
So, The problem occurred in this piece of code.
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
};
Problem 1: Cannot read the property of undefined. This was happening because when you were setting the new state, The whole state value was replaced by only the inputVisible property. The tags property and other ones were fully removed from the variable. So for overcoming this error you need to spread the other values of the state variable by using the Spread Operator. Spread Operator. so the new piece of code would be
const showInput = () => {
setState( prevState =>
({ ...prevState , inputVisible: true })
// () => input.focus()
);
};
Here the prevState is the previous values of the state variable.
Problem 2: By default setState can recieve only one value, that is for updating the state variable. It cannot recieve 2 arguments. If you want to do any task depending on the state change, you can use UseEffect Hooks. Where you just have to add the state variable inside the Dependency Array of your useEffect hooks
Upvotes: 1
Reputation: 202864
The issue you have is that you missed picking up that state updates in function components via the useState
hook are not shallowly merged into state.
Note
Unlike the
setState
method found in class components,useState
does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:const [state, setState] = useState({}); setState(prevState => { // Object.assign would also work return {...prevState, ...updatedValues}; });
Because you were not merging state updates, when you clicked the add tag button the handler was removing the tags
property from state.
const showInput = () => {
setState({ inputVisible: true }); // <-- no tags property!!
};
Unhandled Runtime Error TypeError: Cannot read properties of undefined (reading 'map')
You must manage state merges yourself. Use a functional state update to shallow copy the previous state into the next state object being returned.
const Tags = () => {
const [state, setState] = useState({
tags: ["руль", "фара", "табло", "дворники"],
inputVisible: false,
inputValue: "",
editInputIndex: -1,
editInputValue: ""
});
const handleClose = (removedTag) => {
const tags = state.tags.filter((tag) => tag !== removedTag);
setState((state) => ({ ...state, tags })); // <-- shallow copy previous state
};
const showInput = () => {
setState((state) => ({ ...state, inputVisible: true })); // <-- shallow copy previous state
};
const handleInputChange = (e) => {
setState((state) => ({ ...state, inputValue: e.target.value })); // <-- shallow copy previous state
};
const handleInputConfirm = () => {
const { inputValue } = state;
let { tags } = state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
console.log(tags);
setState((state) => ({
...state, // <-- shallow copy previous state
tags,
inputVisible: false,
inputValue: ""
}));
};
const handleEditInputChange = (e) => {
setState((state) => ({ ...state, editInputValue: e.target.value })); // <-- shallow copy previous state
};
const handleEditInputConfirm = () => {
setState(({ tags, editInputIndex, editInputValue, ...state }) => {
const newTags = [...tags];
newTags[editInputIndex] = editInputValue;
return {
...state, // <-- shallow copy previous state
tags: newTags,
editInputIndex: -1,
editInputValue: ""
};
});
};
...
return (
<>
...
</>
);
};
setState expects 1 argument, but gets 2(2nd one is a function)
The useState
updater functions only take a single argument. If you need to run an effect after a state update, use an useEffect
with appropriate dependency.
Example:
const showInput = () => {
setState({ inputVisible: true });
};
React.useEffect(() => {
if (state.inputVisible) {
input.focus(); // * NOTE
}
}, [state, input]);
* Note: In your code I didn't see where any input
variable is declared.
Upvotes: 1