Definitelytyped: [@types/react] RefObject.current больше не должен быть доступен только для чтения

Созданный на 5 дек. 2018  ·  48Комментарии  ·  Источник: DefinitelyTyped/DefinitelyTyped

Теперь можно присваивать ref.current , см. пример: https://reactjs.org/docs/hooks-faq.html#is -there-something-like-instance-variables

Попытка присвоить ему значение дает ошибку: Cannot assign to 'current' because it is a constant or a read-only property.

  • [x] Я пытался использовать пакет @types/react , и у меня возникли проблемы.
  • [x] Я пытался использовать последнюю стабильную версию tsc. https://www.npmjs.com/package/typescript
  • [x] У меня есть вопрос, который не подходит для StackOverflow . (Пожалуйста, задавайте любые соответствующие вопросы там).
  • [x] [Упомяните](https://github.com/blog/821-mention-somebody-they-re-notified) авторов (см. Definitions by: в index.d.ts ), чтобы они могли отвечать.

    • Авторы: @johnnyreilly @bbenezech @pzavolinsky @digiguru @ericanderson @morcerf @tkrotoff @DovydasNavickas @onigoetz @theruther4d @guilhermehubner @ferdaber @jrakotoharisoa @pascaloliv @Hotell @franklixuefei

Самый полезный комментарий

Это не. Он намеренно оставлен только для чтения, чтобы обеспечить правильное использование, даже если он не заморожен. Ссылки, инициализированные нулевым значением без конкретного указания, что вы хотите иметь возможность присвоить ему значение null, интерпретируются как ссылки, которыми вы хотите управлять с помощью React, т.е. React «владеет» текущим, и вы просто просматриваете его.

Если вам нужен изменяемый объект ref, начинающийся с нулевого значения, не забудьте также указать | null для универсального аргумента. Это сделает его изменчивым, потому что вы «владеете» им, а не React.

Возможно, мне это легче понять, так как я часто работал с языками, основанными на указателях, и право собственности в них _очень_ важно. И это то, что refs, указатель. .current разыменовывает указатель.

Все 48 Комментарий

PR приветствуется! Это должно быть очень простое изменение одной строки 😊

@фердабер ,
Привет, я хотел бы выбрать этот вопрос. Поскольку я новичок в машинописи (всего неделя), и это будет мой первый вклад в открытый исходный код.

Я прошел через node_modules/@types/react/index.d.ts, и в строке № 61 есть следующий код, который объявляет текущий как доступный только для чтения.

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

если это проблема, то я могу решить. Вы можете провести мой первый PR?
Спасибо за уделенное время :)

Да, это просто удаление модификатора readonly в этой строке.

React.useRef возвращает MutableRefObject , свойство current которого не равно readonly . Я предполагаю, что вопрос заключается в том, должны ли быть унифицированы типы объектов ref.

Они должны быть унифицированы, среда выполнения React не имеет ограничений на свойство current , созданное React.createRef() , сам объект запечатан, но не заморожен.

Это не. Он намеренно оставлен только для чтения, чтобы обеспечить правильное использование, даже если он не заморожен. Ссылки, инициализированные нулевым значением без конкретного указания, что вы хотите иметь возможность присвоить ему значение null, интерпретируются как ссылки, которыми вы хотите управлять с помощью React, т.е. React «владеет» текущим, и вы просто просматриваете его.

Если вам нужен изменяемый объект ref, начинающийся с нулевого значения, не забудьте также указать | null для универсального аргумента. Это сделает его изменчивым, потому что вы «владеете» им, а не React.

Возможно, мне это легче понять, так как я часто работал с языками, основанными на указателях, и право собственности в них _очень_ важно. И это то, что refs, указатель. .current разыменовывает указатель.

Это справедливо, я использовал React.createRef() в качестве вспомогательной функции, чтобы просто создать указатель, поскольку React не будет управлять им, если он не будет передан как реквизит ref , но вы можете так же, как хорошо создайте простой объект-указатель с той же структурой.

Мы могли бы изменить createRef , чтобы иметь ту же логику перегрузки, но из-за отсутствия аргумента это должно быть с возвратом условного типа. Если вы включите | null , он вернет MutableRefObject , если вы этого не сделаете, он будет (неизменяемым) RefObject .

