Definitelytyped: [@types/react] RefObject.current não deve mais ser somente leitura

Criado em 5 dez. 2018  ·  48Comentários  ·  Fonte: DefinitelyTyped/DefinitelyTyped

Agora não há problema em atribuir a ref.current , veja o exemplo: https://reactjs.org/docs/hooks-faq.html#is -there-something-like-instance-variables

Tentar atribuir um valor a ele dá o erro: Cannot assign to 'current' because it is a constant or a read-only property.

  • [x] Tentei usar o pacote @types/react e tive problemas.
  • [x] Tentei usar a versão estável mais recente do tsc. https://www.npmjs.com/package/typescript
  • [x] Tenho uma pergunta inadequada para StackOverflow . (Por favor, faça todas as perguntas apropriadas lá).
  • [x] [Mencione](https://github.com/blog/821-mention-somebody-they-re-notified) os autores (veja Definitions by: em index.d.ts ) para que eles possam responder.

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

Comentários muito úteis

Não é. É intencionalmente deixado como somente leitura para garantir o uso correto, mesmo que não esteja congelado. Refs inicializadas com null sem indicar especificamente que você deseja atribuir null a ela são interpretadas como refs que você deseja que sejam gerenciadas pelo React -- ou seja, o React "possui" o atual e você está apenas visualizando-o.

Se você quiser um objeto ref mutável que comece com um valor nulo, certifique-se de fornecer também | null ao argumento genérico. Isso o tornará mutável, porque você o "possui" e não React.

Talvez isso seja mais fácil para mim entender, pois já trabalhei com linguagens baseadas em ponteiro muitas vezes antes e a propriedade é _muito_ importante nelas. E é isso que os refs são, um ponteiro. .current está desreferenciando o ponteiro.

Todos 48 comentários

Um PR seria apreciado! Deve ser uma mudança de uma linha muito fácil 😊

@ferdaber ,
Oi, eu gosto de escolher este problema. Como sou novo no texto datilografado (apenas uma semana) e será minha primeira contribuição para o código aberto.

Eu passei por node_modules/@types/react/index.d.ts e há o seguinte código na linha 61 que declara current como readonly.

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

se este for o problema, então eu posso resolver. você pode orientar para o meu primeiro PR?
Obrigado pelo seu tempo :)

Sim, está apenas removendo o modificador readonly nessa linha.

React.useRef retorna um MutableRefObject cuja propriedade current não é readonly . Suponho que a questão seja se os tipos de objeto ref devem ser unificados.

Eles devem ser unificados, o runtime do React não tem limitação na propriedade current criada por React.createRef() , o próprio objeto é selado, mas não congelado.

Não é. É intencionalmente deixado como somente leitura para garantir o uso correto, mesmo que não esteja congelado. Refs inicializadas com null sem indicar especificamente que você deseja atribuir null a ela são interpretadas como refs que você deseja que sejam gerenciadas pelo React -- ou seja, o React "possui" o atual e você está apenas visualizando-o.

Se você quiser um objeto ref mutável que comece com um valor nulo, certifique-se de fornecer também | null ao argumento genérico. Isso o tornará mutável, porque você o "possui" e não React.

Talvez isso seja mais fácil para mim entender, pois já trabalhei com linguagens baseadas em ponteiro muitas vezes antes e a propriedade é _muito_ importante nelas. E é isso que os refs são, um ponteiro. .current está desreferenciando o ponteiro.

Isso é justo, eu tenho usado React.createRef() como uma função auxiliar para apenas criar um ponteiro, já que o React não irá gerenciá-lo a menos que seja passado como um prop ref , mas você pode apenas bem criar um objeto ponteiro simples com a mesma estrutura.

Poderíamos modificar createRef para ter a mesma lógica de sobrecarga, mas como não há argumento, teria que ser com um retorno de tipo condicional. Se você incluir | null , ele retornará MutableRefObject , se não incluir, será (imutável) RefObject .

Isso funcionaria para aqueles que não têm strictNullTypes habilitados?

🤔

Devemos realmente tornar mais fácil escrever código incorreto para aqueles que _do_ tem strictNullTypes habilitado?

