I have run into a weird bug with my search input component where it loses focus on every keypress and can't figure out why.
const App = () => {
let [characters, setCharacters] = useState([]);
let [filteredCharacters, setFilteredCharacters] = useState([]);
// more state
let [search, setSearch] = useState("");
useEffect(() => {
setIsLoading(true);
axios
.get(`https://swapi.co/api/people/?page=${pageNumber}`)
.then(res => {
console.log(res.data.results);
setCharacters(res.data.results);
setFilteredCharacters(res.data.results);
})
.catch(err => console.error(err))
.then(() => {
setIsLoading(false);
});
}, [pageNumber]);
function updatePage(e) {
// update page
}
function handleSearch(e) {
setSearch(e.target.value);
const filtered = characters.filter(character => {
if (character.name.toLocaleLowerCase().indexOf(e.target.value) !== -1) {
return character;
}
});
setFilteredCharacters(filtered);
}
function SearchBar() {
return (
<React.Fragment>
<StyledInput
type="text"
id="search"
value={search}
placeholder="Search by name..."
onChange={e => handleSearch(e)}
/>
</React.Fragment>
);
}
return (
<React.Fragment>
<GlobalStyles />
<div className="App">
<Heading>React Wars!</Heading>
<SearchBar />
<Pagination handleClick={updatePage} />
{!isLoading && Object.entries(filteredCharacters).length ? (
<CharacterList characters={filteredCharacters} />
) : (
<LoadingBar />
)}
</div>
</React.Fragment>
);
};
I'm also getting a Line 65:50: Expected to return a value at the end of arrow function array-callback-return for the characters.filter() line so I don't know if that has to do with it.
If it helps, the deployed app with the bug can be seen here --> https://mundane-vacation.surge.sh
You have a problem with focus because your component SearchBar is declared inside App component and it is recreated on each rendering. You need to move SearchBar out of App or just move StyledInput on the place of SearchBar. If you choose the first way, I recommend you remove React.Fragment from SearchBar, because it has only one rendered child:
function SearchBar(props) {
return (
<StyledInput
type="text"
id="search"
value={props.value}
placeholder="Search by name..."
onChange={props.onChange}
/>
);
}
And in App:
<SearchBar onChange={handleSearch} value={search} />
To fix your warning, you need always return a boolean value inside the filter function. Now you return nothing (i.e. undefined) in case if the character does not pass the condition. So change your filter function like this:
const filtered = characters.filter(character => {
return character.name.toLocaleLowerCase().indexOf(e.target.value) !== -1
});
Related
I need a search box in React that opens a dropdown as soon as an entry is made. The dropdown should contain buttons that can trigger an event.
The problem is that the dropdown has to disappear as soon as another input box is used in the application.
I could also implement this, but now I have the problem that the event of the button in question is not triggered because the focus of the input field is lost beforehand as soon as I press the button. As a result, the dropdown disappears and the event is never triggered.
This is roughly my Searchbox Component:
import React, { useState } from 'react';
import Dropdown from './Dropdown';
function Search(props) {
const [focused, setFocused] = useState(false);
const inputHandler = (params) => {
if (params.length > 0)
props.apiCall(params);
}
const buttonHandler = (id) => {
console.log(id);
}
return (
<>
<input
type="text"
placeholder="Suchen.."
onChange={(event) => inputHandler(event.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)} // Problem area
/>
{
focused === true && props.apiData.length > 0 ?
props.apiData.map((mappedData, key) => {
return (
<Dropdown
key={key}
id={mappedData.id}
name={mappedData.name}
/*
even more Data
*/
buttonHandler={buttonHandler}
/>
)
})
: null
}
</>
)
}
export default Search;
This is my Dropdown Component:
import React from 'react';
function Dropdown(props) {
return (
<ul key={props.id}>
<li>{props.name}</li>
<li>even more data</li>
<li>
<input
type="button"
value="Select"
onClick={() => {
props.buttonHandler(props.id)
}}
/>
</li>
</ul>
)
}
export default Dropdown;
To resolve this issue blur event in Search Component can be handled with delay.
function Search(props) {
const [focused, setFocused] = useState(false);
const inputHandler = (params) => {
if (params.length > 0) props.apiCall(params);
};
const buttonHandler = (id) => {
console.log(id);
};
return (
<>
<input
type="text"
placeholder="Suchen.."
onChange={(event) => inputHandler(event.target.value)}
onFocus={() => setFocused(true)}
- onBlur={() => setFocused(false)} // Problem area --> remove this line
+ onBlur={() => setTimeout(()=> setFocused(false),500)} // ---> delay added
/>
{focused === true && props.apiData.length > 0
? props.apiData.map((mappedData, key) => {
return (
<Dropdown
key={key}
id={mappedData.id}
name={mappedData.name}
/*
even more Data
*/
buttonHandler={buttonHandler}
/>
);
})
: null}
</>
);
}
Re-render problem
I have two components:
MainForm
CFNumericInput
The values coming from CFNumericInput are correct, but setValue won't render the old one.
Do I have to use an useEffect?
MainForm
const { control, watch, setValue, register } = useFormContext();
return (
<CFNumericInput
name="number1"
control={control}
setValueOnChange={(nextValue: string, oldValue: string) => {
let nextValidValue = checkNumericType(nextValue, "f6.2");
if (nextValidValue !== "") setValue("number1", nextValidValue);
else if (oldValue) setValue("number1", oldValue);
}}
/>;
)
CFNumericInput
export const CFNumericInput: React.FC<any> = ({
name,
control,
setValueOnChange,
}) => {
return control ? (
<Controller
name={name}
control={control}
render={({ field }) => {
return (
<NumericInput
{...field} // onChange, onBlur, value, name, ref
title={title}
onChange={(e) => {
field.onChange(e);
setValueOnChange && setValueOnChange(e, field.value);
}}
/>
);
}}
/>
) : (
<></>
);
};
Working but heavy solution
This solution it's working, but it's really heavy.
const [number1] = watch(["number1"]);
const [old, setOld] = useState("");
useEffect(() => {
let nextValidValue = checkNumericType(number1, "f6.2");
if (nextValidValue !== "") {
setValue("number1", nextValidValue);
setOld(nextValidValue);
} else if (old) setValue("number1", old);
}, [number1]);
Isn't possible that you call checkNumericType on onSubmit?
Also you can try on looking to use Yup (take a look at: https://react-hook-form.com/advanced-usage/#CustomHookwithResolver)
let me know what have you tried
I am new to react and I'm trying to pass a function as a prop to a child component.
In my case this is the parent component:
export default function Game() {
const [gameStarted, setGameStarted] = useState(false)
const [gameSettings, setGameSettings] = useState({})
useEffect(() => {
//setGameStarted(true);
}, [gameSettings]
)
return (
<>
{!gameStarted &&
<div className="game-form">
<GameSelection handleGameSelection={(settings)=> setGameSettings(settings)}/>
</div>}
</>
)}
My child component is:
export default function GameSelection({handleGameSelection}) {
const [labels, setLabels] = useState([])
const [gameMode, setGameMode] = useState('')
const [selectedLabels, setSelectedLabels] = useState([])
const [formError, setFormError] = useState(null)
// create label values for react-select
useEffect(() => {
if(document) {
setLabels(document.cards.map(card => {
return { value: {...card}, label: card.label}
}))
}
}, [document])
const handleSubmit = (e) => {
e.preventDefault()
try{
const gameSettings = {
mode: gameMode.value,
selected: selectedLabels.map((card) => ({...card.value})),
}
handleGameSelection(gameSettings)
}
catch(error){
console.log(error)
}
}
return (
<>
<h2 className="page-title">Please select your game</h2>
<form onSubmit={handleSubmit}>
<label>
<span>Mode:</span>
<Select
onChange={(option) => setGameMode(option)}
options={gameModes}
/>
</label>
<label>
<span>Select labels:</span>
<Select
onChange={(option) => setSelectedLabels(option)}
options={labels}
isMulti
/>
</label>
<button className="btn" >Start game</button>
{formError && <p className="error">{formError}</p>}
</form>
</>
)}
My form works but when I submit the form I keep getting the error TypeError: handleGameSelection is not a function. I tried everything. I have created a separate function in the parent component and gave that as a prop to the child. That also didn't work. I don't know what I am doing wrong. Any ideas?
Run this function inside useEffect, because currently, you are running this function before component is fully mounted and this function is propably undefined... or you can try to use if(typeof handleGameSelection === 'function') to check if its already initialized
Child component
export const FlightRange = (props) => {
const [value, setValue] = useState(props.value);
return (
<>
<input
type='range'
min={1000}
max={50000}
step="500"
value={value}
onChange={(e) => {
setValue(e.target.value);
props.handleSliderChange(value);
}}
/>
<span>{value}</span>
</>
);
};
parent component
useEffect(() => {
const result = axios.get('http://localhost:8000/')
.then((res) => res.json())
.then((data) => {
const flightData = data.filter((value) => {
return (
valuesplit(' ')[1] < priceSlider
);
});
})
}, [priceSlider]);
return(
<Child value={priceSlider} handleSliderChange={(value)=> setPriceSlider(value)} />
)
useEffect does not get called when the slider is changed the first time. It gets called a second time with the stale (previous value) value.
What am I missing?
in onChange you need to call like this
onChange={(e) => {
setValue(e.target.value);
props.handleSliderChange(e.target.value);
}}
since value is not updated instantly when you call setValue(e.target.value); , value will have previous value that you are passing in props.handleSliderChang(value)
to know how setState works see this answer
The issue is on the onClick callback of FlightRange input, see comments on code below
onChange = {(e) => {
setValue(e.target.value); // this is async
// therefore, the value you are passing here is not the same as e.target.value but simply the value before setting the state
props.handleSliderChange(value);
}}
So to fix this just refactor props.handleSliderChange argument to e.target.value
onChange = {(e) => {
setValue(e.target.value);
props.handleSliderChange(e.target.value);
}}
It because the child is having it's own life cycle since you are using useState in child. so whatever props you pass to your child, the child's state won't affected.
plus you are passing incorrect value in onChange
Solution: just use the props value directly on child (do not store in state):
export const FlightRange = (props) => {
const { value, handleSliderChange } = props;
return (
<>
<input
type='range'
min={1000}
max={50000}
step="500"
value={value}
onChange={(e) => {
handleSliderChange(e.target.value);
}}
/>
<span>{value}</span>
</>
);
};
I want to get the state of child component when the button is clicked in the parent component.
child component handles its own states. but when an action is triggered in the parent component I want the data of a child component
the code snippet in the simplest form as this and I can't change the component architecture
const Child = (props) => {
const [name, setName] = useState("")
return (
<input value={name} onChange={(e) => { setName(e.target.value) }} />
)
}
const parent = (props) => {
const abc=()=>{
// i want the name value here
}
return (
<React.Fragment>
<Child />
<button onClick={()=>{abc()}}>Abc</button>
</React.Fragment>
)
}
As with most problems, there are multiple ways to solve this one. Each solution will be more appropriate and readable for a different use case.
One option would be to move the state into the parent component.
const Child = (props) => {
return (
<input value={props.name} onChange={(e) => { props.setName(e.target.value) }} />
)
}
const parent = (props) => {
const [name, setName] = useState("")
const abc=()=>{
// i want the name value here
}
return (
<React.Fragment>
<Child name={name} setName={setName} />
<button onClick={()=>{abc()}}>Abc</button>
</React.Fragment>
)
}
Another method would be to use useRef. More documentation on this use case
const Child = (props) => {
const [name, setName] = useState("")
props.nameRef.current = name
return (
<input value={name} onChange={(e) => { setName(e.target.value) }} />
)
}
const parent = (props) => {
const nameRef = useRef("");
const abc=()=>{
// i want the name value here
}
return (
<React.Fragment>
<Child nameRef={nameRef}/>
<button onClick={()=>{abc()}} >Abc</button>
</React.Fragment>
)
}
IMHO most of the time you should use the first one. However, if it makes the code more readable for the state to live in the child component, or it would take too much time to refactor, then the second example works as well.
const Child = (props) => {
return (
<input value={name} onChange={e => setName(e.target.value) } />
)
}
const parent = (props) => {
const [name, setName] = useState("")
const abc=()=>{
// i want the name value here
}
return (
<React.Fragment>
<Child setName={setName}/>
<button onClick={()=>{abc()}}>Abc</button>
</React.Fragment>
)
}