I have the following code https://codesandbox.io/s/async-http-u9zsg
What I am trying to achieve is to reset the value of the second select box every time I switch movies.
I tried a bunch of different things without any luck. I'm not asking to write me the code, I just want a push in the right direction am I missing something, is my approach completely wrong?
thanks in advance
The code :
import React, { useState, useEffect } from "react";
import fetch from "node-fetch";
const query = `{
allFilms{
films{
title
characterConnection{
characters{
name
species{
name
}
homeworld{
name
}
}
}
}
}
}`;
function App() {
const [state, setState] = useState({
loading: true,
appliedFilters: {},
species: []
});
useEffect(() => {
const fetchMovieData = () => {
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query })
};
return fetch(
"https://swapi-graphql.netlify.com/.netlify/functions/index",
options
)
.then(res => res.json())
.then(res =>
setState({ ...state, data: res.data.allFilms.films, loading: false })
);
};
fetchMovieData();
}, []);
if (state.loading) {
return <div>Loading...</div>;
}
/*work from here */
const getSpeciesOptions = () => {
if (state.appliedFilters.movie === undefined) {
return <option>Select a movie first</option>;
}
const currentMovie = state.data.filter(
movie => movie.title === state.appliedFilters.movie
);
const characters = currentMovie[0].characterConnection.characters;
const speciesList = characters.map(char =>
char.species === null ? "unknown" : char.species.name
);
return [...new Set(speciesList)].map(specie => <option>{specie}</option>);
};
const handleFilterChange = e => {
setState({
...state,
appliedFilters: {
...state.appliedFilters,
[e.target.name]: e.target.value
}
});
};
console.log(state);
const removeFilters = () => setState({ ...state, appliedFilters: {} });
const movieOptions = () =>
state.data.map(movie => <option>{movie.title}</option>);
return (
<div className="App">
<form>
<select
name="movie"
onChange={e =>
e.target.value !== "default"
? handleFilterChange(e)
: removeFilters()
}
>
<option value="default">Please select movie</option>
{movieOptions()};
</select>
<select name="species">
<option value="default">Please select a species</option>
{getSpeciesOptions()}
</select>
</form>
</div>
);
}
export default App;
A better approach is to fully control the value of the second select as well. So we add the value of it to the state as well:
const [state, setState] = useState({
loading: true,
appliedFilters: {},
species: [],
selected: "default"
});
Update the second control to read the value from state and also manipulate the state on change:
<select
value={state.selected}
name="species"
onChange={e => {
setState({ ...state, selected: e.target.value });
}}
>
<option value="default">Please select a species</option>
{getSpeciesOptions()}
</select>
And finally also manipulate the callback of the first to also reset the value:
const handleFilterChange = e => {
setState({
...state,
appliedFilters: {
...state.appliedFilters,
[e.target.name]: e.target.value,
},
selected: "default"
});
};
https://codesandbox.io/s/hardcore-nash-vtyx7 working example
All you need to do is to create a ref for the second select control.
let ref = React.createRef();
...
<select ref={ref}name="species">
<option value="default">Please select a species</option>
{getSpeciesOptions()}
</select>
</form>
Then in the callback of the first select do this:
const handleFilterChange = e => {
setState(...);
ref.current.value = 'default';
};
With the ref to the elememt you can now manipulate the value.
Actually, you know ReactJS exactly, but your approach causes you are complicated now. For such cases just keep the state of second select and after changing the first select reset the second one.
<select name="species" onChange={handleSpeciesChanges}>
I mean to write a state handler for the second select and then in the state handler of first one just reset the second.
Related
I have one issue with fetching the data using the useQuery. Please have a look at the code below:
FetchUser hook:
const useFetchUsers = ({ selectedSchool, selectedYear }) => {
return useQuery(['Users', selectedSchool, selectedYear], async () => {
const URL = getSchoolURL({ selectedSchool, selectedYear })
const response = await fetch(URL)
const data = await response.json()
return {
count: data.count,
users: data.users
}
}, {
enabled: !!(selectedSchool && selectedYear),
onSuccess: () => {
console.log('success')
},
onError: () => {
console.log('errors')
}
})
}
Users component:
const Users = () => {
const {
isLoading,
data,
isError
} = useFetchUsers({ selectedSchool: '', selectedYear: '' })
const updateUsersData = ({ selectedSchool, selectedYear }) => {
// Here, I have to write logic to fetch Users data as per
// selected organization and selectedYear
}
return (
<div className='app'>
<Schools updateUsersData={updateUsersData}/>
{
/**
* Rendering components
* <Component-1/>
* <Component-2/>
* <Component-3/>
* <Component-4/>
* <Component-5/>
*
*/
}
</div>
)
}
School component:
const Schools = () => {
const [school, setSchool] = useState('')
const handleChange = (e) => {
const selectedSchool = e.target.value
setSchool(selectedSchool)
if (selectedSchool) {
// we have other logic to select selected Year
// but here sake for the example, I'm using this value
// hardcoded
updateUsersData({ selectedSchool, selectedYear: '2021' })
}
}
return (
<select
value={school}
onChange={handleChange}
name='school'
id='school'>
<option
value={''}
key={'Select School'}>
Select School
</option>
<option value={'school-1'}>school-1</option>
<option value={'school-2'}>school-2</option>
<option value={'school-3'}>school-3</option>
<option value={'school-4'}>school-4</option>
<option value={'school-5'}>school-5</option>
</select>
)
}
Some notes:
School component: Here, we are rendering the school names and when the user selects any school data, we are calling updateUsersData method and from this method, we have to call again the useFetchUsers hook with updated params but it is not working.
I don't want to take additional states i.e selectedSchool and selectedYear on Users component because of unnecessary component rendering.
Problem: How to again call useFetchUsers hook with updated params from updateUsersData method?
What I want to do
When state changes, I would like to fetch data from API and set it to state.
Problem
When I load a page, componentDidUpdate is automatically emitted although anything doesn't change and this is initial render.
I am beginner to React.
I would like to get data from API when I input some data and state changes.
However, when I load a page I got an error that cannot property map of undefined.
Apparently, componentDidUpdate is called for some reasons when the page is first rendered.
In fact, I compared prevState.line to this.state.line, and then I found prevState.line undefined.
I mean, componentDidUpdate is emitted before "" that is an initial value of this.state.line is set to this.state.line.
I just would like to prevent componentDidUpdate from being initially called.
If you know a way to do that, I would like you to tell me that and why it is happening.
Thank you very much.
=========== ============ ============
My code is like this.
class User_Add_PickUp_Form extends Component {
constructor(props) {
super(props);
this.state = {
owner: this.props.loginUser,
lines: '',
stations: '',
textInput: '',
allLines: '',
allStations: '',
};
this.handleChange = this.handleChange.bind(this);
}
spreadAndSetState = (key, value) => {
this.setState({ ...this.state, [key]:value });
};
componentDidMount() {
console.log('Original is ' + this.state.lines)
axios
.get('http://express.heartrails.com/api/json?method=getLines&prefecture=TOKYO')
.then((res) => {
console.log(res.data)
this.setState({ allLines: res.data.response.line })
})
.catch((err) => console.log(err));
}
componentDidUpdate(prevState){
console.log(prevState)
console.log(this.state)
if(prevState.lines != this.state.lines){
axios.get('http://express.heartrails.com/api/json?method=getStations&line=' + this.state.lines)
.then((res)=> {
//
// res.data.response.stations is going to be an array
//
res.data.response.station.map((station) => {
this.spreadAndSetState(this.state.allStations, station.name)
})
})
}
}
handleChange = (e) => {
const name = e.target.name;
const value = e.target.value;
this.spreadAndSetState(name,value)
};
render() {
const { owner, lines, stations, textInput, allLines, allStations } = this.state;
if (allLines === '') {
return <CircularProgress />;
} else {
return (
<div>
<h2>Add Pickup Places</h2>
<select name="lines" onChange={this.handleChange}>
<option value="">Choose a line</option>
{allLines.map((line) => {
return <option value={line}>{line}</option>;
})}
</select>
<select name="lines" onChange={this.handleChange}>
<option value="">Choose a station</option>
{allLines.map((line) => {
return <option value={line}>{line}</option>;
})}
</select>
</div>
);
}
}
}
I have a react program that displays a table based on values of a dropdown. I want the program to display the table by default based on the first value in the dropdown.
The first value in the dropdown is very different from the value made as default, and the dropdown values are always changing. So the data can be misleading when it loads for the first time.
here is a snippet of my code with a little description within. Thanks.
const CompletenessApp = () => {
const [completeness, setcompleteness] = useState([]);
const [loading, setloading] = useState(false);
const [valueSelected, setValueSelected] = useState({value:'all'});
const [periodSelected, setPeriodSelected] = useState({value:'20200702'}); // the default value that is used to filter the data.
const valid = [
{id:"all", variable:"all"},{id:"true", variable:"true"}, {id:"false", variable:"false"}, {id:"null", variable:"null"},
];
useEffect(() => {
const fetchData = async () => {
try{
const res = await axios.get('http://localhost:8000/api/completeness/');
setcompleteness(res.data);
setloading(true);
} catch (e) {
console.log(e)
}
}
fetchData();
}, []);
// when the page loads for the first time it filters the data based on the period value set in the state. I want the data to be filtered based on the first value in the dropdown instead, the first value in the dropdown is different from the default value set.
const handlePeriodChange = e => {
setPeriodSelected({value : e.target.value})
}
const handleValueChange = e => {
let boolvalue = Boolean
e.target.value === 'true'? boolvalue = true:
e.target.value === 'false'? boolvalue = false:
e.target.value === 'all'? boolvalue = 'all':
boolvalue=null
setValueSelected({value : boolvalue})
}
//filtered data to be displayed
const filteredCompleteness = completeness.filter(
completedata=> (completedata.period === periodSelected.value)
&&(valueSelected.value !== 'all'?completedata.complete === valueSelected.value:{}))
return(
<div>
<div className="selection-row">
<div className="stats-columns">
<div className="stats-label">Period</div>
//the relevant dropdown is here
<select onChange={e => handlePeriodChange(e)}>
{Array.from(new Set(completeness.map(obj => obj.period)))
.sort().reverse()
.map(period => {
return <option value={period}>{period}</option>
})}
</select>
</div>
<div className="stats-columns">
<div className="stats-label">Value</div>
<select onChange={e => handleValueChange(e)}>
{valid.map((obj) => {
return <option value={obj.id}>{obj.variable}</option>
})
}
</select>
</div>
</div>
<hr></hr>
<div className="results-table">
//table is displayed here
</div>
</div>
);
}
export default CompletenessApp;
// above return
const options = Array.from(new Set(completeness.map((obj) => obj.period)))
.sort()
.reverse()
.map((period) => {
return {
value: period,
label: period,
};
});
// in jsx
<select defaultValue={options[0].value} onChange={e => handlePeriodChange(e)}>
{
options.map((obj) => {
return <option value={obj.value}>{obj.label}</option>;
})
}
</select>
Try this and let me know.
I am having a onChange function i was trying to update the array options by index wise and i had passed the index to the function.
Suppose if i am updating the options array index 0 value so only that value should be update rest should remain as it is.
Demo
Here is what i tried:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const handleOptionInputChange = (event, idx) => {
setPostData({
...postdata,
LEVEL: {
...postdata.LEVEL.options,
[event.target.name]: event.target.value
}
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
Suppose if i want to add the objects in another useState variable for all the updated options only, will this work?
const posting = {
"optionUpdates": [],
}
const [sentdata , setSentData] = useState(posting);
setSentData({
...sentdata,
optionUpdates: [{
...sentdata.optionUpdates,
displayOrder: event.target.value
}]
})
Basically, you need to spread properly, use callback approach to set state etc.
Change your handler to like this.
Working demo
const handleOptionInputChange = (event, idx) => {
const target = event.target; // with callback approach of state, you can't use event inside callback, so first extract the target from event.
setPostData(prev => ({ // prev state
...prev, // spread prev state
LEVEL: { //update Level object
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => { // you need to loop thru options and find the one which you need to update.
if (id === idx) {
return { ...item, [target.name]: target.value }; //spread all values and update only orderStatus
}
return item;
})
}
}));
};
Edit Added some comments to code and providing some explanation.
You were spreading postdata.LEVEL.options for LEVEL which is incorrect. For nested object you need to spread each level.
Apparently, your event.target.name is "orderStatus", so it will add an "orderStatus" key to your postData.
You might want to do something like this:
const handleOptionInputChange = (value, idx) => {
setPostData(oldValue => {
const options = oldValue.LEVEL.options;
options[idx].orderStatus = value;
return {
...oldValue,
LEVEL: {
...oldValue.LEVEL,
options
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e.target.value, idx)}
/>
);
})}
</div>
);
See this demo
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