Однако сработает ли это для тех, у кого не включена функция strictNullTypes ?

🤔

Должны ли мы действительно упростить написание неправильного кода для тех, у кого _do_ включено strictNullTypes ?

Когда я создал эту проблему, я не осознавал, что уже была эта перегрузка | null . Это не очень распространенный шаблон, поэтому я не ожидал, что что-то подобное существует. Я не знаю, как я пропустил это в документации, хотя это очень хорошо задокументировано и объяснено. Я согласен закрыть это как непроблему, если только вы не хотите объединить useRef и createRef .

Поскольку сама проблема была основана на useRef , а не createRef , я также согласен закрыть это, поскольку очень немногие люди напрямую изменяют значение разыменованного указателя в createRef .

Однако мне интересно, хорошо ли сработает этот шаблон перегрузки, глядя на документацию по хукам, мы видим несколько случаев использования useRef со значением по умолчанию, и в этом случае значение .current никогда не может быть null, но пользователей может раздражать постоянное использование ненулевого оператора утверждения, чтобы обойти его.

Было бы более разумно, если бы возвращаемое значение было изменяемым, если задано начальное значение, и неизменным, если оно опущено? Если задано начальное значение, то ссылка, вероятно, не будет передаваться компоненту через ref и использоваться больше как переменная экземпляра. С другой стороны, при создании ссылки исключительно для передачи компоненту начальное значение, вероятно, не будет предоставлено.

Примерно так я и думал. @Ковенский , что ты думаешь?

@ferdaber useRef в том виде, в каком он определен прямо сейчас, всегда будет изменяемым _и_ не обнуляемым, если вы явно не дадите ему общий аргумент, который не включает | null и начальное значение null .

Ссылки, которые передаются компоненту, должны быть инициализированы с помощью null , потому что это то, что React устанавливает для ссылок, когда они освобождаются (например, при размонтировании условно смонтированного компонента). useRef без передачи значения приведет к тому, что вместо этого начнется undefined , поэтому теперь вам также придется добавить | undefined к чему-то, что должно было заботиться только о | null .

Проблема в документации React и использовании useRef без начального значения заключается в том, что команда React, похоже, не особо заботится о разнице между null и undefined . Это может быть дело в Потоке.


В любом случае, то, как я определил useRef , точно соответствует варианту использования @bschlenk , за исключением того, что null является «опущенным» значением по причинам, которые я упомянул выше.

А, присмотревшись повнимательнее, это видно, и интересно, что он инициализируется как undefined в исходном коде React без параметра. Круто, так что все персиково, звучит как 👍

Я думаю, что это нормально, просто немного странно, что независимо от того, включаете ли вы | null , тип current по-прежнему может быть нулевым, просто изменилась изменчивость. Кроме того, документы React всегда явно передают значение null, поэтому допустимо ли вообще опускать начальное значение?

Как правило, это не так, и, по крайней мере, в прошлом, когда я указывал на это в какой-то части документации, команда React сказала, что «даже если это работает», это не должно быть опущено.

Вот почему первый аргумент createContext требуется, например, даже если вы хотите, чтобы контекст начинался с undefined . Вы действительно должны пройти undefined .

@ferdaber это потому, что им наплевать на типы. Каким должен быть тип current в useRef() (без аргументов)? undefined ? any ? Что бы это ни было, даже если вы дадите общий аргумент, он должен быть | ed с undefined . _И_ если это ссылка, которую вы используете в качестве ссылки на элемент реакции (а не только как локальное хранилище потока), тогда вам также необходимо | null , потому что React может записать туда null .

Теперь вы обнаружите, что current — это T | null | undefined , а не просто 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)

Да, этот DX работает для меня, и документация на уровне A++, так что я не думаю, что большинство людей запутаются. Спасибо за эту разработку! Честно говоря, вы должны просто оставить ссылку на эту проблему в typedoc :)

Возможно, стоит поддержать useRef<T>() и заставить его вести себя так же, как useRef<T | undefined>(undefined) ; но какую бы ссылку вы ни сделали с этим, все равно нельзя использовать в качестве ссылки на элемент, точно так же, как локальное хранилище потока.

