[Problem]
I have a HTML Input element to focus on inside my bottomsheet which is hidden by default. I would like to focus on it when bottomsheet is shown, but I am keep missing it.
[What I've tried]
I already tried autoFocus={true} but it didn't work.
I tried the following, still not working.
const bottomSheetPage = (props) => {
const [bottomSheetOn, setBottomSheetOn] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
~~~ some codes ~~~
useEffect ( () => {
if(props.autoFocus) {
inputRef?.current?.focus()
}
}, [isBottomsheetOn])
~~~ some codes ~~~
<input ref={inputRef}/>
bottomSheetOn is state that controls the toggle of bottomsheet and checked that prop.autoFocus === true.
How can I focus on the element inside bottomsheet when it's shown?
This could have one of two reasons:
props.autoFocus is false
props.autoFocus is true, but useEffect is only called when isBottomsheetOn changes
Try adding props.autoFocus to the list of useEffect dependencies and console.log/debugger inside useEffect to make sure it is called correctly.
useEffect ( () => {
if(props.autoFocus) {
inputRef?.current?.focus()
}
}, [isBottomsheetOn, props.autoFocus]) // will be triggered on props.autoFocus change
If that doesn't help, try to set the focus manually to make sure it's not a problem with the input ref.
import { useState, useEffect, useRef } from 'react'
export default function Component(props) {
const inputRef = useRef(null)
const [on, setOn] = useState(true)
useEffect ( () => {
if (on) {
inputRef?.current?.focus()
}
}, [on])
return (
<>
<input ref={inputRef}/>
<button onClick={() => setOn(prev => !prev)}>Focus</button>
</>
)
}
I found several ways to fix this up, and there was two methods I've actually tried. Either with different advantages.
using IntersectionObserver and setTimeout
you can check if one element intesects other by IntersectionObserver.observe() so I made a recrurring function to check intersection, and then set focus when it's intersecting. Codes is as follows.
const [ticker, setTicker] = useState(true)
const [isIntersecting, setIntersecting] = useState(false)
const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting))
useEffect(() => {
if(props.autoFocus) {
if(inputRef?.current) {
if (isIntersecting) {
inputRef.current.focus()
} else {
setTimerout(() => setTicker(!ticker), 500)
}
}
}
return () => {observer.disconnect()}
}, [ticker])
But this method was focusing on element only once. I needed to focus on it everytime it's shown.
using setTimeout
I figured out that problem was there's time needed for rendering toggled bottomsheet, so I simply gave timeout for focus. And it worked out.
useEffect(() => {
if (focusRef?.current) {
setTimeout(setFocus, 1000)
}
})
const setFocus = () => {
if (focusRef?.current){
focusReft.focus()
}
}
According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
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
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);
I try to implement scroll indicator in component using custom hook.
Here's the component:
...
const DetailListInfo: React.FC<Props> = props => {
const container = useRef(null)
const scrollable = useScroll(container.current)
const { details } = props
return (
<div
ref={container}
className="content-list-info content-list-info-detailed"
>
{details && renderTypeDetails(details)}
{scrollable && <ScrollIndicator />}
</div>
)
}
export default inject("store")(observer(DetailListInfo))
And the useScroll hook:
import React, { useState, useEffect } from "react"
import { checkIfScrollable } from "../utils/scrollableElement"
export const useScroll = (container: HTMLElement) => {
const [isScrollNeeded, setScrollValue] = useState(true)
const [isScrollable, setScrollable] = useState(false)
const checkScrollPosition = (): void => {
const scrollDiv = container
const result =
scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
scrollDiv.scrollTop === 0
setScrollValue(result)
}
useEffect(() => {
console.log("Hook called")
if (!container) return null
container.addEventListener("scroll", checkScrollPosition)
setScrollable(checkIfScrollable(container))
return () => container.removeEventListener("scroll", checkScrollPosition)
}, [isScrollable, isScrollNeeded])
return isScrollNeeded && isScrollable
}
So on every scroll in this passed component (containers are different, that's why I want to make customizable hook) I want to check for current scroll position to conditionally show or hide the indicator. The problem is, that hook is called only once, when component is rendered. It's not listening on scroll events.
When this hooks was inside the component, it was working fine. What is wrong here?
Let's research your code:
const container = useRef(null)
const scrollable = useScroll(container.current) // initial container.current is null
// useScroll
const useScroll = (container: HTMLElement) => {
// container === null at the first render
...
// useEffect depends only from isScrollable, isScrollNeeded
// these variables are changed inside the scroll listener and this hook body
// but at the first render the container is null so the scroll subscription is not initiated
// and hook body won't be executed fully because there's return statement
useEffect(() => {
if (!container) return null
...
}, [isScrollable, isScrollNeeded])
}
To make everything work correctly, your useEffect hooks should have all dependencies used inside the hook body. Pay attention to the warning notes in the documentation.
Also you can't pass only ref.current into a hook. This field is mutable and your hook will not be notified (re-executed) when ref.current is changed (when it's mounted). You should pass whole ref object to be able to get HTML Element by ref.current inside the useEffect.
The correct version of this function should look like:
export const useScroll = (ref: React.RefObject<HTMLElement>) => {
const [isScrollNeeded, setScrollValue] = useState(true);
const [isScrollable, setScrollable] = useState(false);
useEffect(() => {
const container = ref.current;
if (!container) return;
const checkScrollPosition = (): void => {
const scrollDiv = container;
const result =
scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
scrollDiv.scrollTop === 0;
setScrollValue(result);
setScrollable(checkIfScrollable(scrollDiv));
};
container.addEventListener("scroll", checkScrollPosition);
setScrollable(checkIfScrollable(container));
return () => container.removeEventListener("scroll", checkScrollPosition);
// this is not the best place to depend on isScrollNeeded or isScrollable
// because every time on these variables are changed scroll subscription will be reinitialized
// it seems that it is better to do all calculations inside the scroll handler
}, [ref]);
return isScrollNeeded && isScrollable
}
// somewhere in a render:
const ref = useRef(null);
const isScrollable = useScroll(ref);
Hook that has a scroll listener inside:
export const ScrollIndicator: React.FC<Props> = props => {
const { container } = props
const [isScrollNeeded, setScrollValue] = useState(true)
const [isScrollable, setScrollable] = useState(false)
const handleScroll = (): void => {
const scrollDiv = container
const result =
scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
scrollDiv.scrollTop === 0
setScrollValue(result)
}
useEffect(() => {
setScrollable(checkIfScrollable(container))
container.addEventListener("scroll", handleScroll)
return () => container.removeEventListener("scroll", handleScroll)
}, [container, handleScroll])
return isScrollable && isScrollNeeded && <Indicator />
}
In the render component there's need to check if the ref container exists. This will call hook only if container is already in DOM.
const scrollDiv = useRef(null)
{scrollDiv.current && <ScrollIndicator container={scrollDiv.current} />}
I have a event when you resize the window will show desktop sidebar or mobile sidebar. if window is less than But there are variables that aren't updated immediately to show sidebar if I'm in desktop window, I could that with class
I've created a sandbox https://codesandbox.io/s/sidebar-hooks-8oefi
to see the code, I have the class component in App_class.js which if I replace in App it works, but with hooks (App_hooks.js file, by default in App.js) I can't make it works
Thanks for your attention. I’m looking forward to your reply.
with class I could do that using:
if (isMobile !== wasMobile) {
this.setState({
isOpen: !isMobile
});
}
const App = props => {
//minicomponent to update width
const useListenResize = () => {
const [isOpen, setOpen] = useState(false);
const [isMobile, setMobile] = useState(true);
//const [previousWidth, setPreviousWidth] = useState( -1 );
let previousWidth = -1;
const updateWidth = () => {
const width = window.innerWidth;
const widthLimit = 576;
let newValueMobile = width <= widthLimit;
setMobile(isMobile => newValueMobile);
const wasMobile = previousWidth <= widthLimit;
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
//setPreviousWidth( width );
previousWidth = width;
};
useEffect(() => {
updateWidth();
window.addEventListener("resize", updateWidth);
return () => {
window.removeEventListener("resize", updateWidth);
};
// eslint-disable-next-line
}, []);
return isOpen;
};
const [isOpen, setOpen] = useState(useListenResize());
const toggle = () => {
setOpen(!isOpen);
};
return (
<div className="App wrapper">
<SideBar toggle={toggle} isOpen={isOpen} />
<Container fluid className={classNames("content", { "is-open": isOpen })}>
<Dashboard toggle={toggle} isOpen={isOpen} />
</Container>
</div>
);
};
export default App;
with setState didn't work
The issue is after setting isMobile value here,
setMobile(isMobile => newValueMobile);
You are immediately refering that value,
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
Due to async nature of setState, your are getting previous value of isMobile here all the times.
To make this work you need to make some change to your code.
You are directly mutating previousWidth value, you should have previousWidth in a state and use setter function to change the value.
const [previousWidth, setPreviousWidth] = useState(-1);
setPreviousWidth(width); //setter function
You cannot get value immediately after setState. You should use another useEffect with isMobile and previousWidth as dependency array.
useEffect(() => {
const wasMobile = previousWidth <= widthLimit;
if (isMobile !== wasMobile) {
setOpen(isOpen => !isMobile);
}
}, [isMobile, previousWidth]);
Demo
Since updateWidth is registered once on component mount with useEffect, then it will refer to a stale state (isMobile), which is always true, therefore, setOpen(isOpen => !isMobile) will never work.
EDIT: The following code illustrates a simpler version of your issue:
const Counter = () => {
const [count, setCount] = useState(0)
const logCount = () => {
console.log(count);
}
useEffect(() => {
window.addEventListener('resize', logCount);
}, [])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
If you run this code, you will notice that even if you change the value of count by clicking on the button, you will always get the initial value when resizing the browser (check the console), and that's because logCount refer to a state that was obtained at the moment it was defined.
#ravibagul91 answer works, because it access the state from useEffect, and not from the event handler.
You may wanna check: React hooks behaviour with event listener and Why am I seeing stale props or state inside my function?, it will give you a better idea of what's going on.
This question already has answers here:
Make React useEffect hook not run on initial render
(16 answers)
Closed last month.
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"}
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.
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.
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.
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])
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.