Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

No you're right, I completely agree with you. It's also very telling that there's also this very long article about useEffect dependencies: https://react.dev/learn/removing-effect-dependencies

useEffect and dependencies are the worst part of React at the moment.

The reason is, you do need to use effects, everywhere in your code, constantly. And dependencies, and the linter rule that goes with them, are nasty ergonomics, and hard to reason about.

The article I linked does explain it all quite well, and most devs I work with do understand the dependencies rules, but the rule still needs to be disabled often. Why?

Often the lint errors really are just wrong. Just like ChatGPT is often simply just wrong, no ifs or buts. For example: you have a callback function declared as an expression, that the useEffect hook calls. The linter will say "oh this is an expression, therefore it needs to be declared as a dependency". But it doesn't, because looking at the code as a human, you can immediately see the function never changes.

If you follow the linter, you will inevitably end up going down a destructive and unnecessary refactoring. You'll end up with unnecessary `useCallbacks` sprinkled everywhere that hurt readability for nothing. Often, you'll have to refactor the dependencies out of the component entirely, splitting one cohesive component into two, just to satisfy the linter out of an abundance of caution.

Junior devs will be confused. Senior devs will be frustrated. Every react codebase I've worked on has gagged this linter warning.

So how should this work?

It shouldn't be a a linter error in the first place. Linters should be used to enforce coding standards, not fix application bugs. I feel very strongly on this.



"For example: you have a callback function declared as an expression, that the useEffect hook calls. The linter will say "oh this is an expression, therefore it needs to be declared as a dependency". But it doesn't, because looking at the code as a human, you can immediately see the function never changes."

Can you elaborate on this? In my experience this isn't the case. If a function is declared outside of the component scope then it won't trigger a lint error. If it's defined within the component scope and not wrapped in useCallback then it does in fact change on every render and the lint error is being raised for good reason.


> If it's defined within the component scope and not wrapped in useCallback then it does in fact change on every render and the lint error is being raised for good reason.

So I just did some playing around to investigate this further, and it seems to be more subtle than I thought.

If you have a function expression but the expression is not dependent on any outer scoped variables, the linter does not complain that you didn't specify the function itself as a dependency in useEffect. Example:

```

  let bar = "bar";

  const setResult = (result: string) => {
    // Won't trigger "exhaustive-deps"
    setBio(result);
    // Will trigger "exhaustive-deps"
    if (bar === "1") setBio(result);
  };

  useEffect(() => {
    setResult("");
  }, [person]);
```

So there's some quite sophisticated analysis of the AST going on in the lint rule.

I still think that this rule is too complicated for a linter. (Bugs like inter-dependencies in your data and code should be caught by unit tests).

But it's technically pretty impressive.

Edit: I did some more investigations.

If I make a function that depends on something very unpure (like checking window.location), the linter fires if the unpure function is in the outer scope of the hook, but not if it isn't. Even though the dependency is the same. So the linter rule can only identify locally scoped dependencies I think.


I recommend you read the article again. Just because window.location changes outside scope doesn't mean that React would update the component when window.location changes just because you put it in a dependency array.

This is because props or state didn't change and the parent didn't rerender.

UseEffect dependencies aren't observables. You'd need to wire up an event listener on unload, hashChange, etc

https://stackoverflow.com/questions/58442168/why-useeffect-d...


> Bugs like inter-dependencies in your data and code should be caught by unit tests

Why write a ton of unit tests for what a static check can just do “for free?”. Not sure i follow your logic. I agree React can feel like a nuisance, but the lint rule itself is not the issue i think you have, it sounds like your issue is just with hooks.


Its not necessarily that the function depends on something 'very unpure', its if it depends on anything which is non-primitive that is created or assigned within the component fn body.

window.location returns a Location object which is not a primitive, and so it will trigger exhaustive deps if it is being used by way of a reference inside the component render function (assigned to a variable), since it could theoretically be a different object reference on subsequent renders.

Since the linter can't necessarily tell if the reference has changed out from under it on subsequent renders due to the fact that it could be mutably changed, it marks it as a necessary dep.


This seems obvious to me? Your lambda captures an outside object that will change with each render pass?


There is a new experimental hook, useEffectEvent, whose only discernable functionality based on the documentation and information available is to quiet the linter. I'm not sure that's all it does since I haven't dug into the React source code, but that's what many are speculating.


[Edit: this is all wrong, see comment tree]

It makes it so that the same instance of a function is used for all renders, but that it always points to the data closed over by the latest render; it eliminates a whole class of scenarios that normally lead to gnarly dependency array tomfoolery with useCallback, useMemo and useEffect.

It's quite a common hook to try to invent in user-land (i've seen it in numerous projects), it's not just a linter silencer.


> but that it always points to the data closed over by the latest render

It's my understanding that this was to be the behavior of the canceled useEvent hooks but that the new useEffectEvent hook does not return a stable reference.

Do you have sources for the behavior of the new hook?


https://react.dev/learn/separating-events-from-effects#decla...

> Here, onConnected is called an Effect Event. It’s a part of your Effect logic, but it behaves a lot more like an event handler. The logic inside it is not reactive, and it always “sees” the latest values of your props and state.


A bare closure would always see the latest values because it closes over them during render. And the latest useEffect closure will have closed over that method..

Here is the test suite for useEffectEvent and the test ensuring it does not provide a stable reference: https://github.com/facebook/react/blob/main/packages/react-r...


You're right, i'll redact/edit. Hm, this doc page is exceptionally confusing.

Edit:

So it's kind of interesting, this test (and the functionality behind it) exists specifically to make sure you can't rely on it having a stable identity -- and therefore have to omit it from the dependency array. But it otherwise behaves the same as the popular user-land implementation involving refs (which you still have to include in the dependency array, because it works by providing a stable identity).

So there's some internal trickery in React to make sure that even though the effect event is recreated and has a new identity on every render, any time it's invoked you're accessing the latest inner callback.


As far as I can tell, useEffectEvent is just the useEvent hook but renamed to reduce scope. useEvent was originally going to provide performance optimizations when creating event handlers in a function component and it was going to solve the issue of event handlers causing effects to re-fire[1].

Now it's just going to focus on the effect issue.

[1] https://github.com/reactjs/rfcs/blob/useevent/text/0000-usee...


The funny thing is that React already kind of has its own programming language: JSX. If they wanted to, they could add language features to support React. I expect that would be very controversial though.


Svelte is pretty pleasant, compared to React. It's fundamentally a JS-to-JS compiler that adds reactive assignments, and ridiculously much more usable than useEffectHopeThisWasTheRightThing.

    let count = 1;
    
    // this is reactive
    $: doubled = count * 2;
    
    function handleClick() {
      count += 1;
    }




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: