I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value
Related
How can I retrieve the dishId selected from my options react - select that shows me thedishType in order to send them my parent component FormRender and to the backend.
My first dropdown shows me: Menu1, Menu2...
My second one: type2...
So if I click ontype4, how can I store the related dishId(here = 4). I can click on several values i.e: type2 andtype3.
How do I keep the dish ids i.e : 2 and 3 and send them to my FormRender parent
Menus(first page of my multi - step form):
export default function Menus() {
const [selectionMenus, setSelectionMenus] = useState({});
const [selectionDishes, setSelectionDishes] = useState({});
const [menus, setMenus] = useState([])
const [date, setDate] = useState('')
useEffect(() => {
axios
.post(url)
.then((res) => {
console.log(res);
setMenus(res.data.menus);
})
.catch((err) => {
console.log(err);
});
}, []);
const names = menus?.map(item => {
return {
label: item.menuId,
value: item.name
}
})
const types = menus?.flatMap(item => {
return item.dishes.map(d => ({
label: d.dishId,
value: d.dishType
}))
})
const handle = (e) => {
if (e?.target?.id === undefined) return setInfo(e);
if (e?.target?.id === undefined) return setSelectionMenus(e);
if (e?.target?.id === undefined) return setSelectionDishes(e);
switch (e.target.id) {
case "date":
setDate(e.target.value);
break;
...
default:
}
}
};
return (
<>
<form>
<div>My menus</div>
<label>
Dishes :
<Dropdown
options={names}
value={selectionMenus}
setValue={setSelectionMenus}
isMulti={true}
/>
</label>
<label>
<Dropdown
options={types}
value={selectionDishes}
setValue={setSelectionDishes}
isMulti={true}
/>
</label>
<label>
Date:
<div>
<input
type="date"
name='date'
value={date}
onChange={handle}
id="date"
/>
</div>
</label>
...
</form>
<div>
<button onClick={() => nextPage({ selectionDishes, selectionMenus, date })}>Next</button>
</div>
</>
);
}
Here the parent Component FormRender that is supposed to retrieve the values of all dishId selected and send them to the backend:
export default function FormRender() {
const [currentStep, setCurrentStep] = useState(0);
const [info, setInfo] = useState();
const [user, setUser] = useState();
const headers = ["Menus", "Details", "Final"];
const steps = [
<Menus
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>,
<Details
backPage={() => setCurrentStep((s) => s - 1)}
nextPage={setUser}
/>,
<Final />
];
useEffect(() => {
if (info === undefined || user === undefined) return;
const data = {
date: info.date,
id: //list of dishId selected but don't know how to do that??
};
}, [info, user]);
return (
<div>
<div>
<Stepper steps={headers} currentStep={currentStep} />
<div >{steps[currentStep]}</div>
</div>
</div>
);
}
Dropdown:
export default function Dropdown({ value, setValue, style, options, styleSelect, isMulti = false }) {
function change(option) {
setValue(option.value);
}
return (
<div onClick={(e) => e.preventDefault()}>
{value && isMulti === false ? (
<Tag
selected={value}
setSelected={setValue}
styleSelect={styleSelect}
/>
) : (
<Select
value={value}
onChange={change}
options={options}
isMulti={isMulti}
/>
)}
</div>
);
}
Here my json from my api:
{
"menus": [
{
"menuId": 1,
"name": "Menu1",
"Description": "Descritption1",
"dishes": [
{
"dishId": 2,
"dishType": "type2"
},
{
"dishId": 3,
"dishType": "type3"
},
{
"dishId": 4,
"dishType": "type4"
}
]
},
...
]
}
You already store the selected values inside the selectionMenus and selectionDishes states. So, if you want to send them to the parent FormRender component you can instead create those two states inside that component like this:
export default function FormRender() {
const [selectionMenus, setSelectionMenus] = useState();
const [selectionDishes, setSelectionDishes] = useState();
....
}
Then pass those values to the Menus component:
<Menus
selectionMenus={selectionMenus}
setSelectionMenus={setSelectionMenus}
selectionDishes={selectionDishes}
setSelectionDishes={setSelectionDishes}
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>
Subsequently, you will have to remove the state from the Menus component and use the one you receive from props:
export default function Menus({ selectionMenus, setSelectionMenus, selectionDishes, setSelectionDishes }) {
/*const [selectionMenus, setSelectionMenus] = useState({}); //remove this line
const [selectionDishes, setSelectionDishes] = useState({});*/ //remove this line
...
}
Finally, you can use inside your useEffect hook the two states and map them to only get the selected ids:
useEffect(() => {
// ... other logic you had
if(selectionDishes?.length && selectionMenus?.length){
const data = {
date: info.date,
id: selectionDishes.map(d => d.dishId),
idMenus: selectionMenus.map(m => m.menuId)
};
}
}, [info, user, selectionMenus, selectionDishes]);
react-select has options to format the component:
getOptionLabel: option => string => used to format the label or how to present the options in the UI,
getOptionValue: option => any => used to tell the component what's the actual value of each option, here you can return just the id
isOptionSelected: option => boolean => used to know what option is currently selected
onChange: option => void => do whatever you want after the input state has changed
value => any => if you customize the above functions you may want to handle manually the value
Hope it helps you
What I would like to happen is when displayBtn() is clicked for the items in localStorage to display.
In useEffect() there is localStorage.setItem("localValue", JSON.stringify(myLeads)) MyLeads is an array which holds leads const const [myLeads, setMyLeads] = useState([]); myLeads state is changed when the saveBtn() is clicked setMyLeads((prev) => [...prev, leadValue.inputVal]);
In DevTools > Applications, localStorage is being updated but when the page is refreshed localStorage is empty []. How do you make localStorage persist state after refresh? I came across this article and have applied the logic but it hasn't solved the issue. I know it is something I have done incorrectly.
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const localStoredValue = JSON.parse(localStorage.getItem("localValue")) ;
const [localItems] = useState(localStoredValue || []);
useEffect(() => {
localStorage.setItem("localValue", JSON.stringify(myLeads));
}, [myLeads]);
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
// setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true);
};
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
);
}
export default App;```
You've fallen into a classic React Hooks trap - because using useState() is so easy, you're actually overusing it.
If localStorage is your storage mechanism, then you don't need useState() for that AT ALL. You'll end up having a fight at some point between your two sources about what is "the right state".
All you need for your use-case is something to hold the text that feeds your controlled input component (I've called it leadText), and something to hold your display boolean:
const [leadText, setLeadText] = useState('')
const [display, setDisplay] = useState(false)
const localStoredValues = JSON.parse(window.localStorage.getItem('localValue') || '[]')
const handleChange = (event) => {
const { name, value } = event.target
setLeadText(value)
}
const saveBtn = () => {
const updatedArray = [...localStoredValues, leadText]
localStorage.setItem('localValue', JSON.stringify(updatedArray))
setDisplay(false)
}
const displayBtn = () => {
setDisplay(true)
}
const displayLocalItems = localStoredValues.map((item) => {
return <li key={item}>{item}</li>
})
return (
<main>
<input name="inputVal" value={leadText} type="text" onChange={handleChange} required />
<button onClick={saveBtn}> Save </button>
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
)
I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.
I am having a slight issue with my react code. I want the data to be completely fetched before rendering. However, I have tried various ways such as 'setGroupLoaded to true' after the async call, but it is still not working. When the component first mounts, 'groupLoaded == false and myGroup == [],', then it goes to the next conditional statement where 'groupLoaded == true' but the myGroup [] is still empty. I was expecting the myGroup [] to be filled with data since groupLoaded is true. Please I need help with it.
function CreateGroup({ currentUser }) {
const [name, setName] = useState("");
const [myGroup, setMyGroup] = useState([]);
const [groupAdded, setGroupAdded] = useState(false);
const [groupLoaded, setGroupLoaded] = useState(false);
const handleChange = (e) => {
const { value, name } = e.target;
setName({
[name]: value,
});
};
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
useEffect(() => {
let groupId = myGroup.length ? myGroup[0].id : "";
let groupName = myGroup.length ? myGroup[0].groupName : "";
if (myGroup.length) {
JoinGroup(currentUser, groupId, groupName);
setTimeout(() => fetchGroupMembers(), 3000);
}
}, [myGroup]);
let itemsToRender;
if (groupLoaded && myGroup.length) {
itemsToRender = myGroup.map((item) => {
return <Group key={item.id} item={item} deleteGroup={deleteGroup} />;
});
} else if (groupLoaded && myGroup.length === 0) {
itemsToRender = (
<div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="exampleInputTitle">Group Name</label>
<input
type="text"
className="form-control"
name="name"
id="name"
aria-describedby="TitleHelp"
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Add group{" "}
</button>
</form>
</div>
);
}
return (
<div>
<h3>{currentUser ? currentUser.displayName : ""}</h3>
{itemsToRender}
</div>
);
}
The problem is here:
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
When you call setGroupLoaded(true), the first call to setMyGroup(groupArr) hasn't happened yet because fetchMyGroup(currentUser) is asynchronous. If you've never done this before, I highly recommend putting in some logging statements, to see the order in which is executes:
useEffect(() => {
const fetchGroupCreated = async () => {
console.log("Got data")
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
console.log("Before starting to load")
fetchGroupCreated();
setGroupLoaded(true);
console.log("After starting to load")
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
The output will be:
Before starting to load
After starting to load
Got data
This is probably not the order you expected, but explains exactly why you get groupLoaded == true, but the myGroup is still empty.
The solution (as Nick commented) is to move the setGroupLoaded(true) into the callback, so that it runs after the data is retrieved:
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
setGroupLoaded(true);
};
fetchGroupCreated();
An alternative approach may be to await the call to fetchGroupCreated(). I'm not sure if it'll work, but if it does it'll be simpler:
await fetchGroupCreated();
I have the following code:
import React, { useState, useEffect } from "react"
function callSearchApi(userName: string, searchOptions: SearchOptions, searchQuery: string): Promise<SearchResult>{
return new Promise((resolve, reject) => {
const searchResult =
searchOptions.fooOption
? ["Foo 1", "Foo 2", "Foo 3"]
: ["Bar 1", "Bar 2"]
setTimeout(()=>resolve(searchResult), 3000)
})
}
type SearchOptions = {
fooOption: boolean
}
type SearchResult = string[]
export type SearchPageProps = {
userName: string
}
export function SearchPage(props: SearchPageProps) {
const [isSearching, setIsSearching] = useState<boolean>(false)
const [searchResult, setSearchResult] = useState<SearchResult>([])
const [searchOptions, setSearchOptions] = useState<SearchOptions>({fooOption: false})
const [searchQuery, setSearchQuery] = useState<string>("")
const [lastSearchButtonClickTimestamp, setLastSearchButtonClickTimestamp] = useState<number>(Date.now())
// ####################
useEffect(() => {
setIsSearching(true)
setSearchResult([])
const doSearch = () => callSearchApi(props.userName, searchOptions, searchQuery)
doSearch().then(newSearchResult => {
setSearchResult(newSearchResult)
setIsSearching(false)
}).catch(error => {
console.log(error)
setIsSearching(false)
})
}, [lastSearchButtonClickTimestamp])
// ####################
const handleSearchButtonClick = () => {
setLastSearchButtonClickTimestamp(Date.now())
}
return (
<div>
<div>
<label>
<input
type="checkbox"
checked={searchOptions.fooOption}
onChange={ev => setSearchOptions({fooOption: ev.target.checked})}
/>
Foo Option
</label>
</div>
<div>
<input
type="text"
value={searchQuery}
placeholder="Search Query"
onChange={ev => setSearchQuery(ev.target.value)}
/>
</div>
<div>
<button onClick={handleSearchButtonClick} disabled={isSearching}>
{isSearching ? "searching..." : "Search"}
</button>
</div>
<hr/>
<div>
<label>Search Result: </label>
<input
type="text"
readOnly={true}
value={searchResult}
/>
</div>
</div>
)
}
export default SearchPage
also see this Codesandbox.
The code works fine. I can change the search query in the text field and click the option checkbox. Once, I am ready, I can click the "Search" button and only then the side effect occurs by fetching the data.
Now, the problem is that the compiler complains about:
React Hook useEffect has missing dependencies: 'props.user.loginName', 'searchFilter', and 'searchQuery'. Either include them or remove the dependency array. [react-hooks/exhaustive-deps]
However, if I add props.user.loginName, searchFilter and searchQuery to the dependency list, then the side effect is triggered whenever I click the checkbox or type a single character in the text field.
I do understand the concept of hook dependencies, but I don't know how to first enter some data and only with a button click trigger the side effect.
What is the best practice for this? I have read both https://reactjs.org/docs/hooks-effect.html and https://www.robinwieruch.de/react-hooks-fetch-data but couldn't find any example concerning my question.
Update 1:
I have also come up with this solution which looks like:
type DoSearch = {
call: ()=>Promise<SearchResult>
}
export function SearchPage(props: SearchPageProps) {
const [isSearching, setIsSearching] = useState<boolean>(false)
const [searchResult, setSearchResult] = useState<SearchResult>([])
const [searchOptions, setSearchOptions] = useState<SearchOptions>({fooOption: false})
const [searchQuery, setSearchQuery] = useState<string>("")
const [doSearch, setDoSearch] = useState<DoSearch>()
// ####################
useEffect(() => {
if(doSearch !==undefined){
setIsSearching(true)
setSearchResult([])
doSearch.call().then(newSearchResult => {
setSearchResult(newSearchResult)
setIsSearching(false)
}).catch(error => {
console.log(error)
setIsSearching(false)
})
}
}, [doSearch])
// ####################
const handleSearchButtonClick = () => {
setDoSearch({call: () => callSearchApi(props.userName, searchOptions, searchQuery)})
}
return (<div>...</div>)
}
Now the actual function is the only dependency which works fine and the compiler is happy, too.
However, what I do not like, is that fact that I need that wrapper object with the call property.
If I want to pass an arrow function directly to the state, this does not work as expected, e.g.:
const [doSearch, setDoSearch] = useState<()=>Promise<SearchResult>>()
...
setDoSearch(() => callSearchApi(props.userName, searchOptions, searchQuery))
The doSearch is not set to the arrow function, but callSearchApi is executed straight away. Does anybody know why?
You could remove setIsSearching(true) from your effect, and set it apart when you click your button.
const handleSearchButtonClick = () => {
setLastSearchButtonClickTimestamp(Date.now())
setIsSearching(true);
}
Then, you can modify your useEffect statement like this:
useEffect(() => {
if(!isSearching) {
return false;
}
setSearchResult([])
const doSearch = () => callSearchApi(props.userName, searchOptions, searchQuery)
doSearch().then(newSearchResult => {
setSearchResult(newSearchResult)
setIsSearching(false)
}).catch(error => {
console.log(error)
setIsSearching(false)
})
}, [allYourSuggestedDependencies]) // add all the suggested dependencies
This will accomplish what you are looking for. Another way would be just disabling the react-hooks/exhaustive-deps rule.
If you just need to trigger the fetch only when the button is clicked, I'd just use a function.
useEffect is useful for instance, when you have a list of filters (toggles), and you want to make a fetch every time you toggle one filter (imagine an e-commerce). This is a naive example, but it makes the point:
useEffect(() => {
fetchProducts(filters);
}, [filters])
That is how useEffect supposed to be.
props.userName make sense to be in the dependency list, cuz we definitely want to fetch new data when userName is changed.
searchOptions and searchQuery when you have this case, it's better to use reducer, so you just need to dispatch action ==> searchOptions and searchQuery won't be inside userEffect. This article from Dan Abramov provides deep explanation and they simple example implementing it
I quickly convert your example using useReducer, please have a look
import React, { useState, useEffect, useReducer } from "react";
function callSearchApi(
userName: string,
searchOptions: SearchOptions,
searchQuery: string
): Promise<SearchResult> {
return new Promise((resolve, reject) => {
const searchResult = searchOptions.fooOption
? ["Foo 1", "Foo 2", "Foo 3"]
: ["Bar 1", "Bar 2"];
setTimeout(() => resolve(searchResult), 3000);
});
}
const initialState = {
searchOptions: { fooOption: false },
searchQuery: "",
startSearch: false, // can replace searching
searchResult: []
};
const reducer = (state: any, action: any) => {
switch (action.type) {
case "SEARCH_START":
return { ...state, startSearch: true, setSearchResult: [] }; //setSearchResult: [] base on your example
case "SEARCH_SUCCESS":
return { ...state, setSearchResult: action.data, startSearch: false };
case "SEARCH_FAIL":
return { ...state, startSearch: false };
case "UPDATE_SEARCH_OPTION":
return { ...state, searchOptions: { fooOption: action.data } };
case "UPDATE_SEARCH_QUERY":
return { ...state, searchQuery: action.data };
default:
return state;
}
};
function SearchPage(props: SearchPageProps) {
const [
lastSearchButtonClickTimestamp,
setLastSearchButtonClickTimestamp
] = useState<number>(Date.now());
const [state, dispatch] = useReducer(reducer, initialState);
const { searchOptions, startSearch, searchQuery, searchResult } = state;
// ####################
useEffect(() => {
dispatch({ type: "SEARCH_START" });
}, [lastSearchButtonClickTimestamp]);
// ####################
const handleSearchButtonClick = () => {
setLastSearchButtonClickTimestamp(Date.now());
};
if (startSearch) {
callSearchApi(props.userName, searchOptions, searchQuery)
.then(newSearchResult => {
dispatch({ type: "SEARCH_SUCCESS", data: newSearchResult });
})
.catch(error => {
console.log(error);
dispatch({ type: "SEARCH_FAIL" });
});
}
return (
<div>
<div>
<label>
<input
type="checkbox"
checked={searchOptions.fooOption}
onChange={ev =>
dispatch({
type: "UPDATE_SEARCH_OPTION",
data: ev.target.checked
})
}
/>
Foo Option
</label>
</div>
<div>
<input
type="text"
value={searchQuery}
placeholder="Search Query"
onChange={ev =>
dispatch({ type: "UPDATE_SEARCH_QUERY", data: ev.target.value })
}
/>
</div>
<div>
<button onClick={handleSearchButtonClick} disabled={startSearch}>
{startSearch ? "searching..." : "Search"}
</button>
</div>
<hr />
<div>
<label>Search Result: </label>
<input type="text" readOnly={true} value={searchResult} />
</div>
</div>
);
}
export default SearchPage;
Codesandbox for that
Side effects are not meant for that. But if you want to execute the useEffect when there is change in variable, you can put that in dependency array.
Example
function effect() {
let [n, setN] = useState('')
useEffect(() => {
//some api need to call when 'n' value updated everytime.
}, [n])
//update the N variable with ur requirement
const updateN = (val) => {
setN(val)
}
}
Hope this helps