6

I'm trying to use the useEffect hook inside a controlled form component to inform the parent component whenever the form content is changed by user and return the DTO of the form content. Here is my current attempt

const useFormInput = initialValue => {
  const [value, setValue] = useState(initialValue)

  const onChange = ({target}) => {
    console.log("onChange")
    setValue(target.value)
  }

  return { value, setValue, binding: { value, onChange }}
}
useFormInput.propTypes = {
  initialValue: PropTypes.any
}

const DummyForm = ({dummy, onChange}) => {

  const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
  const {value: bar, binding: barBinding} = useFormInput(dummy.value)

  // This should run only after the initial render when user edits inputs
  useEffect(() => {
    console.log("onChange callback")
    onChange({foo, bar})
  }, [foo, bar])

  return (
    <div>
      <input type="text" {...fooBinding} />
      <div>{foo}</div>

      <input type="text" {...barBinding} />
      <div>{bar}</div>
    </div>
  )
}


function App() {
  return (
    <div className="App">
      <header className="App-header">
        <DummyForm dummy={{value: "Initial"}} onChange={(dummy) => console.log(dummy)} />
      </header>
    </div>
  );
}

However, now the effect is ran on the first render, when the initial values are set during mount. How do I avoid that?

Here are the current logs of loading the page and subsequently editing both fields. I also wonder why I get that warning of missing dependency.

onChange callback
App.js:136 {foo: "Initial", bar: "Initial"}
backend.js:1 ./src/App.js
  Line 118:  React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback  react-hooks/exhaustive-deps
r @ backend.js:1
printWarnings @ webpackHotDevClient.js:120
handleWarnings @ webpackHotDevClient.js:125
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:190
push../node_modules/sockjs-client/lib/event/eventtarget.js.EventTarget.dispatchEvent @ eventtarget.js:56
(anonymous) @ main.js:282
push../node_modules/sockjs-client/lib/main.js.SockJS._transportMessage @ main.js:280
push../node_modules/sockjs-client/lib/event/emitter.js.EventEmitter.emit @ emitter.js:53
WebSocketTransport.ws.onmessage @ websocket.js:36
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial"}
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial2"}
Tuomas Toivonen
  • 21,690
  • 47
  • 129
  • 225

5 Answers5

9

You can see this answer for an approach of how to ignore the initial render. This approach uses useRef to keep track of the first render.

  const firstUpdate = useRef(true);
  useLayoutEffect(() => {
    if (firstUpdate.current) {
      firstUpdate.current = false;
    } else {
     // do things after first render
    }
  });

As for the warning you were getting:

React Hook useEffect has a missing dependency: 'onChange'

The trailing array in a hook invocation (useEffect(() => {}, [foo]) list the dependencies of the hook. This means if you are using a variable within the scope of the hook that can change based on changes to the component (say a property of the component) it needs to be listed there.

lovelikelando
  • 7,593
  • 6
  • 32
  • 50
5

If you are looking for something like componentDidUpdate() without going through componentDidMount(), you can write a hook like:

export const useComponentDidMount = () => {
  const ref = useRef();
  useEffect(() => {
    ref.current = true;
  }, []);
  return ref.current;
};

In your component you can use it like:

const isComponentMounted = useComponentDidMount();

useEffect(() => {
 if(isComponentMounted) {
  // Do something
 }
}, [someValue])

In your case it will be:

const DummyForm = ({dummy, onChange}) => {
  const isComponentMounted = useComponentDidMount();
  const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
  const {value: bar, binding: barBinding} = useFormInput(dummy.value)

  // This should run only after the initial render when user edits inputs
  useEffect(() => {
    if(isComponentMounted) {
      console.log("onChange callback")
      onChange({foo, bar})
    }
  }, [foo, bar])

  return (
    // code
  )
}

Let me know if it helps.

Praneeth Paruchuri
  • 1,012
  • 3
  • 12
2

I create a simple hook for this

https://stackblitz.com/edit/react-skip-first-render?file=index.js

It is based on paruchuri-p

const useSkipFirstRender = (fn, args) => {
  const isMounted = useRef(false);

  useEffect(() => {
    if (isMounted.current) {
      console.log('running')
      return fn();
    }
  }, args)

  useEffect(() => {
    isMounted.current = true
  }, [])
}

The first effect is the main one as if you were using it in your component. It will run, discover that isMounted isn't true and will just skip doing anything.

Then after the bottom useEffect is run, it will change the isMounted to true - thus when the component is forced into a re-render. It will allow the first useEffect to render normally.

It just makes a nice self-encapsulated re-usable hook. Obviously you can change the name, it's up to you.

Callum Linington
  • 14,213
  • 12
  • 75
  • 154
1

You can use custom hook to run use effect after mount.

const useEffectAfterMount = (cb, dependencies) => {
  const mounted = useRef(true);

  useEffect(() => {
    if (!mounted.current) {
      return cb();
    }
    mounted.current = false;
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

Here is the typescript version:

const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
  const mounted = useRef(true);

  useEffect(() => {
    if (!mounted.current) {
      return cb();
    }
    mounted.current = false;
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

Example:

useEffectAfterMount(() => {
  console.log("onChange callback")
  onChange({foo, bar})
}, [count])
Foyez
  • 334
  • 3
  • 9
0

I don't understand why you need a useEffect here in the first place. Your form inputs should almost certainly be controlled input components where the current value of the form is provided as a prop and the form simply provides an onChange handler. The current values of the form should be stored in <App>, otherwise how ever will you get access to the value of the form from somewhere else in your application?

const DummyForm = ({valueOne, updateOne, valueTwo, updateTwo}) => {
  return (
    <div>
      <input type="text" value={valueOne} onChange={updateOne} />
      <div>{valueOne}</div>

      <input type="text" value={valueTwo} onChange={updateTwo} />
      <div>{valueTwo}</div>
    </div>
  )
}


function App() {
  const [inputOne, setInputOne] = useState("");
  const [inputTwo, setInputTwo] = useState("");

  return (
    <div className="App">
      <header className="App-header">
        <DummyForm
          valueOne={inputOne}
          updateOne={(e) => {
            setInputOne(e.target.value);
          }}
          valueTwo={inputTwo}
          updateTwo={(e) => {
            setInputTwo(e.target.value);
          }}
        />
      </header>
    </div>
  );
}

Much cleaner, simpler, flexible, utilizes standard React patterns, and no useEffect required.

jered
  • 11,220
  • 2
  • 23
  • 34
  • 1
    Consider the case where I need to iterate array of entities and render a form for each one. Parent component should keep track of which entities have changed and access their current state for batch update. I also want to utilize the generic form input hook for reduced boilerplate. – Tuomas Toivonen Sep 09 '19 at 19:16