Currently I am working on a project where i am using react and redux to build it.
Scenario
There is a toggle button which will toggle between Group Queue and My Queue. Both these will hit a api and load the data(list of tickets) .[i.e. 2 separate entities]
Now there is a search box while will give the result based on the value i enter. I have added the 'Delay functionality' i.e. after 2 seconds onChange event will fire and it will dispatch an action to all reducers.
Problem
Let's say, i search for a value(68) under group Queue, it works fine. Now if i toggle to my queue, SearchBox value should be null.but it shows as 68
As i told, both queue are separate bodies, if i toggle, searchBox's value should disappear, but its not.
Reason
i am storing a state of search box value and timer to set the value on typing after 2 seconds and on every character I type, setTimeOut and clearTimeOut will be invoked to accommodate the delay functionality.
Thus i have a state inside my React SearchBox Component and once i type and stay put for 2 seconds, then it will dispatch the action via useEffect/ComponentDidUpdate.
i decided to have a state inside the component as i did not want to fire an action on every character user types. Only the intended search Value Redux should store.
Things i tried
i tried with getDerivedStateFromProps, but as i need to use SetState and its asynchronous, its not reflecting the current value.
i have added 2 functionality (onChange and OnEnter). With OnEnter, i think it will work as i can pass the redux store's value and action to the input's value and OnEnter attribute. [i have not tried]
But i want to have the delay functionality. Can anyone suggest an alternative ?
Codes
1. SearchComponent
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { filterAction } from "../../actions";
import "./search.css";
const WAIT_INTERVAL = 2000;
const ENTER_KEY = 13;
const Search = (props) => {
console.log("search", props.value);
let [value, setValue] = useState(props.value);
let [timer, setTimer] = useState(0);
useEffect(() => {
setTimer(
setTimeout(() => {
if (timer) props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
return () => clearTimeout(timer);
}, [value]);
const handleKeyDown = (event) => {
if (event.keyCode === ENTER_KEY) {
clearTimeout(timer);
props.searchAction("SEARCH_FILTER", value);
}
};
// GETDERIVEDSTATEFROMPROPS
// if (props.value !== value) {
// setValue("");
// // console.log("check2", props.value, value);
// }
return (
<input
className="search-box"
type="search"
placeholder="Search any Request Id/ Email Id..."
aria-label="Search"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
></input>
);
};
const mapStateToProps = (store) => ({
value: store.filterValues.searchFilter,
});
export default connect(mapStateToProps, { filterAction })(Search);
Redux Redcuer (FilterReducer) -- working fine i.e. on toggling, searchValue is getting emptied('').
export const filterReducer = (filterValues = {}, action) => {
switch (action.type) {
case "TOGGLE_QUEUE":
return {
order: [],
tileFilter: action.payload.tileType,
searchFilter: "",
buttonFilter: {},
timelineFilter: "all",
};
case "SEARCH_FILTER":
let arr = [...filterValues.order];
arr.push("SEARCH_FILTER");
let ob = {
...filterValues,
searchFilter: action.payload.value,
order: arr,
};
return ob;
}
return filterValues;
};
Please let me know if you have any solutions.
EDIT 1
i tried with props.value on UseEffect, now the search itself is not working..
useEffect(() => {
// setValue(props.value); // not working
setTimer(
setTimeout(() => {
if (timer) props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
setValue(props.value); // not working
return () => clearTimeout(timer);
}, [props.value]);
Edit2
i added a separate useffect as suggested by Sabbit, but if i type 68, the search functionality is getting invoked on 6 and typed value (68) alternatively.
useEffect(() => {
setTimer(
setTimeout(() => {
if (timer && value.length > 0)
props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
return () => clearTimeout(timer);
}, [value]);
useEffect(() => {
if (value !== props.value) setValue(props.value);
}, [props.value]);
Did you tried using another useEffect for props.value?
useEffect(() => {
setTimer(
setTimeout(() => {
if (timer && value.length > 0) props.filterAction("SEARCH_FILTER", value);
}, WAIT_INTERVAL)
);
return () => clearTimeout(timer);
}, [value]);
useEffect(() => {
setValue(props.value);
}, [props.value]);
That will update the value in the state each time the props.value is changed. I believe the searchValue from reducer is passed via props to the Search component.
Update: I have updated the condition to execute props.filterAction("SEARCH_FILTER", value) from if(timer) to if(timer && value.length > 0). We need to do that because everytime the props changes we are setting the value in state and we have used an useEffect depending on the change of value in state. That is why it is searching with empty string. We need to make sure that the searching doesn't happen unless we have something in the input field.
Related
What the code does: It's performing a DOM search based on what's typed in an input (it's searching elements by text). All this is happening in a React component.
import { useEffect, useReducer } from "react";
let elements: any[] = [];
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
elements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
console.log('elements 1:', elements)
}, [state]);
console.log('elemets 2:', elements)
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
The problem: The value of elements outside of useEffect is the old data. For example, if you type a in the input, console.log('elements 1:', elements) will log 1, and console.log('elements 2:', elements) will log 0. Note: there are 3 buttons, and one of them has the text a.
The strange thing is that the problem doesn't happen if you comment out this if-statement:
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
In this case, if you type anything (since the pattern matching has been commented out), console.log('elements 1:', elements) and console.log('elements 2:', elements) will log 3. Note: there are 3 buttons.
Question: What could be the problem, and how to fix it? I want to render the current length of elements.
Live code:
It's happening because of the elements variable is not a state, so it's not reactive.
Create a state for the elements:
const [elements, setElements] = useState<HTMLButtonElement[]>([])
And use this state to handle the elements.
import { useEffect, useReducer, useState } from "react";
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
const [elements, setElements] = useState<HTMLButtonElement[]>([])
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
let newElements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
setElements(newElements)
console.log("elements 1:", elements?.length);
}, [state]);
console.log("elemets 2:", elements?.length);
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements?.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
Your useEffect() runs after your component has rendendered. So the sequence is:
You type something into input, that triggers handleInputChange
handleInputChange then updates your state using updateState()
The state update causes a rerender, so App is called App()
console.log('elemets 2:', elements.length) runs and logs elements as 0 as it's still empty
App returns the new JSX
Your useEffect() callback runs, updating elements
Notice how we're only updating the elements after you've rerendered and App has been called.
The state of your React app should be used to describe your UI in React. Since elements isn't React state, it has a chance of becoming out of sync with the UI (as you've seen), whereas using state doesn't have this issue as state updates always trigger a UI update. Consider making elements part of your state. If it needs to be accessible throughout your entire App, you can pass it down as props to children components, or use context to make it accessible throughout all your components.
With that being said, I would make the following updates:
Add elements to your state
Remove your useEffect() with the dependency of [state]. If we were to update the elements state within this effect, then that would trigger another rerender directly after the one we just did for the state update. This isn't efficient, and instead, we can tie the update directly to your event handler. See You Might Not Need an Effect for more details and dealing with other types of scenarios:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useEffect, useReducer} = React;
const App = () => {
const initialState = {keyEvent: {}, value: "Initial state", elements: []};
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates}),
initialState
);
function searchDOM(value) {
const selectors = "button";
const pattern = new RegExp(value === "" ? "^$" : value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
return nodeWithText?.textContent?.match(pattern);
}
return false;
}
);
}
function handleInputChange(event) {
updateState({
value: event.target.value,
elements: searchDOM(event.target.value)
});
}
function isCommand(event) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event) {
if (isCommand(event)) {
updateState({
keyEvent: event
});
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
console.log("elements:", state.elements.length);
return (
<div>
<input id="input" type="text" onChange={handleInputChange} value={state.value} />
<div id="count">{state.elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
ReactDOM.createRoot(document.body).render(<App />);
</script>
useEffect triggered after react completed its render phase & flush the new changes to the DOM.
In your case you have two useEffects. The first one register your event lister which will then update your component state when input field change. This triggers a state update.( because of the setState )
So React will start render the component again & finish the cycle. And now you have 2nd useEffect which has state in dependency array. Since the state was updated & the new changes are committed to the DOM, react will execute 2nd useEffect logic.
Since your 2nd useEffect just assign some values to a normal variable React will not go re render your component again.
Based on your requirement you don't need a 2nd useEffect. You can use a useMemo,
let elements = useMemo(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
})
}, [state])
Note: You don't need to assign your elements into another state. It just create another unwanted re render in cycle. Since you are just doing a calculation to find out the element array you can do it with the useMemo
I have a group of react-native components that have similar interfaces. The important method in the interface is the run method. Users can execute this run method by pressing an action button for each of the components or by pressing an action button in the parent level, that will iterate through each of these similar components using their ref and call the run method. Here is a simplified version of the setup.
const Test1 = forwardRef((props, ref) => {
const [importantState, setimportantState] = useState('initial')
useImperativeHandle(ref, () => ({
run: run,
}))
const run = async () => {
// This function will open a modal
// where users will eventually change the important state
openInteractionModal()
// The idea of this promiss is to settle when the users
// eventually change the importantState thorugh thier interaction
// It does so by checking the state every 100 miliseconds
// This however is a problem as a result of the setInterval function will
// inclose the initial state instead of getting the new value each time
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
if (importantStat === 'complete') {
resolve('success')
clearInterval(interval)
} else if (importantStat === 'failed') {
reject('failed')
clearInterval(interval)
}
}, 100)
})
}
return (
<>
<Button title="run" onPress={run} />
</>
)
})
const parentComponent = () => {
const test1Ref = userRef(null)
const test2Ref = userRef(null)
const test3Ref = userRef(null)
const testRefs = [test1Ref, test2Ref, test3Ref]
const autoRun = async () => {
testRefs.forEach(ref => {
await ref?.current.run()
});
}
return (
<>
<Button title="Auto run" onPress={autoRun} />
<Test1 ref={test1Ref} />,
<Test2 ref={test2Ref} />,
<Test3 ref={test3Ref} />,
</>
)
}
The problem I am having right now is the promise I am returning from these individual components never settles as a result of the closure I mentioned above in the comments. Is there a way to overcome this closure, or is there a better react-native pattern that I am using right now?
Don't go polling. Instead, pass the resolve function as an argument to openInteractionModal, and call it together with setImportantState from the modal dialog.
If that is for some reason not possible, you can use a ref to store the resolve function and run it from an effect when the state changes:
const onImportantStateChange = useRef();
useEffect(() => {
if (!onImportantStateChange.current) return;
if (importantState === 'complete') {
onImportantStateChange.current('success');
onImportantStateChange.current = undefined;
} else if (importantState === 'failed') {
onImportantStateChange.current(Promise.reject(new Error('failure')));
onImportantStateChange.current = undefined;
}
}, [importantState])
const run = async () => {
return new Promise(resolve => {
onImportantStateChange.current = resolve;
// This function will open a modal
// where users will eventually change the important state
openInteractionModal();
});
}
Create a ref and store the status in that instead of the state of the Test1 component.
Like that
const importantState = useRef(null);
Set value in the current of the ref
importantState.current="complete"
Later use it in the setInterval like
if(importantState.current === "complete")
I want to store "jokes" and console log a new value every time the interval runs (a get call is made) after 5 seconds. However, the value doesn't render anything after each interval. I'm unsure if jokes are being called and captured since it prints out as JOKE: []. My end goal is to create a logic using the "joke" state.
If you wish to test it yourself, https://codesandbox.io/s/asynchronous-test-mp2fq?file=/AutoComplete.js
const [joke, setJoke] = React.useState([]);
React.useEffect(() => {
const interval = setInterval(() => {
axios.get("https://api.chucknorris.io/jokes/random").then((res) => {
setJoke(res.data.value);
console.log("JOKE: ", joke); // <- Doesn't print every time it is called.
});
console.log("Every 5 seconds");
}, 5000);
if (joke.length !== 0) {
clearInterval(interval);
console.log("Returns True");
}
return () => clearInterval(interval);
}, []);
Your setJoke call is actually working. The problem is the console.log being called right after setJoke. As mentioned in the answer of this question, setState is async. You can find this issue explained in the React docs:
Calls to setState are asynchronous - don’t rely on this.state to reflect the new value immediately after calling setState. Pass an updater function instead of an object if you need to compute values based on the current state (see below for details).
You can see that joke variable is changing every 5 seconds by adding it to the JSX:
return (
<>
{JSON.stringify(joke)}
<Autocomplete
sx={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
...
To achieve your goal you have to, for example, split your code into two useEffect calls:
const [joke, setJoke] = React.useState([]);
React.useEffect(() => {
const interval = setInterval(() => {
axios.get("https://api.chucknorris.io/jokes/random").then((res) => {
if (res.data.value.length !== 0) {
setJoke(res.data.value);
clearInterval(interval);
console.log("Returns True");
}
});
console.log("Every 5 seconds");
}, 5000);
return () => clearInterval(interval);
}, []);
React.useEffect(() => {
// here you can write any side effect that depends on joke
}, [joke]);
So I have this bit of code that does not work as expected. Current focus has been set using useState() on the parent component, hence its a state variable. However, when the currentFocus value changes in the parent, focus variable here itself is not updated. I would have expected the re render of the parent component, which in turn rerenders this component would cause the foucs value to change.
import React, { useRef, useEffect, useState } from 'react';
const CookieDetails = props => {
const {
name,
cost,
value,
numOwned,
onMouseClick,
cookieId,
currentFocus,
} = props;
let cookieInfoRef = useRef(null);
//My focus doesnt change even if I change currentFocus in parent component
const [focus, setFocus] = useState(currentFocus);
useEffect(() => {
console.log('currentFocus', currentFocus);
console.log('focus', focus);
console.log('cookieID', cookieId);
if (cookieInfoRef.current && cookieId === focus) {
console.log('current', cookieInfoRef.current);
cookieInfoRef.current.focus();
}
}, [focus]);
return (
<React.Fragment>
<button
onClick={onMouseClick}
ref={cookieId === focus ? cookieInfoRef : null}
>
<h3>{name}</h3>
<p>Cost:{cost}</p>
<p>Value:{value}</p>
<p>Owned:{numOwned}</p>
</button>
</React.Fragment>
);
};
export default CookieDetails;
Now I can solve this problem by doing the following instead,
import React, { useRef, useEffect, useState } from 'react';
const CookieDetails = props => {
const {
name,
cost,
value,
numOwned,
onMouseClick,
cookieId,
currentFocus,
} = props;
let cookieInfoRef = useRef(null);
useEffect(() => {
console.log('currentFocus', currentFocus);
console.log('cookieID', cookieId);
if (cookieInfoRef.current && cookieId === currentFocus) {
console.log('current', cookieInfoRef.current);
cookieInfoRef.current.focus();
}
});
return (
<React.Fragment>
<button
onClick={onMouseClick}
ref={cookieId === currentFocus ? cookieInfoRef : null}
>
<h3>{name}</h3>
<p>Cost:{cost}</p>
<p>Value:{value}</p>
<p>Owned:{numOwned}</p>
</button>
</React.Fragment>
);
};
export default CookieDetails;
But I only wanted to run the useEffect hook when focus/currentFocus is updated and not after every render. So why does this happen? What am I am missing to understand here.
Also I previously noticed if you did something like this, the handleStuff function always uses the initial value of 100 (so its always 100 + 10) instead of incrementing by 10 each time a key is pressed. I can solve this by removing the empty [] as the second argument in useEffect. However, I expected the handleStuff to still update with the latest value instead, given that the eventListner has already been added on initial render and on second keydown it should add 10 to 110 instead of 100, but it keeps using the initial state value of 100 instead. Why must you clear the old listener and add the new one each time for it work?
[value, setValue] = useState(100)
handleStuff = (event) => {
setValue(value+10)
}
useEffect(() => {
window.addEventListener('keydown', handleStuff)
return () => {
window.removeEventListener('keydown', handleStuff)
}
},[]);
I am not really insterested in the solutions, I am really insterested in understanding how useEffect() and useState() hooks function in this conditions.
Hey the useEffect hook can be a bit confusing.
But the way to think of it is
useEffect(() => {
// Called in an infinite loop (most cases you won't need this)
})
useEffect(() => {
// Called on first render
}, [])
useEffect(() => {
// Called when x y & z updates
}, [x, y, z])
The problem is you aren't listening for the currentFocus update in your code.
useEffect(() => {
// Called when focus updates
}, [focus]);
useEffect(() => {
// Called when props.currentFocus & focus updates
}, [props.currentFocus, focus]);
Now to explain your confusion:
// My focus doesnt change even if I change currentFocus in parent component
const [focus, setFocus] = useState(currentFocus);
When you are passing in currentFocus here the only time the currentFocus value is being passed to the focus useState is on the first render. So you need to listen for the currentFocus update and then set the new focus which should work how you want!
// Focus set to props.currentFocus on first render
const [focus, setFocus] = useState(currentFocus);
useEffect(() => {
// Update focus
setFocus(props.currentFocus)
}, [props.currentFocus])
useEffect(() => {
// currentFocus will be the same as focus now
console.log('currentFocus', currentFocus);
console.log('focus', focus);
console.log('cookieID', cookieId);
if (cookieInfoRef.current && cookieId === focus) {
console.log('current', cookieInfoRef.current);
cookieInfoRef.current.focus();
}
}, [focus]);
Hope that helps!
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.