Cannot assign to read only property 'current' in React useRef
Asked Answered
S

3

25

i used react useRef in functional components to get link on html object and store it in Recoil atom. For example:

const Children = () => {
  const [refLink, setSrefLink] = useRecoilState(refLink)
  return <input ref={someRef}/>
}
const Parent = () => {
  const [refLink, setSrefLink] = useRecoilState(refLink)
  const someRef = useRef();
  setSomeRef(someRef)
  return <Children />;
}

export const refLink = atom({
    key: 'refLink',
    default: null ,
});

But when my Parent component ummounts I get error:

react-dom.development.js:20997 Uncaught TypeError: Cannot assign to read only property 'current' of object '#' in file reac-dom.development.js

enter image description here

I can't imagine what's the problem;

Stouthearted answered 5/2, 2021 at 17:17 Comment(7)
reactjs.org/docs/forwarding-refs.htmlSpann
In your edited code, where does someRef come from in Children? (Also: Please try to avoid posting code that doesn't actually represent what you're actually using, then later changing it.)Mb
Also: Storing a ref in state seems...wrong. Why do you think you want to do that?Mb
@T.J.Crowder is any other way to tooltip for different elements? I want to just change links of anchor element to show tooltips in diferent componentsStouthearted
That's a completely different question from the question you asked. I'd need much more context to answer it. I suggest posting it separately. But fundamentally: you just render different tootips based on data. No refs required.Mb
@T.J.Crowder thanks, but tooltip position based on element postition. So i need to get current element position to bind it with tooltipStouthearted
@Stouthearted - That's fine, you'd use a ref for that. But you wouldn't store it in state.Mb
R
7

The issue here is that atoms are are frozen by default (see the documentation) and a ref works by mutating the current property of an object.

You could prevent object freezing by passing dangerouslyAllowMutability: true.

export const refLinkState = atom({
    key: 'refLink',
    default: null ,
    dangerouslyAllowMutability: true,
});

Note that this will only update all subscribers if the ref itself is replaced by another ref. If a ref consumer changes the current property, subscribers will not re-render because the ref object is still the same object.

You could solve this by not using a ref, but by passing the ref value directly into your shared state.

// without dangerouslyAllowMutability
export const refLinkState = atom({
    key: 'refLink',
    default: null ,
});

const Children = () => {
  const [refLink, setRefLink] = useRecoilState(refLinkState);
  return <input ref={setRefLink} />;
};

In the above scenario we've completely eliminated refs and instead store the DOM element in the recoil state without the ref wrapper.

However like the forward refs documentation mentions:

React components hide their implementation details, including their rendered output. Other components using FancyButton usually will not need to obtain a ref to the inner button DOM element. This is good because it prevents components from relying on each other’s DOM structure too much.

Without knowing much about the structure and what exactly you want to achieve, you could for example extract the relevant data in Child and store that in a shared state. But there is probably a better solution if we had more context.

Resistive answered 5/2, 2021 at 18:17 Comment(0)
A
47

If you're getting this error in TypeScript, try listing null in the type annotation, which changes this from a RefObject to a MutableRefObject.

From

const myRef = useRef<MyType>(null)

To

const myRef = useRef<MyType | null>(null)

After doing so, reassigning current should not result in this error (e.g. myRef.current = null).

Source

Artifice answered 22/3, 2022 at 15:3 Comment(1)
It is indeed changing the type as you said @nelu. Do you know how TypeScript does it?Crossgrained
R
7

The issue here is that atoms are are frozen by default (see the documentation) and a ref works by mutating the current property of an object.

You could prevent object freezing by passing dangerouslyAllowMutability: true.

export const refLinkState = atom({
    key: 'refLink',
    default: null ,
    dangerouslyAllowMutability: true,
});

Note that this will only update all subscribers if the ref itself is replaced by another ref. If a ref consumer changes the current property, subscribers will not re-render because the ref object is still the same object.

You could solve this by not using a ref, but by passing the ref value directly into your shared state.

// without dangerouslyAllowMutability
export const refLinkState = atom({
    key: 'refLink',
    default: null ,
});

const Children = () => {
  const [refLink, setRefLink] = useRecoilState(refLinkState);
  return <input ref={setRefLink} />;
};

In the above scenario we've completely eliminated refs and instead store the DOM element in the recoil state without the ref wrapper.

However like the forward refs documentation mentions:

React components hide their implementation details, including their rendered output. Other components using FancyButton usually will not need to obtain a ref to the inner button DOM element. This is good because it prevents components from relying on each other’s DOM structure too much.

Without knowing much about the structure and what exactly you want to achieve, you could for example extract the relevant data in Child and store that in a shared state. But there is probably a better solution if we had more context.

Resistive answered 5/2, 2021 at 18:17 Comment(0)
M
3

You can't just pass a ref as a prop.

Normally, a component hides its implementation so a parent component shouldn't be able to access a DOM element created by the child. But in the rare situation where you want to allow that on a component, you have to do so explicitly with forwardRef:

    const Children = React.forwardRef((props, someRef) => {
// −−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      return <input ref={someRef}/>
    });
// −−^
    const Parent = () => {
      const someRef = useRef();
      return <Children ref={someRef} />;
    };

More in the documentation.

Mb answered 5/2, 2021 at 17:24 Comment(4)
@Resistive - I completely missed that name, thanks!!Mb
@Resistive i localize my problem, i stored ref in recoil atom, if simple pass to components it works fineStouthearted
@Resistive the reason is to fast access to ref link in different components. For example, i want to make tooltip, so i can change refs in store to show it.Stouthearted
(Why do you keep @ notifying someone else?) "For example, i want to make tooltip, so i can change refs in store to show it." That isn't how refs work. Refs are managed by React, not by you.Mb

© 2022 - 2024 — McMap. All rights reserved.