crazyones110
crazyones110

Reputation: 397

multiple layer of forwardRef and useImperativeHandle

I need to use ref to get a grandchild component's state, and I customized it by using useImperativeHandle, the whole code is simplified here.

function App() {
  const ref = useRef(null);
  return (
    <div className="App">
      <button
        onClick={() => console.log(ref.current.name, ref.current.age)}
      >click me</button>
      <FilterFather ref={ref} />
    </div>
  );
}

const FilterFather = (_, ref) => {
  const filter1Ref = useRef(null);
  const filter2Ref = useRef(null);
  useImperativeHandle(ref, () => ({
    name: filter1Ref.current.name,
    age: filter2Ref.current.age,
  }))
  return (
    <>
      <Filter1 ref={filter1Ref}/>
      <Filter2 ref={filter2Ref} />
    </>
  )
}
export default forwardRef(FilterFather);
const Filter1 = (props, ref) => {
  const [name, setName] = useState('lewis')
  useImperativeHandle(ref, () => ({
    name
  }), [name])
  return (
    <>
      <div>
        name:
        <input 
          value={name}
          onChange={e => setName(e.target.value)}
        />
      </div>
    </>
  )
}
const Filter2 = (props, ref) => {
  const [age, setAge] = useState(18)
  useImperativeHandle(ref, () => ({
    age
  }), [age])
  return (
    <>
      <div>
        age:
        <input 
          value={age}
          onChange={e => setAge(e.target.value)}
        />
      </div>
    </>
  )
}
export default {
  Filter1: forwardRef(Filter1),
  Filter2: forwardRef(Filter2),
}

one layer of forwardRef and useImperativeHandle works fine, two layers went wrong

Upvotes: 4

Views: 5994

Answers (1)

trixn
trixn

Reputation: 16309

Your imperative handle in FilterFather is not required. It doesn't add/remove anything to the handle. You can just forward it directly:

const FilterFather = (_, ref) => {
  return <Filter ref={ref} />;
};

Edit useImperativeHandle

Also there is a problem with it because it will not update correctly.

useImperativeHandle(ref, () => ({
    name: filterRef.current.name,
    age: filterRef.current.age,
}), [filterRef]) // this will not update correctly

You passed filterRef as a dependency but filterRef is static and will not change even when filterRef.current.name or filterRef.current.age changes.

Note that useImperativeHandle is not supposed to be used to read state from child components. It is actually discouraged to use any kind of imperative methods in react except there is no other way. This is most of the time the case if you work with 3rd party libraries that are imperative in nature.

EDIT:

Given your updated code the react way to do that is to lift state up. It doesn't require any refs:

export default function App() {
  const [values, setValues] = useState({
    name: "lewis",
    age: 18
  });

  const handleChange = useCallback(
    (key, value) => setValues(current => ({ ...current, [key]: value })),
    []
  );

  return (
    <div className="App">
      <button onClick={() => console.log(values.name, values.age)}>
        click me
      </button>
      <FilterFather values={values} onChange={handleChange} />
    </div>
  );
}
const FilterFather = props => {
  return (
    <>
      <Filter label="Name" name="name" {...props} />
      <Filter label="Age" name="age" {...props} />
    </>
  );
};
const Filter = ({ label, name, values, onChange }) => {
  const handleChange = useCallback(e => onChange(name, e.target.value), [
    name,
    onChange
  ]);

  return (
    <>
      <div>
        <label>{label}:</label>
        <input value={values[name]} onChange={handleChange} />
      </div>
    </>
  );
};

Edit useImperativeHandle

Upvotes: 3

Related Questions