Definitelytyped: [@types/react] RefObject.current ya no debería ser de solo lectura

Creado en 5 dic. 2018  ·  48Comentarios  ·  Fuente: DefinitelyTyped/DefinitelyTyped

Ahora está bien asignar a ref.current , vea el ejemplo: https://reactjs.org/docs/hooks-faq.html#is -there-something-like-instance-variables

Intentar asignarle un valor da el error: Cannot assign to 'current' because it is a constant or a read-only property.

  • [x] Intenté usar el paquete @types/react y tuve problemas.
  • [x] Intenté usar la última versión estable de tsc. https://www.npmjs.com/package/mecanografiado
  • [x] Tengo una pregunta que no es apropiada para StackOverflow . (Por favor haga las preguntas apropiadas allí).
  • [x] [Mencionar](https://github.com/blog/821-mention-somebody-they-re-notified) a los autores (ver Definitions by: en index.d.ts ) para que puedan responder.

    • Autores: @johnnyreilly @bbenezech @pzavolinsky @digiguru @ericanderson @morcerf @tkrotoff @DovydasNavickas @onigoetz @theruther4d @guilhermehubner @ferdaber @jrakotoharisoa @pascaloliv @Hotell @franklixuefei

Comentario más útil

No es. Se deja intencionalmente como de solo lectura para garantizar el uso correcto, incluso si no está congelado. Las referencias inicializadas con nulo sin indicar específicamente que desea poder asignarle un valor nulo se interpretan como referencias que desea que React administre, es decir, React "posee" el actual y usted solo lo está viendo.

Si desea un objeto de referencia mutable que comience con un valor nulo, asegúrese de proporcionar también | null al argumento genérico. Eso lo hará mutable, porque lo "posees" y no React.

Quizás esto sea más fácil de entender para mí, ya que he trabajado con lenguajes basados ​​en punteros a menudo antes y la propiedad es _muy_ importante en ellos. Y eso es lo que son los árbitros, un puntero. .current está desreferenciando el puntero.

Todos 48 comentarios

¡Se agradecería un PR! Debería ser un cambio de una línea muy fácil 😊

@ferdaber ,
Hola, me gusta elegir este tema. Como soy nuevo en mecanografiado (solo una semana) y será mi primera contribución al código abierto.

Revisé node_modules/@types/react/index.d.ts y hay el siguiente código en la línea 61 que declara actual como de solo lectura.

interface RefObject<T> {
        readonly current: T | null;
    }

si este es el problema, entonces puedo resolverlo. ¿Pueden guiarme para mi primer PR?
Gracias por tu tiempo :)

Sí, solo está eliminando el modificador readonly en esa línea.

React.useRef devuelve un MutableRefObject cuya propiedad current no es readonly . Supongo que la pregunta es si los tipos de objetos ref deben unificarse.

Deben estar unificados, el tiempo de ejecución de React no tiene limitación en la propiedad current creada por React.createRef() , el objeto en sí está sellado pero no congelado.

No es. Se deja intencionalmente como de solo lectura para garantizar el uso correcto, incluso si no está congelado. Las referencias inicializadas con nulo sin indicar específicamente que desea poder asignarle un valor nulo se interpretan como referencias que desea que React administre, es decir, React "posee" el actual y usted solo lo está viendo.

Si desea un objeto de referencia mutable que comience con un valor nulo, asegúrese de proporcionar también | null al argumento genérico. Eso lo hará mutable, porque lo "posees" y no React.

Quizás esto sea más fácil de entender para mí, ya que he trabajado con lenguajes basados ​​en punteros a menudo antes y la propiedad es _muy_ importante en ellos. Y eso es lo que son los árbitros, un puntero. .current está desreferenciando el puntero.

Eso es justo, he estado usando React.createRef() como una función de ayuda para crear un puntero, ya que React no lo administrará a menos que se transfiera como un apoyo ref , pero puede igual vamos a crear un objeto de puntero simple con la misma estructura.