Quando criei esse problema, não percebi que já havia essa sobrecarga de | null . Não é um padrão muito comum, então eu não esperava que algo assim existisse. Eu não sei como eu perdi isso na documentação, porém, está muito bem documentado e explicado. Não há problema em fechar isso como um não problema, a menos que você queira unificar useRef e createRef .

Como o problema em si foi baseado em useRef e não createRef , também concordo em fechar isso, pois poucas pessoas modificam diretamente o valor do ponteiro desreferenciado em createRef .

Eu me pergunto se esse padrão de sobrecarga funcionará bem, olhando para a documentação de ganchos, vemos alguns usos de useRef com um valor padrão, caso em que o valor de .current pode nunca ser null, mas pode ser meio irritante para os usuários usarem constantemente o operador de asserção não nulo para passar por isso.

Faria mais sentido que o valor de retorno fosse mutável se um valor inicial fosse fornecido, mas imutável se fosse omitido? Se um valor inicial for fornecido, então o ref provavelmente não será passado para um componente via ref e usado mais como uma variável de instância. Por outro lado, ao criar um ref apenas para ser passado para um componente, um valor inicial provavelmente não será fornecido.

É mais ou menos isso que eu estava pensando. @Kovensky quais são seus pensamentos?

@ferdaber useRef , como está definido agora, sempre será mutável _e_ não anulável, a menos que você explicitamente forneça um argumento genérico que não inclua | null e um valor inicial de null .

Refs que são passadas para um componente devem ser inicializadas com null porque é para isso que o React define refs quando elas estão sendo liberadas (por exemplo, ao desmontar um componente que está montado condicionalmente). useRef sem passar um valor faria com que começasse undefined , então agora você também teria que adicionar | undefined a algo que só precisava se preocupar com | null .

O problema na documentação do React e usando useRef sem um valor inicial é que a equipe do React não parece se importar muito com a diferença entre null e undefined . Isso pode ser uma coisa do Flow.


De qualquer forma, a forma como defini useRef se encaixa exatamente no caso de uso do @bschlenk , só que null é o valor "omitido", pelos motivos que mencionei acima.

Ah, olhando mais de perto mostra isso, e é interessante que seja inicializado como undefined na fonte React sem um parâmetro. Legal, então está tudo perfeito, parece 👍

Eu acho que está tudo bem do jeito que está, apenas um pouco estranho que, se você incluir o | null , o tipo de current ainda pode ser nulo, apenas a mutabilidade mudou. Além disso, os documentos do React sempre passam explicitamente null, então é válido omitir o valorInicial?

Geralmente não é, e pelo menos no passado, quando apontei isso em alguma documentação, a equipe do React disse que "mesmo que funcione" não deve ser omitido.

É por isso que o primeiro argumento para createContext é necessário, por exemplo, mesmo se você quiser que um contexto comece com undefined . Você tem que realmente passar undefined .

Parece que em alguns usos não está usando o parâmetro:

https://reactjs.org/docs/hooks-faq.html#is -there-something-like-instance-variables
https://reactjs.org/docs/hooks-faq.html#how -to-get-the-previous-props-or-state
https://reactjs.org/docs/hooks-faq.html#how -to-read-an-often-change-value-from-usecallback

@ferdaber isso é porque eles não estão se importando com os tipos lá. Qual deve ser o tipo de current em um useRef() (sem argumentos)? undefined ? any ? Seja o que for, mesmo que você dê um argumento genérico, terá que ser | ed com undefined . _E_ se for um ref que você usa como um elemento react ref (não apenas como armazenamento local de thread), então você também tem que | null porque o React pode escrever um null lá.

Agora você encontra current sendo T | null | undefined em vez de apenas 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)

Sim, este DX funciona para mim, e a documentação é A++, então não acho que a maioria das pessoas ficará confusa com isso. Obrigado por esta elaboração! Honestamente, você deve apenas vincular permanentemente esse problema no typedoc :)

Pode haver um caso para suportar useRef<T>() e fazê-lo se comportar da mesma forma que useRef<T | undefined>(undefined) ; mas qualquer ref que você faça com isso ainda não pode ser usado como um elemento ref, apenas como armazenamento local de thread.

