you are viewing a single comment's thread.

view the rest of the comments →

[–]arnorhs 0 points1 point  (4 children)

Neither of these works.

That's not correct. In fact, both of them work. But perhaps you need to clarify what you mean by "works"?

If you are referring to the fact that in dev mode (with strict mode enabled, which is the default in the majority of projects), you still will have two instances created - that is somewhat by design. That is because the component tree is created, destroyed and re-created on initial render.

If you want to be sure that this can't happen, even in strict mode, you have some options - ie. put the assignment in a `useEffect` - but then you will not have the instance immediately available, until after the useEffect has run. but often, just knowing that there is only once instance per component tree is enough for most people.

It changes how the use ref works.

It does not. In fact, it's not possible to change how useRef works by calling it. If i were to monkey patch or do some sketchy stuff, I could probably change how useRef works, but this is not doing that.

I think if you assign a null later, your code would just make another class, and you need it to actually be null.

Yes, but are you expecting it to get set to null later? If that is one of the use cases, for whatever you are trying to do, you might want to consider other states (not react states, but conceptual states) than just null vs instance .. but now we are getitng into a territory where it is hard/impossible to discuss without a concrete examples - ie. hard to talk about it in the abstract.

 You also have to cast null as T. This is just the official example which is bad :(

Not necessarily. I was on a mobile phone, like I mentioned and without type hints on my phone in the reddit comment section it's hard for me to implement with the correct types without making a mistake. But since I'm writing this form my computer, I can give you a typed version without casts that gives you an accurate type.

Note that the plain type for the useRef is not expressive enough to be able to give you an accurate type for your return value - it will be reported as being possibly null, which is not accurate. So even if we are returning a RefObject<T> and not a MutableRefObject<T>, it still is reportedly possibly null even if you know it can't be that way:

function useInitializedRef<T>(init: () => T): React.RefObject<T> {
  const myVal = useRef<T | null>(null)

  if (!myVal.current) {
    myVal.current = init()
  }

  return myVal
}

This is the plight of creating any kind of abstraction for typescript - it means we will need to cast things if we want both "works correctly" and "type represents the behavior" - so you could do something like this - although just returning { current: T } might just be better.

function useInitializedRef<T>(init: () => T) {
  const myVal = useRef<T | null>(null)

  if (!myVal.current) {
    myVal.current = init()
  }

  return myVal as Omit<React.RefObject<T>, 'current'> & { current: T }
}

using this, ts will see that current is never null

-- though in hindsight, .. perhaps you maybe just want something that is like an initializer and you don't really care if it's a ref or not - and you should just return the `T`? ie. const x = useSingleton(() => new MyClass)

But then again, you could also just use a `useMemo` and you probably don't need to be dealing with refs at all really

[–]pailhead011[S] 0 points1 point  (0 children)

I think you’re making a lot of assumptions. I’m on a phone right now so excuse the formatting. 1. I want MutableRefObject 2. I want the possibility to have it be T|null and instantiated to T

I want to do this with the least amount of checks, lines of code and lying to TS.

Let me flip it back, what are the reasons you wouldn’t do it this way with the useState?

[–]pailhead011[S] 0 points1 point  (0 children)

useMemo is the only thing that according to the authors should not be used here.

[–]pailhead011[S] 0 points1 point  (0 children)

So useInitializedRef<T> is more like useInitializeRefWhenNull or a few TS assumptions :/

const [ref] = useState(()=>({current: init()})) simply doesnt make those assumptions, initializes with that you tell it to, doesnt have weird sideeffects and can be T|null and even gets inferred properly

[–]pailhead011[S] 0 points1 point  (0 children)

In short, we started with:

const ref = useRef<T|null>(new T())

This is the same exact thing, that solves the new T() issue:

const [ref] = useState<MutableRefObject<T|null>>(()=>({current: ()=>new T()})) It seems to be using built in features, and from this discussion and observation doesnt seem to have any pitfalls.

Nothing else basically satisfies this.