Dmitry Bochok
Dmitry Bochok

Reputation: 71

changing class to function component antd

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

Answers (3)

Dmitry Bochok
Dmitry Bochok

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

Argha Shipan Sarker
Argha Shipan Sarker

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

Drew Reese
Drew Reese

Reputation: 202864

Issue

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.

useState

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!!
};

Solution

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.

Edit changing-class-to-function-component-antd

Upvotes: 1

Related Questions