O problema é... o que acontece se você _não_ der um argumento genérico, o que é permitido? O TypeScript apenas inferirá {} . O tipo padrão correto é unknown mas não podemos usá-lo.

Estou recebendo este erro com o seguinte código:

~~~js
// ...
deixe intervaloRef = useRef(nulo); // também tentei com const em vez de let
// ...
useEfeito( () => {
const interval = setInterval( () => { /* faça algo */}, 1000);
intervaloRef.atual = intervalo; // Nesta linha estou recebendo o erro

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

})
// ...
~E quando eu removo o readonly aqui funciona:~ js
interface RefObject{
corrente somente leitura: T | nulo;
}
~~~

Eu sou novo com ganchos de reack e texto datilografado (apenas testando-os juntos) para que meu código possa estar errado

Por padrão, se você criar uma referência com um valor padrão null e especificar seu parâmetro genérico, você estará sinalizando sua intenção de que o React "possa" a referência. Se você quiser alterar uma referência que você possui, você deve declará-la assim:

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

@ferdaber Obrigado por isso!

Levando isso adiante e olhando para o tipo de retorno de current , talvez seja T em vez de T | null ? Com o advento dos ganchos, _nem sempre_ temos o caso de que todos os refs podem ser null , particularmente no caso frequente em que useRef é chamado com um inicializador não nulo.

Continuando com a excelente lista de exemplos em https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment -446660394, se eu escrever:

const numericRef = useRef<number>(42);

qual deve ser o tipo de numericRef.current ? Não há _necessidade_ para que seja number | null .

Se definimos os tipos e funções da seguinte forma:

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 produziria os seguintes usos e 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

Tem alguma coisa errada?

Para a resposta "qual é o tipo de um useRef não parametrizado?", a resposta é que essa chamada (apesar da documentação) está incorreta, de acordo com a equipe do React.

Eu adicionei a sobrecarga com |null _specifically_ como uma sobrecarga de conveniência para DOM/component refs, porque eles sempre iniciam null, eles sempre são redefinidos para null na desmontagem e você nunca reatribui a atual, apenas React.

O readonly existe mais para se proteger contra erros de lógica do que uma representação do verdadeiro objeto congelado JavaScript/propriedade somente getter.

Você só pode fazer isso por acidente quando ambos fornecem um argumento genérico que diz que você não aceita null enquanto inicializa o valor como null de qualquer maneira. Qualquer outro caso será mutável.

Ah, sim, vejo agora que MutableRefObject<T> já tem o case | null removido, comparado a RefObject<T> . Então o caso useRef<number>(42) já funciona corretamente. Obrigado pelo esclarecimento!

O que precisamos fazer com createRef ? No momento, ele retorna um RefObject<T> imutável, o que está causando problemas em nossa base de código, pois gostaríamos de passá-los e usá-los da mesma maneira que objetos (mutáveis) useRef ref. Existe uma maneira de ajustar a digitação createRef para permitir que ela forme objetos ref mutáveis?

(E o atributo ref é definido para ser do tipo Ref<T> que inclui RefObject<T> , tornando tudo imutável. Este é um grande problema para nós: mesmo se obtivermos um valor mutável ref de useRef , não podemos usar o fato de que é imutável por meio de uma chamada forwardRef .)

Hmm... talvez pudéssemos usar o mesmo truque, só porque não tem argumentos (e sempre começa como null) precisaria de um tipo condicional.

: null extends T ? MutableRefObject<T> : RefObject<T> deve fazer e usar a mesma lógica. Se você disser que deseja colocar nulos, é mutável, se não, ainda é imutável no significado atual.

Essa é uma ideia muito legal. Como createRef não aceita nenhum parâmetro, ele provavelmente sempre precisa incluir opções | null (diferente useRef ), então isso provavelmente precisaria dizer MutableRefObject<T | null> ?

Também não consegui trabalhar no TS. Aqui está o playground TS configurado com ele:
https://tinyurl.com/y75c32y3

O tipo é sempre detectado como MutableRefObject .

