Search code examples
reactjsreact-propsreact-state

React: multiple state values changing a composed parent's value?


I'm having hard time introducing UrlInput component in React. The main idea is to get props, like url and its setter (from parent) and split it internally into 3 UrlInput state values, e.g. protocol, host and port. UrlInput should use 3 form fields and change a single url in parent.

Simplified version:

export const UrlInput = ({ url, parentSetter }) => {
  const [fields, setFields] = useState({ protocol: "", host: "", port: 0 });

  useEffect(() => {
    const urlRegex = /(https?):\/\/([a-z]+):([0-9]{4,6})/;
    const [, protocol, host, port] = url.match(urlRegex);
    setFields({ protocol, host, port });
  }, [url]);

  const onChange = e => {
    setFields({ ...fields, [e.target.name]: e.target.value });
    parentSetter(`${fields.protocol}://${fields.host}:${fields.port}`);
  };

  return (
    <>
      <input type="text" name="protocol" value={fields.protocol} onChange={onChange} />
      ://
      <input type="text" name="host" value={fields.host} onChange={onChange} />
      :
      <input type="number" name="port" value={fields.port} onChange={onChange} />
      <p>{JSON.stringify(fields)}</p>
    </>
  );
};
function App({ url: initialUrl }) {
  const [url, setUrl] = useState(initialUrl);
  const [age, setAge] = useState(0);
  return (
    <>
      <input type="number" name="age" value={age} onChange={e => setAge(e.target.value)} />
      <br />
      <UrlInput url={url} parentSetter={setUrl} />
      <p>{JSON.stringify({ url, age })}</p>
    </>
  );
}

Current behavior:

  1. First change, e.g. protocol: UrlInput state changes, parent's state does not
  2. Second change, e.g. host: Parent synchronizes protocol with UrlInput and UrlInput changes have no effect
  3. Same as 1
  4. Same as 2

and so on. You can verify the behavior here.

I want to have a simple API - just url and setter, but maybe it's not how it should be done?


Solution

  • use this code:

    const onChange = e => {
      const data = { ...fields, [e.target.name]: e.target.value };
      setFields(data);
      parentSetter(`${data.protocol}://${data.host}:${data.port}`);
    };