2

I would like to do something like this:

//snippet 1
const logFooOnlyOnce = (foo: string) => console.log(foo) //"...only once during the lifetime of this component"

export const MyComponent = ({ foo }: { foo: string }): JSX.Element => {
  React.useEffect(() => {
    logFooOnlyOnce(foo)
  }, []) //<-- error
  return <div />
}

Ie. i want to look at a prop of a react component only once and only in a certain scope. This code works with plain JS as i would expect it.

But the eslint rule won't let me. It changes my code to do this:

//snippet 2
import React from 'react'

const logFooOnlyOnce = (foo: string) => console.log(foo) //"...only once during the lifetime of this component"

export const MyComponent = ({ foo }: { foo: string }): JSX.Element => {
  React.useEffect(() => {
    logFooOnlyOnce(foo)
  }, [foo])
  return <div />
}

However this is not what i want, if foo changes it's value, it will be logged more than once.

This seems to work, though:

//snippet 3
const logFooOnlyOnce = (foo: string) => console.log(foo) //"...only once during the lifetime of this component"

export const MyComponent = ({ foo }: { foo: string }): JSX.Element => {
  const fooRef = React.useRef(foo)
  React.useEffect(() => {
    logFooOnlyOnce(fooRef.current)
  }, [])
  return <div />
}

If snippet 3 is valid, can i ignore the empty dep list in snippet 1? I don't want to handle an edge case, i want to ignore it altogether.

Also, i notice that the rule can somewhat be cheated with:

//snippet 4
const logFooOnlyOnce = (foo: string) => console.log(foo) //"...only once during the lifetime of this component"
const myCallback = (foo: string) => () => {
  logFooOnlyOnce(foo)
}
export const MyComponent = ({ foo }: { foo: string }): JSX.Element => {
  React.useEffect(myCallback(foo), [])
  return <div />
}

As well as this:

//snippet 5
const logFooOnlyOnce = (foo: string) => console.log(foo) //"...only once during the lifetime of this component"
export const MyComponent = ({ foo }: { foo: string }): JSX.Element => {
  const myCallback = (foo: string) => () => logFooOnlyOnce(foo)
  React.useEffect(myCallback(foo), [])
  return <div />
}

Why is this rule complaining in this case, and what would be the cleanest way to achieve what i want? In order to apply an eslint-ignore directive, i need to have a very good case to argue with a higher power.

pailhead
  • 5,162
  • 2
  • 25
  • 46
  • 1
    In VSCode at least, the original code should be flagged as an error. If you hover over the red mark, it should tell you what rule applies, and there should be an option to ignore it. See also this answer: https://stackoverflow.com/a/29592334/2740650 – user2740650 Oct 11 '21 at 23:12
  • Ive added an explanation. I can't just add eslint-ignore directives without having a good reason for it. I first saw this rule a couple of days ago, and i'm trying to figure out if there is an argument against it in particular cases, and if i have a particular case. – pailhead Oct 11 '21 at 23:16
  • OK, at least now you know the rule is "react-hooks/exhaustive-deps". You might want to read this: https://stackoverflow.com/questions/58866796/understanding-the-react-hooks-exhaustive-deps-lint-rule Also https://typeofnan.dev/you-probably-shouldnt-ignore-react-hooks-exhaustive-deps-warnings/ – user2740650 Oct 11 '21 at 23:20
  • I've read through it but it didn't help :( "is because it's possible for onChange to change between renders" sure, thats fine, i want to use the first version before it changed, if it changed. – pailhead Oct 11 '21 at 23:21
  • `useMemo(foo)` and then pass that to `useEffect` as a dep. – morganney Oct 11 '21 at 23:27
  • 1
    It seems to me like you fully understand the rule and why it exists, but you want to do something unusual. That's exactly what the `ignore` is for. But if you strictly want to avoid `ignore`, you could store a boolean indicating that you've already logged it with https://reactjs.org/docs/hooks-reference.html#useref – user2740650 Oct 11 '21 at 23:30
  • But for all intents and purposes I can use a ref as well? – pailhead Oct 11 '21 at 23:31

2 Answers2

0

Just ignore the rule, using this comment

// eslint-disable-next-line react-hooks/exhaustive-deps

Ex.

export const MyComponent = ({ foo }: { foo: string }): JSX.Element => {
  React.useEffect(() => {
    logFooOnlyOnce(foo)
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])
  return <div />
}
smac89
  • 39,374
  • 15
  • 132
  • 179
  • Unfortunately, this may not be an option (higher power) unless i can argue exactly why this would be a worthy exception. – pailhead Oct 11 '21 at 23:14
  • 1
    @pailhead, Well if it is in your interest for the effect to only run once, then this is the only way to accomplish that. – smac89 Oct 11 '21 at 23:18
  • 1
    @pailhead you might want to leave an anonymous note for said "higher power", that eslint rules are not written in stone. When I write react, I never globally turn off such rules; I use them as a reminder to make sure what I am doing is correct, then I disable them when it is obvious that they are wrong. In this case, it is wrong for the rule to want you to add the `prop` to the dependency list, because that will only serve to achieve the opposite effect (no pun intended). _Treat the rules as recommendations, not law_ – smac89 Oct 11 '21 at 23:27
  • 1
    It’s not set in stone, but there has to be a valid enough reason. As is I was actually able to refactor this in a relatively painless fashion. So I think this one may not be it :) – pailhead Oct 12 '21 at 06:01
  • @pailhead, care to share how you did that? I'm kinda curious – smac89 Oct 12 '21 at 15:23
  • well i wasn't able to get the effect to run only once, but i was able to do it in a react sort of a way. Basically i memoized foo (outside of this component) using `useMemo` there i put an empty dep with sort of ensures that foo doesn't change. Got approved :D – pailhead Oct 13 '21 at 03:15
  • @pailhead That's interesting. How did you get `foo` to be outside the component when it was passed as a prop? – smac89 Oct 13 '21 at 03:18
  • I mean wherever it was declared. In case foo was a function, i was recreating it with every render, but ignoring it in the data flow, which seems to be a big react no no. But it worked for me for so long. – pailhead Oct 13 '21 at 03:19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/238080/discussion-between-smac89-and-pailhead). – smac89 Oct 13 '21 at 03:20
0

If you dislike ignoring the rule, you can also make a helper function that makes it clear what you want without having to put eslint-disables everywhere:

import { EffectCallback, useEffect } from 'react';

const useEffectOnce = (effect: EffectCallback) => {
    useEffect(effect, []);
};

(You can reference this but it's extremely short)

DemiPixel
  • 1,768
  • 11
  • 19
  • Thanks I really like this approach. At least it’s clear what I want to do from the naming convention. – pailhead Oct 12 '21 at 07:27