O que você acha que podemos fazer com forwardRef ? Ele declara o ref como sendo um Ref<T> , o que não inclui a possibilidade de um MutableRefObject .

(Nosso caso de uso que está causando dificuldade é o de uma função mergeRefs que recebe uma matriz de refs, que podem ser refs funcionais ou objetos ref, e cria uma única ref combinada [uma ref funcional] que pode ser passada para um componente. Essa ref combinada entrega qualquer elemento referenciado de entrada para todas as refs fornecidas, chamando-as se forem refs funcionais ou definindo .current se forem objetos ref. Mas a existência de o imutável RefObject<T> e a falta de inclusão de MutableRefObject<T> in Ref<T> tornam isso difícil. Devo levantar uma questão separada para o forwardRef e Refdificuldades?)

Não estamos alterando o tipo de useRef pelos motivos detalhados acima (embora createRef ainda esteja em discussão), você teve alguma dúvida sobre o raciocínio?

Devo levantar um problema separado para os itens abordados em https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment -457650501?

Sim, vamos separar.

Se você quiser um objeto ref mutável que comece com um valor nulo, certifique-se de fornecer também | null ao argumento genérico. Isso o tornará mutável, porque você o "possui" e não React.

Obrigado por isso! Adicionar null ao genérico useRef resolveu para mim.

Antes de

const ref = useRef<SomeType>(null) 

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

Depois de

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

Outro comentário sobre componentes forwardRef foi criado? Essencialmente, você não pode encaminhar uma referência e alterar seu valor atual diretamente, o que acho que faz parte do objetivo de encaminhar a referência.

Criei o seguinte 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,
    );

E recebo um erro: "Não é possível atribuir a 'atual' porque é uma propriedade somente leitura."
Existe uma maneira de resolvê-lo sem alterar current para gravável?

Para isso, você vai ter que trapacear.

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

Sim, isso é um pouco incorreto, mas é o único caso em que consigo pensar em que esse tipo de atribuição é deliberada e não um acidente - você está replicando um comportamento interno do React e, portanto, precisa quebrar as regras.

Poderíamos modificar createRef para ter a mesma lógica de sobrecarga, mas como não há argumento, teria que ser com um retorno de tipo condicional. Se você incluir | null , ele retornará MutableRefObject , se não incluir, será (imutável) RefObject .

Muito obrigado

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

deveria estar:

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

você está replicando um comportamento interno do React

Isso significa que os documentos aqui estão desatualizados? Porque eles descrevem isso claramente como um fluxo de trabalho pretendido, não como um comportamento interno.

O docs nesse caso está se referindo a um ref que você possui, mas para refs que você passa como um atributo ref para um elemento HTML, então eles devem ser readonly _to you_.

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>
}

Foi mal, deveria ter rolado mais para cima. Recebi um erro sobre readonly para um ref que eu possuía e presumi que era o mesmo caso. (Eu uso um fluxo de trabalho diferente agora e não consigo mais reproduzir o erro, infelizmente...)

De qualquer forma, muito obrigado pela explicação!

Não estou claro se a parte createRef do tópico mudou - mas vou postar aqui também.

Eu uso uma biblioteca de navegação popular para React Native (React Navigation). Em sua documentação, ele normalmente chama createRef e, em seguida, altera a ref. Tenho certeza de que isso ocorre porque o React não os está gerenciando (não há DOM).

O tipo para React Native deve ser diferente?

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

@sylvanaar
Também enfrentei esse problema ao inicializar a navegação, você encontrou uma solução?

Resumindo o que acabei de ler: A única maneira de definir um valor para um ref criado por createRef<T> é convertê-lo toda vez que você o usar? Qual é o raciocínio por trás disso? Nesse caso, o readonly está praticamente impedindo a configuração de um valor para o ref, portanto, anulando todo o propósito da função.

TLDR: Se o seu valor inicial for null (detalhes: ou qualquer outra coisa fora do parâmetro de tipo), adicione | null ao seu parâmetro de tipo, e isso deve tornar .current capaz para ser atribuído como normal.

Esta página foi útil?
0 / 5 - 0 avaliações