Podríamos modificar createRef para tener la misma lógica de sobrecarga, pero debido a que no hay ningún argumento, tendría que ser con un retorno de tipo condicional. Si incluye | null , devolverá MutableRefObject , si no lo hace, sería (inmutable) RefObject .

Sin embargo, ¿eso funcionaría para aquellos que no tienen strictNullTypes habilitado?

🤔

¿Realmente deberíamos facilitar la escritura de código incorrecto para aquellos que _sí_ tienen strictNullTypes habilitados?

Cuando creé este problema, no me di cuenta de que ya existía esta sobrecarga | null . No es un patrón muy común, así que no esperaba que existiera algo así. Sin embargo, no sé cómo me lo perdí en la documentación, está muy bien documentado y explicado. Estoy de acuerdo con cerrar esto como un problema a menos que desee unificar useRef y createRef .

Dado que el problema en sí se basó en useRef y no createRef , también estoy de acuerdo con cerrar esto ya que muy pocas personas modifican directamente el valor del puntero sin referencia en createRef .

Sin embargo, me pregunto si este patrón de sobrecarga funcionará bien. Al mirar la documentación de los ganchos, vemos algunos usos de useRef con un valor predeterminado, en cuyo caso el valor de .current puede que nunca sea nulo, pero puede ser un poco molesto para los usuarios usar constantemente el operador de aserción no nulo para superarlo.

¿Tendría más sentido que el valor de retorno sea mutable si se da un valor inicial, pero inmutable si se omite? Si se proporciona un valor inicial, es probable que la referencia no se pase a un componente a través ref y se use más como una variable de instancia. Por otro lado, al crear una referencia únicamente para pasarla a un componente, es probable que no se proporcione un valor inicial.

Eso es algo de lo que estaba pensando. @Kovensky ¿cuáles son tus pensamientos?

@ferdaber useRef tal como está definido ahora siempre será mutable _y_ no anulable a menos que ambos le den explícitamente un argumento genérico que no incluya | null y un valor inicial de null .

Las referencias que se pasan a un componente deben inicializarse con null porque eso es a lo que React establece las referencias cuando se liberan (por ejemplo, al desmontar un componente que está montado condicionalmente). useRef sin pasar un valor haría que comenzara undefined en su lugar, por lo que ahora también tendría que agregar | undefined a algo que solo tenía que importar | null .

El problema en la documentación de React y el uso useRef sin un valor inicial es que al equipo de React no parece importarle mucho la diferencia entre null y undefined . Eso podría ser una cosa de Flow.


De todos modos, la forma en que definí useRef se ajusta exactamente al caso de uso de @bschlenk , solo que null es el valor "omitido", por las razones que mencioné anteriormente.

Ah, mirar más de cerca muestra eso, y es interesante que se inicialice como undefined en la fuente de React sin un parámetro. Genial, así que todo es color de rosa, suena como 👍

Creo que está bien tal como está, solo un poco extraño que si incluyes | null , el tipo de current todavía puede ser nulo, solo que la mutabilidad ha cambiado. Además, los documentos de React siempre pasan explícitamente nulos, entonces, ¿es válido omitir el valor inicial?

Por lo general, no lo es, y al menos en el pasado, cuando señalé que en algún documento, el equipo de React dijo que "incluso si funciona", no debe omitirse.

Es por eso que se requiere el primer argumento para createContext , por ejemplo, incluso si desea que un contexto comience con undefined . Tienes que pasar undefined .

@ferdaber eso es porque no les importan los tipos allí. ¿Cuál debería ser el tipo de current en un useRef() (sin argumentos)? undefined ? any ? Sea lo que sea, incluso si proporciona un argumento genérico, tendrá que ser | ed con undefined . _Y_ si es una referencia que usa como referencia de elemento de reacción (no solo como almacenamiento local de subprocesos), entonces también debe | null porque React puede escribir un null allí.

Ahora te encuentras con current siendo T | null | undefined en lugar de solo T .

