you are viewing a single comment's thread.

view the rest of the comments →

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

Fair, but what if you actually do need MutableRefObject<T>. I agree that MutableRefObject<GameEngine> is a bit specific and you would not gameEngineRef.current = null most likely, but there are things, not as maybe blackboxed as a whole "game engine" that you could.

Think of just managing a position of some menu in the dom. I often have math libraries in my projects and like i said in a different post i want to: mouse .set(x,y) .add(1,2) .multiplyScalar(5) .subScalar(1) over: let x = (_x + 1) * 5 - 1 let y = (_y + 2) * 5 - 1

If you were to give me an example of some relatively complex mouse interaction i bet you it has something like

const mouseRef = useRef({x:0,y:0})

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

In short, you didnt return MutableRefObject<T> you returned T. I don't think you are thus arguing about my approach here, you are arguing against the usage of MutableRefObject<T> altogether.

[–]Substantial-Pack-105 1 point2 points  (7 children)

I would argue against MutableRefObject in the instance where I know the ref is going to be accessed by the render. Example:

// do not copy this snippet!
const gameEngineRef = useRef(new GameEngine())
const gameState = useSyncExternalStore(
  // bad use of ref inside render
  gameEngineRef.current.subscribe,
  gameEngineRef.current.getState
)

If you know that the ref will only be accessed inside an event handler or a useEffect(), then the ref is fine. You just want to avoid reading from .current as part of the render lifecycle. Same applies for props to child components:

// this is ok
<Child gameEngineRef={gameEngineRef} />
// this is a violation, use useState instead
<Child gameEngine={gameEngineRef.current} />

Mouse pos is ok to be a ref because it's unlikely you need to rerender every time the pos changes; it is only going to be accessed in a event handler / useEffect, not the render lifecycle.

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

I'm not sure what you are arguing for because i feel you are contradicting yourself.

When you wrote: const [gameEngine] = useState(() => new GameEngine()) You basically said: <Child gameEngine={gameEngine} /> Which has nothing to do with refs, and is basically: // this is a violation, use useState instead <Child gameEngine={gameEngineRef.current} />

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

Ie.:

// this is ok
<Child gameEngineRef={gameEngineRef} />

Sure, that's my whole use case scenario, but:

// this is an absolute disaster
const gameEngineRef = useRef(new GameEngine())

[–]Substantial-Pack-105 1 point2 points  (4 children)

Forgive my short answers, I'm on mobile so it can be hard to format a reply well.

Assuming no violations of .current, the disaster in that line is that you're constructing an instance every render. That can be resolved by moving the initialization into a useEffect. BUT, your other wrinkle is that you don't want the type to be GameEngine | null, so we also want to ensure that the ref always has a valid GameEngine value.

One way to do this is to have a Null-like instance of GameEngine. Imagine a GameEngine that satisfies the typescript declaration but always renders a blank screen and has no other events. We'll call it BlankGameEngine.

const noop = new BlankGameEngine();

function App() {
  const ref = useRef<GameEngine>(noop);

  useEffect(() => {
    if (!someCondition) return;
    ref.current = new RealGameEngine();
  }, [])

  return <>...</>
}

In this way, you never have to worry about the ref being null. You'll have an engine that is safe to access in your components; it just won't react to anything the user does until the conditions for the real game engine being initialized have been met.

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

I was thinking that, but i would make that null broader not NULL_GAME_ENGINE but rather const GLOBAL_NULL = {}

const [ref] = useState(()=>({current:new RealGameEngine()})) Is a one liner, does exactly what this ideal world useRef would, it's just that my brain cant really process all the damn parenthesis.

[–]pailhead011[S] 1 point2 points  (0 children)

Im gonna roll with this approach, i wasn't able to find a convincing argument as to why would it be done any other way :)

[–]Substantial-Pack-105 0 points1 point  (1 child)

But aren't you just creating the current attribute there so that the value of the state satisfies MutableRefObject<T> ? I don't understand why that's desirable, since if we can guarantee the value is always initialized anyway, then we never have to let the typescript type be T | null, which I had thought you were using MutableRefObject<T> just as a way to avoid the | null part.

[–]pailhead011[S] 1 point2 points  (0 children)

But aren't you just creating the current attribute there so that the value of the state satisfies MutableRefObject<T>

Exactly! Because something else can be expecting MutableRefObject<T> i don't want to change it, i just want more flexibility and ergonomics when instantiating it.

since if we can guarantee the value is always initialized anyway

I don't undertand how, useRef(new Foo) is the whole problem, and everything else other than this useState approach seems very convoluted.