reactjstypescriptintellij-ideatslint

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


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);
  // ...
}

Solution

  • 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!, []);
    
      ...
    });