0

I have a component that's supposed to read a property from the component (which is either string "fill" or string "stroke") and pull the according key from an object to read it's context.

This gets mounted as soon as an object is selected, accesses the active object and pulls out it's color either as a fill color or a stroke color.

useEffect(() => {
    setButtonColor(context.objects.getActiveObject()[props.type]);
}, []); //on mount

Mounting it like this:

<ColorPicker type="fill" />
<ColorPicker type="stroke" />

This supposed to run only once on mount. I thought when the dep array is empty, it runs on any case once when it's mounted.

So how do I run something once on mount utilizing props and context?

And why does it need a dependency at all when I want it to ALWAYS run only ONCE on mount, no matter what?

user6329530
  • 596
  • 1
  • 7
  • 21
  • The effect you have already will indeed only run once per mount per component it is used in – Dan Feb 05 '20 at 13:08
  • *"I thought when the dep array is empty, it runs on any case once when it's mounted."* yes, that's correct. It should work as you intended, so it's not clear what the issue is. However, note that if the component is rerendered with a different prop value, the code is not run. This doesn't seem like the correct behavior. Hence why you should add the prop as dependency. Don't know about context tbh. – Felix Kling Feb 05 '20 at 13:09
  • Does this answer your question? [How to call loading function with React useEffect only once](https://stackoverflow.com/questions/53120972/how-to-call-loading-function-with-react-useeffect-only-once) – Muhammad Zeeshan Feb 05 '20 at 13:09
  • @MuhammadZeeshan the first answer is exactly what I did here and that makes it complain about missing dependencies. – user6329530 Feb 05 '20 at 13:11
  • @DanPantry yes, that's the desired effect. The component changes the color of the selected object with a color picker. When the object is deselected, the component gets unmounted. When selecting it again, I want to get the color from the object as it was set and initialize the color-button with it. Therefore pulling the color from object only should run exactly once on mount. – user6329530 Feb 05 '20 at 13:13
  • The complaint is a linter rule. It's complaining because your hook, `useEffect`, relies on the value of `context` and `props.type`. You can disable the rule using `//eslint-disable-next-line` if you're sure you really only want to run this effect once, but you should bear in mind that you will need to remount the component if you want the effect to run again in response to changes to `type`. – Dan Feb 05 '20 at 13:14
  • @DanPantry I rather not want to hide warnings because I expect them to come up for a reason. However here, [] should normally mean "run only once". Therefore I wonder why there is a dependency at all because normally with stating empty deps array [] you'd want to run it **always but only once on mount**. – user6329530 Feb 05 '20 at 13:19
  • The warning is showing up because your effect *depends* on the value of `props.type` and `context`. The rule is essentially saying that it thinks your dependency array is incomplete. Your only options to remove the warning is to either disable it *or* add context & props.type to the dependency array (which will cause the effect to run in response to changes to those values). – Dan Feb 05 '20 at 13:20
  • @DanPantry that's the issue here. How would I normally, without hiding warnings get a value from props and/or context but only when the component was mounted and never again? Not when props changes, not when context changes, the only condition that matters here is, that the component was mounted. In object oriented I'd use componentDidMount but with hooks? – user6329530 Feb 05 '20 at 13:21
  • I could like use mounted = useRef(false) in the component, encapsulate getting the color in if(!mounted.current) and set that to true at the end of the effect hook but that seems awkward to me. Is there no other way? Edit: And with that I get another warning, that I call a function here and it could run into a loop... – user6329530 Feb 05 '20 at 13:29
  • Ok found a workaround. If I pull the color from the object in an useRef variable. – user6329530 Feb 05 '20 at 13:35

2 Answers2

1

It's best to move away from the thinking that effects run at certain points in the lifecycle of a component. While that is true, a model that might help you better get to grips with hooks is that the dependency array is a list of things that the effect synchronizes with: That is, the effect should be run each time those things change.

When you get a linter error indicating your dependency array is missing props, what the linter is trying to tell you is that your effect (or callback, or memoization function) rely on values that are not stable. It does this because more often than not, this is a mistake. Consider the following:

function C({ onSignedOut }) {
  const onSubmit = React.useCallback(() => {
    const response = await fetch('/api/session', { method: 'DELETE' })
    if (response.ok) {
      onSignedOut()
    }
  }, [])

  return <form onSubmit={onSubmit}>
    <button type="submit">Sign Out</button>
  </form>
}

The linter will issue a warning for the dependency array in onSubmit because onSubmit depends on the value of onSignedOut. If you were to leave this code as-is, then onSubmit will only be created once with the first value of onSignedOut. If the onSignedOut prop changes, onSubmit won't reflect this change, and you'll end up with a stale reference to onSignedOut. This is best demonstrated here:

import { render } from "@testing-library/react"

it("should respond to onSignedOut changes correctly", () => {
  const onSignedOut1 = () => console.log("Hello, 1!")
  const onSignedOut2 = () => console.log("Hello, 2!")
  const { getByText, rerender } = render(<C onSignedOut={onSignedOut1} />)
  getByText("Sign Out").click()
  // stdout: Hello, 1!
  rerender(<C onSignedOut={onSignedOut2} />)
  getByText("Sign Out").click()
  // stdout: Hello, 1!
})

The console.log() statement does not update. For this specific example that would probably violate your expectations as a consumer of the component.


Let's take a look at your code now.

As you can see, this warning is essentially stating that your code might not be doing what you think it is doing. The easiest way to dismiss the warning if you're sure you know what you're doing is to disable the warning for that specific line.

useEffect(() => {
    setButtonColor(context.objects.getActiveObject()[props.type]);
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); //on mount

The correct way to do this would be to place your dependencies inside of the array.

const { type } = props
useEffect(() => {
    setButtonColor(context.objects.getActiveObject()[type]);
}, [context, type]);

This would, however, change the button colour every time type changed. There's something to note here: You're setting state in response to props changing. That's called derived state.

You only want that state to be set on the initial mount. Since you only want to set this on the initial mount, you could simply pass your value to React.useState(initialState), which would accomplish exactly what you want:

function C({ type }) {
  const initialColor = context.objects.getActiveObject()[type];
  const [color, setButtonColor] = React.useState(initialColor);
  ...
}

This still leaves the problem that the consumer might be confused as to why the view does update when you change the props. The convention that was common before functional components took off (and one I still use) is to prefix props that are not monitored for changes with the word initial:

function C({ initialType }) {
  const initialColor = context.objects.getActiveObject()[initialType];
  const [color, setButtonColor] = React.useState(initialColor);
}

You should still be careful here, though: It does mean that, for the lifetime of C, it will only ever read from context or initialType once. What if the value of the context changes? You might end up with stale data inside of <C />. That might be acceptable to you, but it's worth calling out.


React.useRef() is indeed a good solution to stabilize values by only capturing the initial version of it, but it's not necessary for this use-case.

Dan
  • 10,282
  • 2
  • 37
  • 64
  • Related: https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount/ – Dan Feb 05 '20 at 14:50
0

This is my workaround for the issue:

Set the color to a variable and then use that variable to set the button color on mount of the component.

const oldColor = useRef(context.objects.getActiveObject()[props.type]);

useEffect(() => {
    setButtonColor(oldColor.current);
}, []); //on mount

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

user6329530
  • 596
  • 1
  • 7
  • 21