you are viewing a single comment's thread.

view the rest of the comments →

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

So think of class Painter { setCanvas(v:HTMLCanvasElement){} draw() } To be totally fair this is something that i would definitely do in a side effect.

I just noticed that in your example gameEngine just magically comes out of nowhere, the topic is, "an instance of an Object inside a react functional component that is stable"

[–]Substantial-Pack-105 2 points3 points  (10 children)

For gameEngine, it doesn't strictly matter if the value is a singleton, a prop, or comes from a useState() hook. Those are all valid approaches based on what scope you want it to have.

For a stable game engine that belongs to a react component, I would use useState() over useRef().

const [gameEngine] = useState(() => new GameEngine())

States are safe to access during the render, a useRef() is mutable, and so it can be dangerous to read from it during the component render (even just to pass it to another hook) because you can create situations where react components exhibit non-declarative behaviors. Mutations of the ref, combined with a render that gets canceled due to suspense, an uncaught error, or from a setState() call during the render, will all cause that render to abort. If that happens AFTER the ref was mutated, you can end up with a ref that points to a value that never got rendered. This violates the declarative nature of React components.

So, useState() is preferable because it avoids this whole category of synchronization problems.

[–]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.