Проблема в том... что произойдет, если вы _не_ дадите общий аргумент, что разрешено? TypeScript просто выведет {} . Правильный тип по умолчанию — unknown , но мы не можем его использовать.

Я получаю эту ошибку со следующим кодом:

~~~js
// ...
пусть intervalRef = useRef(нулевой); // также пробовал с const вместо let
// ...
использоватьЭффект( () => {
const interval = setInterval( () => { /* сделать что-нибудь */}, 1000);
intervalRef.current = интервал; // В этой строке я получаю ошибку

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

})
// ...
~И когда я удаляю readonly здесь, это работает:~ js
интерфейс RefObject{
ток только для чтения: T | нулевой;
}
~~~

Я новичок в использовании как reack hooks, так и typescript (просто пробую их вместе), поэтому мой код может быть неправильным.

По умолчанию, если вы создаете ссылку со значением по умолчанию null и указываете ее общий параметр, вы сигнализируете о своем намерении заставить React «владеть» ссылкой. Если вы хотите иметь возможность изменять ссылку, которой вы владеете, вы должны объявить ее следующим образом:

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

@ferdaber Спасибо за это!

Продвинувшись дальше и взглянув на возвращаемый тип current , возможно, он должен быть T , а не T | null ? С появлением хуков у нас _не всегда_ есть случай, когда все refs могут быть null , особенно в частом случае, когда useRef вызывается с ненулевым инициализатором.

Продолжая превосходный список примеров в https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment -446660394, если я напишу:

const numericRef = useRef<number>(42);

каким должен быть тип numericRef.current ? Нет необходимости в том, чтобы это было number | null .

Если бы мы определили типы и функции следующим образом:

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

это приведет к следующим использованиям и типам:

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

Что-то не так?

Что касается ответа на вопрос «какой тип непараметризованного useRef ?», ответ заключается в том, что этот вызов (несмотря на документацию) неверен, согласно команде React.

Я добавил перегрузку с |null _специфически_ в качестве удобной перегрузки для ссылок на DOM/компонент, потому что они всегда начинаются с нуля, они всегда сбрасываются до нуля при размонтировании, и вы никогда не переназначаете текущий самостоятельно, только React.

Readonly — это больше для защиты от логических ошибок, чем представление истинного JavaScript замороженного объекта / свойства только для получения.

Вы можете сделать это только случайно, когда вы оба даете общий аргумент, который говорит, что вы не принимаете значение null при инициализации значения в любом случае. Любой другой случай будет изменчивым.

Ах, да, теперь я вижу, что MutableRefObject<T> уже удалил регистр | null по сравнению с RefObject<T> . Так что случай useRef<number>(42) уже работает корректно. Спасибо за разъяснения!

Что нам нужно сделать с createRef ? Прямо сейчас он возвращает неизменяемый RefObject<T> , что вызывает проблемы в нашей кодовой базе, поскольку мы хотели бы передавать их и использовать так же, как (изменяемые) объекты ref useRef . Есть ли способ настроить тип createRef , чтобы он мог формировать изменяемые объекты ref?

(Атрибут ref определен как тип Ref<T> , который включает RefObject<T> , что делает все неизменяемым. Для нас это большая проблема: даже если мы получим изменяемый ref из useRef , мы не можем использовать тот факт, что он неизменен через вызов forwardRef .)

Хм... возможно, мы могли бы использовать тот же трюк, только потому, что он не имеет аргументов (и всегда начинается с нуля), ему нужен условный тип.

: null extends T ? MutableRefObject<T> : RefObject<T> должны делать и использовать ту же логику. Если вы говорите, что хотите поместить в него нули, он изменяем, если вы этого не сделаете, он по-прежнему неизменен в текущем значении.

Это очень хорошая идея. Поскольку createRef не принимает никаких параметров, по-видимому, всегда нужно включать параметры | null (в отличие от useRef ), так что, вероятно, нужно будет сказать MutableRefObject<T | null> ?