// you should always give an argument even if the docs sometimes miss them
// $ExpectError
const ref1 = useRef()
// this is a mutable ref but you can only assign `null` to it
const ref2 = useRef(null)
// this is also a mutable ref but you can only assign `undefined`
const ref3 = useRef(undefined)
// this is a mutable ref of number
const ref4 = useRef(0)
// this is a mutable ref of number | null
const ref5 = useRef<number | null>(null)
// this is a mutable ref with an object
const ref6 = useRef<React.CSSProperties>({})
// this is a mutable ref that can hold an HTMLElement
const ref7 = useRef<HTMLElement | null>(null)
// this is the only case where the ref is immutable
// you did not say in the generic argument you want to be able to write
// null into it, but you gave a null anyway.
// I am taking this as the sign that this ref is intended
// to be used as an element ref (i.e. owned by React, you're only sharing)
const ref8 = useRef<HTMLElement>(null)
// not allowed, because you didn't say you want to write undefined in it
// this is essentially what would happen if we allowed useRef with no arguments
// to make it worse, you can't use it as an element ref, because
// React might write a null into it anyway.
// $ExpectError
const ref9 = useRef<HTMLElement>(undefined)

Sí, este DX me funciona, y la documentación es A++, así que no creo que la mayoría de la gente se confunda. ¡Gracias por esta elaboración! Honestamente, deberías vincular este problema de forma permanente en el typedoc :)

Podría haber un caso para admitir useRef<T>() y hacer que se comporte igual que useRef<T | undefined>(undefined) ; pero cualquier referencia que haga con eso aún no se puede usar como una referencia de elemento, solo como almacenamiento local de subprocesos.

El problema es... ¿qué pasa si _no_ das un argumento genérico, que está permitido? TypeScript simplemente inferirá {} . El tipo predeterminado correcto es unknown pero no podemos usarlo.

Estoy recibiendo este error con el siguiente código:

~~~js
// ...
let intervaloRef = useRef(nulo); // también probé con const en lugar de let
// ...
usarEfecto( () => {
const intervalo = setInterval( () => { /* hacer algo */}, 1000);
refintervalo.actual = intervalo; // En esta linea me sale el error

return () => {
    clearInterval(intervalRef.current);
}

})
// ...
~Y cuando elimino el readonly aquí, funciona:~ js
interfaz RefObjeto{
corriente de solo lectura: T | nulo;
}
~~~

Soy nuevo con los ganchos reack y el mecanografiado (solo los pruebo juntos), por lo que mi código podría estar equivocado

De forma predeterminada, si crea una referencia con un valor predeterminado null y especifica su parámetro genérico, está indicando su intención de que React sea "propietario" de la referencia. Si desea poder mutar una referencia que posee, debe declararlo así:

const intervalRef= useRef<NodeJS.Timeout | null>(null) // <-- the generic type is NodeJS.Timeout | null

@ferdaber ¡ Gracias por eso!

Llevando esto un paso más allá y observando el tipo de devolución de current , ¿debería ser quizás T en lugar de T | null ? Con la llegada de los ganchos, _no siempre_ tenemos el caso de que todas las referencias puedan ser null , particularmente en el caso frecuente en el que se llama a useRef con un inicializador no nulo.

Continuando con la excelente lista de ejemplos en https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment -446660394, si escribo:

const numericRef = useRef<number>(42);

¿Cuál debería ser el tipo de numericRef.current ? No hay _necesidad_ de que sea number | null .

Si definimos los tipos y funciones de la siguiente manera:

interface RefObject<T> {
  current: T;
}

function useRef<T>(initialValue: T): RefObject<T>;
function useRef<T>(initialValue: T|null): RefObject<T | null>;

function createRef<T>(): RefObject<T | null>;

que produciría los siguientes usos y tipos:

const r1 = useRef(42);
// r1 is of type RefObject<number>
// r1.current is of type number (not number | null)

