Type inference not working on reference parameter of React.forwardRef()

140 views Asked by At

I am trying to utilize React.forwardRef() and I'm scratching my head over the following issue:

enter image description here

It appears that IntelliJ is able to correctly interfer the type of the reference, which is FilterRef in this case.

However, I am unable to access the current value ref.current without a compilation error stating that this would not exist.

Printing it out at runtime, of course, confirms that current is an existing attribute.

Not sure what causes this issue.

Code

I am creating a reference which I want to share with a Filter component and a context-provider:

const filterRef = useRef<FilterElement>({ filters, setFilters: onSetFilters });

console.log('filterRef', filterRef);

return (
  <>
    <Filter ref={filterRef} />
    <FilterContext.Provider value={{ ref: filterRef }}>
      <Outlet />
    </FilterContext.Provider>
  </>
);

However, current is always null:

export const Filter = React.forwardRef<FilterElement, FilterProps>((props, ref) => {
  const filterRef = useRef<FilterElement>(null);
  useImperativeHandle(ref, () => filterRef.current!, []);

  console.log('filter', filterRef, ref);
  // ...
}
1

There are 1 answers

5
Danziger On BEST ANSWER

Edit 2

In order to have a separate state being managed by Filter itself, the following can be done as well:

export const Filter = React.forwardRef<FilterElement | undefined, FilterProps>((props, ref) => {
  const [filters, setFilters] = useState<FilterItem[]>([]);
  const refInstance: FilterElement = useMemo(() => {
    return {
      filters,
      setFilters,
    };
  }, [filters]);
  useImperativeHandle(ref, () => refInstance, [refInstance]);
  // ...
}

Edit:

Based on the updated code, I think you don't need the ref at all, as you can just pass filters and setFilters to both the Filter component as well as the FilterContext provider:

<Filter filters={ filters } setFilters={ onSetFilters } />
<FilterContext.Provider value={{ filters, setFilters: onSetFilters }}>
  <Outlet />
</FilterContext.Provider>

In any case, if you want to do that, you don't need to use forwardRef, just name your prop something else and type it as RefObject:

const App = () => {
  const filterRef = useRef<FilterElement>({ filters, setFilters: onSetFilters });

  return (
    <>
      <Filter filterRef={filterRef} />
      <FilterContext.Provider value={{ ref: filterRef }}>
        <Outlet />
      </FilterContext.Provider>
    </>
  );
} 

export interface FilterProps {
  filterRef: RefObject<FilterElement>;
}

export const Filter: React.FC<FilterProps> = ({ filterRef }) => {
  console.log('filter', filterRef);

  // ...
}

The answer below with useImperativeHandler would be used if you want to use a ref inside the Filter component, but also want to expose that same ref to the parent.

Original answer:

Refs can either be an object with a current property, like the ones returned by useRef or a ref callback function, so the compiler is right.

You should create a new ref in your Filter component and use useImperativeHandle to sync it with the forwarded one:

export const Filter = forwardRef<FilterRef, FilterProps>((props, ref) => {
  const filterRef = useRef(null);

  useImperativeHandle(ref, () => filterRef.current!, []);

  ...
});