Я не смог заставить ни работать в TS. Вот сконфигурированная с ним игровая площадка TS:
https://tinyurl.com/y75c32y3

Тип всегда определяется как MutableRefObject .

Как вы думаете, что мы можем сделать с forwardRef ? Он объявляет ref как Ref<T> , что не включает возможность MutableRefObject .

(Наш вариант использования, вызывающий трудности, — это функция mergeRefs , которая принимает массив ссылок, которые могут быть либо функциональными ссылками, либо объектами ссылок, и создает одну комбинированную ссылку [функциональную ссылку], которую можно передать к компоненту. Эта объединенная ссылка затем доставляет любой входящий ссылочный элемент всем предоставленным ссылкам, либо вызывая их, если они функциональные ссылки, либо устанавливая .current , если они являются объектами ссылок. неизменяемый RefObject<T> и отсутствие включения MutableRefObject<T> in Ref<T> усложняют задачу. Должен ли я поднимать отдельный вопрос для forwardRef и Refтрудности?)

Мы не меняем тип для useRef по причинам, изложенным выше (хотя createRef все еще обсуждается). У вас есть какие-либо вопросы по обоснованию?

Должен ли я поднять отдельную проблему для элементов, описанных в https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment -457650501?

Да, давайте выделять.

Если вам нужен изменяемый объект ref, начинающийся с нулевого значения, не забудьте также указать | null для универсального аргумента. Это сделает его изменчивым, потому что вы «владеете» им, а не React.

Спасибо за это! Добавление null к общему useRef решило это для меня.

До

const ref = useRef<SomeType>(null) 

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

После

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

Был ли создан еще один комментарий о компонентах forwardRef? По сути, вы не можете перенаправить ссылку и напрямую изменить ее текущее значение, что, я думаю, является частью пересылки ссылки.

Я создал следующий хук:

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,
    );

И я получаю сообщение об ошибке: «Невозможно назначить «текущему», потому что это свойство доступно только для чтения».
Есть ли способ решить эту проблему, не меняя current на записываемый?

Для этого вам придется обманывать.

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

Да, это немного неправильно, но это единственный случай, который я могу придумать, когда такое назначение является преднамеренным, а не случайным — вы воспроизводите внутреннее поведение React и, следовательно, должны нарушать правила.

Мы могли бы изменить createRef , чтобы иметь ту же логику перегрузки, но из-за отсутствия аргумента это должно быть с возвратом условного типа. Если вы включите | null , он вернет MutableRefObject , если вы этого не сделаете, он будет (неизменяемым) RefObject .

большое спасибо

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

должно быть:

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

вы воспроизводите внутреннее поведение React

Означает ли это, что документы здесь устарели? Потому что они четко описывают это как предполагаемый рабочий процесс, а не внутреннее поведение.

Документы в этом случае ссылаются на ссылку, которой вы владеете, но для ссылок, которые вы передаете как атрибут ref элементу HTML, они должны быть доступны только для чтения _вам_.

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

Мой плохой, должен был прокрутить дальше. Получил ошибку о readonly для ссылки, которой я владел, и предположил, что это тот же случай. (Сейчас я использую другой рабочий процесс и, к сожалению, больше не могу воспроизвести ошибку...)

В любом случае большое спасибо за объяснение!

Мне неясно, перемещена ли часть темы createRef , но я также опубликую здесь.

Я использую популярную навигационную библиотеку для React Native (React Navigation). В своей документации он обычно вызывает createRef , а затем изменяет ref. Я уверен, что это потому, что React ими не управляет (нет DOM).

Должен ли тип для React Native быть другим?

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

@сильванаар
Я тоже столкнулся с этой проблемой при инициализации навигации, вы нашли решение?

Подводя итог тому, что я только что прочитал: единственный способ установить значение для ссылки, созданной createRef<T> , — это приводить ее каждый раз, когда вы ее используете? Что стоит за этим? В этом случае readonly практически полностью предотвращает установку значения для ссылки, тем самым сводя на нет всю цель функции.

TLDR: если ваше начальное значение равно null (детали: или что-то еще за пределами параметра типа), добавьте | null к параметру типа, и это должно сделать .current способным быть назначены как обычно.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги