I am using primereact's Autocomplete component. The challenge is that I don't want to set the options array to the state when the component loads; but instead I fire an api call when the user has typed in the first 3 letters, and then set the response as the options array (This is because otherwise the array can be large, and I dont want to bloat the state memory).
const OriginAutocomplete = () => {
const [origins, setOrigins] = useState([]);
const [selectedOrigin, setSelectedOrigin] = useState(null);
const [filteredOrigins, setFilteredOrigins] = useState([]);
useEffect(() => {
if (!selectedOrigin || selectedOrigin.length < 3) {
setOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
});
}
}, [selectedOrigin, setOrigins]);
const handleSelect = (e) => {
//update store
}
const searchOrigin = (e) => {
//filter logic based on e.query
}
return (
<>
<AutoComplete
value={selectedOrigin}
suggestions={ filteredOrigins }
completeMethod={searchOrigin}
field='code'
onChange={(e) => { setSelectedOrigin(e.value) }}
onSelect={(e) => { handleSelect(e) }}
className={'form-control'}
placeholder={'Origin'}
/>
</>
)
}
Now the problem is that the call is triggered when I type in 3 letters, but the options is listed only when I type in the 4th letter.
That would have been okay, infact I tried changing the code to fire the call when I type 2 letters; but then this works as expected only when I key in the 3rd letter after the api call has completed, ie., I type 2 letters, wait for the call to complete and then key in the 3rd letter.
How do I make the options to be displayed when the options array has changed?
I tried setting the filteredOrigins on callback
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins([...origins])
});
But it apparently doesn't seem to work.
Figured it out. Posting the answer in case someone ponders upon the same issue.
I moved the code inside useEffect into the searchOrigin function.
SO the searchOrigin functions goes like below:
const searchOrigin = (e) => {
const selectedOrigin = e.query;
if (!selectedOrigin || selectedOrigin.length === 2) {
setOrigins([]);
setFilteredOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins(origins);
});
}
if (selectedOrigin && selectedOrigin.length > 3) {
const filteredOrigins = (origins && origins.length) ? origins.filter((origin) => {
return origin.code
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.name
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.city
.toLowerCase()
.startsWith(e.query.toLowerCase())
}) : [];
setFilteredOrigins(filteredOrigins);
}
}
Related
I made a filter by id. I'm trying to make a live filter by the first letters entered from the input, but it doesn't work.
const [foodList, setFoodList] = useState(DATA)
const [input, setInput] = useState()
const getInputValue = (event) => {
setInput(event.target.value)
}
const liveFilter = () => {
if (isNaN(input) !== false || input !== undefined) {
// setFoodList(foodList.filter(e => (e.foodName).startsWith(input) === true)) // I tried to write like this but it doesn't work
setFoodList(foodList.filter(el => el.id === parseInt(input)))
}
if (input === '' || input === undefined) {
setFoodList(DATA)
}
if (input > foodList.length) {
setFoodList(DATA)
}
}
useEffect(liveFilter,[input])
Also I have a warning in useEffect: "React Hook useEffect has a missing dependency: 'foodList'. Either include it or remove the dependency array. You can also do a functional update 'setFoodList(f => ...)' if you only need 'foodList' in the 'setFoodList' call"
I don't understand how to fix it
Try to use two arrays: one for data source and another for filtered items
setFoodList(data_source.filter(e => (e.foodName).startsWith(input, 0) === true))
I do apologise in advance as I'm still new to ReactJS. I'm fooling around with eventListeners on keyboard press's and I've come across an example of a custom hook being used on handling keypress's. I've tried refactoring the code to suite my needs but I'm unable to do so. Currently I have the eventListener assigned on checking for the "Enter" key being pressed, but I'd like to change it to the use of "Ctrl+S" being pressed instead, how would I go about doing that and if a short explanation could be provided, I'd really appreciate that. I understand that keydown would be suited best, But I'm unable to get it working.
here is what I have:
function useKey(key, cb){
const callback = useRef(cb);
useEffect(() => {
callback.current = cb;
})
useEffect(() => {
function handle(event){
if(event.code === key){
callback.current(event);
}
}
document.addEventListener('keypress',handle);
return () => document.removeEventListener("keypress",handle)
},[key])
}
function App() {
const handleSubmit = () => {
alert('submitted')
}
useKey("Enter", handleSubmit)
render (
<div>....</div>
)
}
To capture ctrl+S what you need is e.ctrlKey along with `e.code === 's'. So here is a little modification:
function useKey(key, cb){
const callback = useRef(cb);
useEffect(() => {
callback.current = cb;
})
useEffect(() => {
function handle(event){
if(event.code === key){
callback.current(event);
} else if (key === 'ctrls' && event.key === 's' && event.ctrlKey) {
callback.current(event);
}
}
document.addEventListener('keydown',handle);
return () => document.removeEventListener("keydown",handle)
},[key])
}
function App() {
const handleSubmit = () => {
alert('submitted')
}
useKey("Enter", handleSubmit)
useKey('ctrls', () => console.log('Ctrl+S fired!'));
render (
<div>....</div>
)
}
I'm new to React and trying to understand why my state value is blank. In my component, I have a state for the HTML input element. When I put a console log on the function "searchChange" handler it is properly receiving my keyboard entries. But when I click on the enter key the value of "searchState" is blank. So I'm wondering what is wrong with my code?
export default (props: any, ref: any) => {
const SearchNavigation = forwardRef((props: any, ref: any) => {
const [searchState, setSearchState] = React.useState('');
const searchChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
console.log(`Input ${evt.target.value}`);
setSearchState(evt.target.value);
};
function enterHandler(evt: any): any {
if ((evt.which && evt.which === 13) || (evt.keyCode && evt.keyCode === 13)) {
console.log(`Enter value ${searchState}`);
return false;
}
}
useEffect(() => {
document.body.addEventListener('keypress', enterHandler);
}, []);
return (
<section id={props.id} className="is-shadow-d2">
<input type="text" onChange={searchChange} />
</section>
);
}
If I add the following hook, it is logging my value of "searchState" properly as I type each letter, but I want to use the value when the users clicks on the enter key.
useEffect(() => {
console.log(`State Changed: ${searchState}`);
}, [searchState]);
Update
So I modified my code to handle a keypress on the input and seems to work.
const searchKeyPress = (evt: React.KeyboardEvent) => {
if (evt.key === 'Enter') {
console.log(`Enter value ${searchState}`);
}
};
....
<input type="text" onChange={searchChange} onKeyPress={searchKeyPress} />
And now its showing the values. But what I'm wondering about is if I want to have a global enter key to do something like save the data but the states values are blank, what is the best way to handle that?
useEffect(() => {
document.body.addEventListener('keypress', enterHandler);
}, []);
The empty dependency array says that this effect should be run once, and then never again. At the time that the effect runs, searchState is an empty string, and so enterHandler closes over that empty string. Since the functions never get updated, it will still see an empty string when the keypress occurs.
Instead, you can change it to update when the searchState changes like this:
useEffect(() => {
function enterHandler(evt: any): any {
if ((evt.which && evt.which === 13) || (evt.keyCode && evt.keyCode === 13)) {
console.log(`Enter value ${searchState}`);
return false;
}
}
document.body.addEventListener('keypress', eventHandler);
return () => {
document.body.removeEventListener('keypress', eventHandler);
}
}, [searchState]);
The issue is that states are not accessible in the context of event listeners.
You can find an answer to this problem here: Wrong React hooks behaviour with event listener
I'm making a rock, paper, scissors game.
I want to increment wins after player has selected their option.
I get an error as if the if statement is unaware of any value.
Heres the code:
What the state looks like
const [wins, setWins] = useState(0);
const [playerSelect, setPlayerSelect] = useState(null);
const [computerSelect, setComputerSelect] = useState(null);
// in browser
{id: 1, name:'rock'}
Player select function
const handleSelection = choice => {
const optionClicked = options.find(o => o.id === choice);
setPlayerSelect(optionClicked);
if (playerSelect.name === 'rock' && computerSelect.name === 'scissors') {
setWins(wins => wins + 1);
}
};
I have tried added precautions to check that playerSelect state contains a value first to no prevail. win state in not incremented.
Example:
const handleSelection = choice => {
const optionClicked = options.find(o => o.id === choice);
setPlayerSelect(optionClicked);
if (
playerSelect &&
playerSelect.name &&
playerSelect.name === 'rock' &&
computerSelect.name === 'scissors'
) {
setWins(wins => wins + 1);
}
};
Edit: I have tried the following code and it works after the second click only.
const checkWhoWins = () => {
if (playerSelect.name === 'rock' && computerSelect.name === 'scissors') {
setWins(wins => wins + 1);
}
};
playerSelect && checkWhoWins();
if you want to do an action after a state update you would use useEffect for that, and specify its dependencies as second argument:
useEffect(() => {
// you can use optional chaining to avoid triggering errors here
if (playerSelect?.name === 'rock' && computerSelect?.name === 'scissors') {
setWins(wins => wins + 1);
}
}, [playerSelect, computerSelect])
// playerSelect & computerSelect are the correct dependencies here
To restrict useEffect from running on the first render we can do:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
According to example here: https://stackoverflow.com/a/53351556/3102993
But what if my component has multiple useEffects, each of which handle a different useState change? I've tried using the isFirstRun.current logic in the other useEffect but since one returns, the other one still runs on the initial render.
Some context:
const Comp = () => {
const [ amount, setAmount ] = useState(props.Item ? Item.Val : 0);
const [ type, setType ] = useState(props.Item ? Item.Type : "Type1");
useEffect(() => {
props.OnAmountChange(amount);
}, [amount]);
useEffect(() => {
props.OnTypeChange(type);
}, [type]);
return {
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
}
}
The reason I've used separate useEffects for each is because if I do the following, it doesn't update the amount.
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
As far as I understand, you need to control the execution of useEffect logic on the first mount and consecutive rerenders. You want to skip the first useEffect. Effects run after the render of the components.
So if you are using this solution:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
useEffect (() => {
// second useEffect
if(!isFirstRun) {
console.log("Effect was run");
}
});
So in this case, once isFirstRun ref is set to false, for all the consecutive effects the value of isFirstRun becomes false and hence all will run.
What you can do is, use something like a useMount custom Hook which can tell you whether it is the first render or a consecutive rerender. Here is the example code:
const {useState} = React
function useMounted() {
const [isMounted, setIsMounted] = useState(false)
React.useEffect(() => {
setIsMounted(true)
}, [])
return isMounted
}
function App() {
const [valueFirst, setValueFirst] = useState(0)
const [valueSecond, setValueSecond] = useState(0)
const isMounted = useMounted()
//1st effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueFirst ran")
}
}, [valueFirst])
//2nd effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueSecond ran")
}
}, [valueSecond])
return ( <
div >
<
span > {
valueFirst
} < /span> <
button onClick = {
() => {
setValueFirst((c) => c + 1)
}
} >
Trigger valueFirstEffect < /button> <
span > {
valueSecond
} < /span> <
button onClick = {
() => {
setValueSecond((c) => c + 1)
}
} >
Trigger valueSecondEffect < /button>
<
/div>
)
}
ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I hope it helps !!
You can use a single useEffect to do both effects in, you just implemented the logic incorrectly.
Your original attempt:
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
The issue here is the if/elseif, treat these as independent effects instead:
useEffect(() => {
if (amount !== 0) props.onAmountChange(amount);
if (type !== "Type1") props.onTypeChange(type);
}, [amount, type])
In this method if the value is different than the original value, it will call the on change. This has a bug however in that if the user ever switches the value back to the default it won't work. So I would suggest implementing the entire bit of code like this instead:
const Comp = () => {
const [ amount, setAmount ] = useState(null);
const [ type, setType ] = useState(null);
useEffect(() => {
if (amount !== null) {
props.onAmountChange(amount);
} else {
props.onAmountChange(0);
}
}, [amount]);
useEffect(() => {
if (type !== null) {
props.onTypeChange(type);
} else {
props.onTypeChange("Type1");
}
}, [type]);
return (
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
)
}
By using null as the initial state, you can delay calling the props methods until the user sets a value in the Radio that changes the states.
If you are using multiple useEffects that check for isFirstRun, make sure only the last one (on bottom) is setting isFirstRun to false. React goes through useEffects in order!
creds to #Dror Bar comment from react-hooks: skip first run in useEffect