const r2 = useRef<number>(null);
// r2 is of type RefObject<number | null>
// r2.current is of type number | null

const r3 = useRef(null);
// r3 is of type RefObject<null>
// r3.current is of type null

const r4 = createRef<number>();
// r4 is of type RefObject<number | null>
// r4.current is of type number | null

¿Hay algo mal?

Para la respuesta a "¿cuál es el tipo de useRef sin parametrizar?", la respuesta es que esa llamada (a pesar de la documentación) es incorrecta, según el equipo de React.

Agregué la sobrecarga con |null _específicamente_ como una sobrecarga conveniente para las referencias de DOM/componentes, porque siempre comienzan en nulo, siempre se restablecen a nulo al desmontarlos, y nunca reasignas la corriente tú mismo, solo React.

El solo lectura está ahí más para protegerse contra los errores lógicos que una representación del verdadero objeto congelado de JavaScript/propiedad de solo captador.

Solo puede hacerlo por accidente cuando ambos dan un argumento genérico que dice que no acepta nulo mientras inicializa el valor en nulo de todos modos. Cualquier otro caso será mutable.

Ah, sí, ahora veo que MutableRefObject<T> ya tiene el caso | null eliminado, en comparación con RefObject<T> . Entonces el caso useRef<number>(42) ya funciona correctamente. ¡Gracias por la aclaración!

¿Qué tenemos que hacer con createRef ? En este momento, devuelve un RefObject<T> inmutable, que está causando problemas en nuestra base de código, ya que nos gustaría pasarlos y usarlos de la misma manera que los objetos ref (mutables) useRef . ¿Hay alguna manera de modificar el tipo createRef para permitirle formar objetos de referencia mutables?

(Y el atributo ref se define como del tipo Ref<T> que incluye RefObject<T> , lo que hace que todo sea inmutable. Este es un gran problema para nosotros: incluso si obtenemos un atributo mutable ref de useRef , no podemos hacer uso del hecho de que es inmutable a través de una llamada forwardRef ).

Hmm... tal vez podríamos usar el mismo truco, solo porque no tiene argumentos (y siempre comienza como nulo) necesitaría un tipo condicional.

: null extends T ? MutableRefObject<T> : RefObject<T> debería hacer y usar la misma lógica. Si dice que quiere poner nulos, es mutable, si no lo hace, sigue siendo inmutable en el significado actual.

Esa es una muy buena idea. Debido a que createRef no toma ningún parámetro, presumiblemente siempre debe incluir una opción | null (a diferencia useRef ), por lo que probablemente tendría que decir MutableRefObject<T | null> ?

No he podido conseguir ninguno de los dos para trabajar en TS. Aquí está el patio de juegos de TS configurado con él:
https://tinyurl.com/y75c32y3

El tipo siempre se detecta como MutableRefObject .

¿Qué crees que podemos hacer con forwardRef ? Declara el ref como un Ref<T> , que no incluye la posibilidad de un MutableRefObject .

(Nuestro caso de uso que está causando dificultad es el de una función mergeRefs que toma una matriz de referencias, que podrían ser referencias funcionales u objetos de referencia, y crea una sola referencia combinada [una referencia funcional] que se puede pasar a un componente. Esa referencia combinada luego entrega cualquier elemento referenciado entrante a todas las referencias proporcionadas, ya sea llamándolas si son referencias funcionales, o configurando .current si son objetos de referencia. Pero la existencia de el inmutable RefObject<T> y la falta de inclusión de MutableRefObject<T> in Ref<T> lo hace difícil. ¿Debería plantear un problema por separado para el forwardRef y Ref?¿dificultades?)

No vamos a cambiar el tipo de useRef por los motivos detallados anteriormente (aunque createRef todavía está en discusión), ¿tiene alguna pregunta sobre la justificación?

¿Debería plantear un problema por separado para los elementos cubiertos en https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment -457650501?

Sí, vamos a separarlo.

Si desea un objeto de referencia mutable que comience con un valor nulo, asegúrese de proporcionar también | null al argumento genérico. Eso lo hará mutable, porque lo "posees" y no React.

¡Gracias por esto! Agregar nulo al genérico useRef lo resolvió por mí.

Antes

const ref = useRef<SomeType>(null) 

// Later error: Cannot assign to 'current' because it is a constant or a read-only property.

Después

const ref = useRef<SomeType | null>(null)

¿Se creó otro comentario sobre los componentes forwardRef? Esencialmente, no puede reenviar una referencia y cambiar su valor actual directamente, lo que creo que es parte del objetivo de reenviar la referencia.

Creé el siguiente gancho:

export const useCombinedRefs = <T>(...refs: Ref<T>[]) =>
    useCallback(
        (element: T) =>
            refs.forEach(ref => {
                if (!ref) {
                    return;
                }

                if (typeof ref === 'function') {
                    ref(element);
                } else {
                    ref.current = element; // this line produces error
                }
            }),
        refs,
    );

Y aparece un error: "No se puede asignar a 'actual' porque es una propiedad de solo lectura".
¿Hay alguna manera de resolverlo sin cambiar current a escribible?

Para eso, vas a tener que hacer trampa.

(ref.current as React.MutableRefObject<T> ).current = element;

Sí, esto es un poco erróneo, pero es el único caso en el que puedo pensar en el que este tipo de asignación es deliberada y no accidental: está replicando un comportamiento interno de React y, por lo tanto, tiene que romper las reglas.

Podríamos modificar createRef para tener la misma lógica de sobrecarga, pero debido a que no hay ningún argumento, tendría que ser con un retorno de tipo condicional. Si incluye | null , devolverá MutableRefObject , si no lo hace, sería (inmutable) RefObject .

muchas gracias

(ref.current as React.MutableRefObject<T>).current = element;

debiera ser:

(ref as React.MutableRefObject<T>).current = element;

estás replicando un comportamiento interno de React

¿Eso significa que los documentos aquí están desactualizados? Porque describen claramente esto como un flujo de trabajo previsto, no como un comportamiento interno.

Los documentos en ese caso se refieren a una referencia de su propiedad, pero para las referencias que pasa como un atributo ref a un elemento HTML, entonces deberían ser de solo lectura _para usted_.

function Component() {
  // same API, different type semantics
  const countRef = useRef<number>(0); // not readonly
  const divRef = useRef<HTMLElement>(null); // readonly

  return <button ref={divRef} onClick={() => countRef.current++}>Click me</button>
}

Mi error, debería haberme desplazado más arriba. Recibí un error sobre readonly para una referencia que poseía y asumí que era el mismo caso. (Uso un flujo de trabajo diferente ahora y ya no puedo reproducir el error, desafortunadamente...)

De todos modos, muchas gracias por la explicación!

No tengo claro si la parte createRef del tema se movió, pero también publicaré aquí.

Uso una biblioteca de navegación popular para React Native (React Navigation). En su documentación, comúnmente llama a createRef y luego muta la ref. Estoy seguro de que esto se debe a que React no los está administrando (no hay DOM).

¿Debería ser diferente el tipo de React Native?

Ver: https://reactnavigation.org/docs/navigating-without-navigation-prop

@sylvanaar
También enfrenté este problema al inicializar la navegación, ¿has encontrado una solución?

Resumiendo lo que acabo de leer: ¿La única forma de establecer un valor para una referencia creada por createRef<T> es emitirla cada vez que la usa? ¿Cuál es el razonamiento detrás de eso? En ese caso, el readonly prácticamente evita que se establezca un valor para la referencia, por lo que anula todo el propósito de la función.

TLDR: si su valor inicial es null (detalles: o algo más fuera del parámetro de tipo), agregue | null a su parámetro de tipo, y eso debería hacer que .current sea ​​capaz para ser asignado a como normal.

¿Fue útil esta página
0 / 5 - 0 calificaciones