8

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"} 
0

5 Answers 5

10

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.

Sign up to request clarification or add additional context in comments.

Comments

6

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.

Comments

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.

3 Comments

I feel sorry for using ref for such a simple task. I hope React team will tweak the hook API to consider this issue which I believe is very common.
They did write it on their docs saying that is a very rare case and hence not implemented yet.
yeah it is an interesting case, I can't say I've needed it once I got my head round hooks - in the very first instance I needed it for sure
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]) 

Comments

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.

1 Comment

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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.