Apollo React Why Can't Query inside UseEffect hook - javascript

I am just learning about Apollo-React but I couldn't make graphql request
This is how I do without Apollo
const Search = () => {
const [searchedText, setSearchedText] = React.useState('')
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
const debounceHandler = (searchedText) => debounce(() => {
sendQuery(`{search(str:"${searchedText}") {name}}`).then(({search}) => {
if (!search) return
setSuggestions(search)
})
}, 500)
const handleInputChange = async (e) => {
if(e.key === 'Enter') {
const name = e.target.value
sendQuery(`{getPokemon(str:"${name}"){name, image}}`).then(({getPokemon}) => {
setSelected(getPokemon)
})
}
debounceHandler(searchedText)()
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onChange={(e) => setSearchedText(e.target.value)} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
)) }
</div>
</div>
)
}
Now without my own sendQuery function, I want to use Apollo's useQuery hook.
const GET_POKEMON = gql`
query getPokemon ($str: String!) {
getPokemon(str: $str) {
name
image
}
}
`;
const SEARCH = gql `
query search($str: String!) {
search(str:$str) {
name
}
}
`;
These are my queries and results correctly on the playground. Now I write Search function again. I say whenever searchedText changes (WHen user types in), query Search and set the returning data as suggestions. Whenever user hits enter, I want to query the Pokemon from backend and set it as selected.
const Search = () => {
const [searchedText, setSearchedText] = React.useState(null)
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
React.useEffect(() => {
const { data } = useQuery(SEARCH, {
variables: { "str": searchedText },
pollInterval: 500,
});
if (data) {
setSuggestions(data)
}
}, [searchedText])
const fetchAndSelect = name => {
setSearchedText('')
const { pokemon } = useQuery(GET_POKEMON, {
variables: {
"str": name
}
})
setSelected(pokemon)
}
const handleInputChange = (e) => {
const name = e.target.value
if(e.key === 'Enter') {
return fetchAndSelect(name)
}
setSearchedText(name)
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
))}
</div>
</div>
)
}
But this gives Invalid hook call error. If I don't make the query inside useEffect ( I am not sure what is wrong with this?) this time I get Rendered more hooks than during the previous render. error. I am not sure what I am doing wrong?
EDIT
Based on answer I edit the code like following
const Search = () => {
const [searchedText, setSearchedText] = React.useState(null)
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
const debouncedSearch = debounce(searchedText, 1000) // Trying to debounce the searched text
const [searchPokemons, { data }] = useLazyQuery(SEARCH);
const [getPokemon, { pokemon }] = useLazyQuery(GET_POKEMON)
React.useEffect(() => {
if (!searchedText) return
setSelected(null)
searchPokemons({ variables: { str: searchedText }})
if (data) {
console.log(data)
setSuggestions(data)
}
}, [debouncedSearch])
const fetchAndSelect = name => {
setSearchedText('')
getPokemon({variables: {str: name}})
if (pokemon) {
setSelected(pokemon)
}
}
const handleInputChange = (e) => {
const name = e.target.value
if(e.key === 'Enter') {
return fetchAndSelect(name)
}
setSearchedText(name)
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
))}
</div>
</div>
)
}
I am unable to type anything on the input. It is fetching like crazy. Please help

You should use useLazyQuery Hook in this case. It is very useful for things that happen at an unknown point in time, such as in response to a user's search operation.
How about If you call use your hook on the top of your function and just call it inside the useEffect hook.
const [search, { data }] = useLazyQuery(SEARCH, {
variables: { "str": searchedText },
pollInterval: 500,
});
React.useEffect(() => {
if (searchedText)
search() // Function for executing the query
if (data)
setSuggestions(data)
}, [searchedText])
As you see, useLazyQuery handles fetching data in a synchronous way without any promises.

Related

Best way to update the local state of a component in react on some external event

Here's the scenario. I have a app and a search component.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
It's all good until this point. But I want to know what's the best way to change the search text on an external event. So far, the best approach I've found is using events.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
useEffect(() => {
onSomeExternalEvent((newSearch) => {
setSearch(newSearch);
EventBus.emit("updateSearch", newSearch);
});
}, []);
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
useEffect(() => {
EventBus.subscribe("updateSearch", (newSearch) => {
setLocalSearch(newSearch);
});
}, []);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
Is there a better (correct) way of doing this?

i have to click on the button twice to post data to api in react

I have console.log(ed) the values while executing and what happens is on the first click, inputValue is sent with a null string to api, then on the next click the inputValue with string is sent to api. I have already changed the value of inputValue using the setter function in input tag with onChange function and then i have called the api so How do i fix it so it sends it on the first click.
const InputEmail = () => {
const navigate = useNavigate()
const [inputValue, setInputValue] = useState('')
const [apiData, setApiData] = useState('')
const [isError, setIsError] = useState(false)
// useEffect(() => {
// //API()
// }, [])
const API = () => {
console.log(inputValue)
axios
.post(url, {
email: inputValue
})
.then((response) => {
setApiData(response.data)
})
console.log(apiData.is_active)
}
const handleSubmit = () => {
API()
if(apiData.is_active) {
localStorage.setItem('email',inputValue)
navigate("/assessment")
} else {
setIsError(true)
}
}
return (
<div className='main'>
<FormControl>
<h2 className='text'>Registered Email address</h2>
<Input id='email' type='email' value={inputValue} onChange={e => setInputValue(e.target.value)}/>
{
isError ? <FormHelperText color='red'>Email not found. Please enter registered email</FormHelperText> : null
}
<Button
mt={4}
colorScheme='teal'
type='submit'
onClick={handleSubmit}
>
Submit
</Button>
</FormControl>
</div>
)
}
You must wait for your axios to fetch data from the url before making a handle. It will work if you await untill your async API() functions brings data.
const API = () => {
return axios.post(url, {
email: inputValue,
});
};
const handleSubmit = async () => {
const response = await API();
if (response.data.is_active) {
localStorage.setItem("email", inputValue);
navigate("/assessment");
} else {
setIsError(true);
}
};

React.js: filtering API object with 2 search bars

I am fetching an API data set and filtering that data with a search bar to locate by first or last name. I also have an input field that allows you to add "tags" to the data set that I am mapping through. I am trying to add a second search bar to filter the original data by the unique tags as well, but can not figure out how to incorporate that information into the filter.
export default function Home() {
const [students, setStudents] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [showTests, setShowTests] = useState({});
const [tagSearch, setTagSearch] = useState("");
const [tags, setTags] = useState([]);
useEffect(() => {
const getData = async () => {
try {
const response = await axios.get(
<!-- API -->
);
setStudents(response.data);
setError(null);
} catch (err) {
setError(err.message);
setStudents(null);
} finally {
setLoading(false);
}
};
getData();
}, []);
return (
<div className="home-main">
<Search setSearch={setSearch} />
<TagSearch setTagSearch={setTagSearch} />
{loading && <div>Loading, please wait ...</div>}
{error && (
<div>{`An Error has occurred. - ${error}`}</div>
)}
<div className="students">
<Fragment>
{
students
&&
students.students.filter((val) => {
if(search === '' || tagSearch === '') {
return val
} else if(val.firstName.toLowerCase().includes(search.toLowerCase())
|| val.lastName.toLowerCase().includes(search.toLowerCase())
|| tags.text.toLowerCase().includes(tagSearch.toLowerCase()) ){
return val
}
}).map(({val}) => (
<!-- additional info -->
<div className="tags">
<Tags setTags={setTags} />
</div>
</div>
</div>
))
}
</Fragment>
</div>
</div>
);
}
This is where the "tag" state is coming from...
export default function Tags({setTags}) {
const [inputText, setInputText] = useState('');
const [tiles, setTiles] = useState([]);
const inputTextHandler = (e) => {
setInputText(e.target.value);
};
const submitTagHandler = () => {
setTiles([
...tiles, {text: inputText, id: Math.floor(Math.random() * 1000000)}
]);
setTags([
...tiles, {text: inputText}
])
setInputText('');
};
return (
<div className="tags-main">
<div className="tiles-contain">
{
tiles.map((obj) => (
<Tiles key={obj.id} text={obj.text} id={obj.id} tiles={tiles} setTiles={setTiles} />
))
}
</div>
<input value={inputText} onChange={inputTextHandler} onKeyPress={(e) => {
if(e.key === 'Enter') {
if(inputText !== "") {
submitTagHandler();
} else {
alert("Please enter a tag")
}
};
}} placeholder='Add Tag Here' type="text" />
</div>
);
}
It works without the tag state added to the filter. After adding the tag logic neither search bar works. How can I add the array of tags to the filter dependency to sort by first or last name and tags?
I'm pretty sure you were getting an error "cannot read toLowerCase of undefined"
You probably wanted to do something like this
tags.some(tag => tag.text.toLowerCase() === tagSearch.toLowerCase())
or
tags.map(tag => tag.text.toLowerCase()).includes(tagSearch.toLowerCase())

Can't display a new filtered array in React

I'm trying to display a new, filtered array that hides the rest of the elements and leaves only the ones I type in the search bar. The const newFilter works in the console but doesn't read in the return. I tried placing the const in other places but it's beyond the scope..
import React, { useState } from "react";
function SearchBar({ placeholder }) {
const [filteredData, setFilteredData] = useState([]);
const [wordEntered, setWordEntered] = useState("");
const [pokemonData, setPokemonData] = React.useState({});
React.useEffect(() => {
fetch(
"https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json"
)
.then((res) => res.json())
.then((data) => setPokemonData(data.pokemon));
}, []);
const allPokes = pokemonData;
const pokemons = Object.values(allPokes);
const handleFilter = (event) => {
const searchWord = event.target.value;
setWordEntered(searchWord);
const newFilter = pokemons.filter((value) => {
return value.name.toLowerCase().includes(searchWord.toLowerCase());
});
if (searchWord === "") {
setFilteredData([]);
} else {
setFilteredData(newFilter);
}
console.log(newFilter);
};
let checkConsole = () => alert("Check the console :)");
return (
<div className="search-div">
<p className="search-text">Name or Number</p>
<div className="search">
<div className="searchInputs">
<input
type="text"
placeholder={placeholder}
value={wordEntered}
onChange={handleFilter}
/>
</div>
</div>
</div>
);
}
export default SearchBar;
In the given snippet, there is no filtered data displaying logic inside the return
import React, { useState } from "react";
export default function SearchBar({ placeholder }) {
const [filteredData, setFilteredData] = useState([]);
const [wordEntered, setWordEntered] = useState("");
const [pokemonData, setPokemonData] = React.useState({});
React.useEffect(() => {
fetch(
"https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json"
)
.then((res) => res.json())
.then((data) => setPokemonData(data.pokemon));
}, []);
React.useEffect(() => {
console.log(filteredData);
});
const allPokes = pokemonData;
const pokemons = Object.values(allPokes);
const handleFilter = (event) => {
const searchWord = event.target.value;
setWordEntered(searchWord);
const newFilter = pokemons.filter((value) => {
return value.name.toLowerCase().includes(searchWord.toLowerCase());
});
if (searchWord === "") {
setFilteredData([]);
} else {
setFilteredData(newFilter);
}
console.log(newFilter);
};
let checkConsole = () => alert("Check the console :)");
return (
<div className="search-div">
<p className="search-text">Name or Number</p>
<div className="search">
<div className="searchInputs">
<input
type="text"
placeholder={placeholder}
value={wordEntered}
onChange={handleFilter}
/>
</div>
/* Add filteredData logic in the return */
{filteredData.map((each) => (
<p>{each.name}</p>
))}
</div>
</div>
);
}

There's a bug with Search and Pagination in React

I'm building my site in React and I have created pagination and search. When I search for something on the site, it only works when after that I go to another page. I think this is due to the fact that Softwares and Pagination are in the same component.
Then I tried lifting-state-up, but I got an error: React Minified Error # 31.
Here's Pagination component:
const Paginator = ({
total, // Total records
startPage = 1,
totalPages = null,
onMovePage = null,
}) => {
...
return (
<>
<section id={styles.paginator}>
<Header/>
...
{range(1, totalPages+1).map(p => (
<PagItem key={p} handleClick={ () => {setCurrentPage(p); onMovePage && onMovePage({currentPage: p})} } title={p} name={p} />
))}
...
</section>
</>
);
};
Here's Softwares component:
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const fetchData = async ({ currentPage }) => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
useEffect(() => {
fetchData({ currentPage: 1 });
}, []);
return (
<>
{
valid &&
<section className={styles.softwares}>
<Header header={"new softwares"} />
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator totalPages={totalPages} total={total} onMovePage={fetchData} />
</section>
}
</>
);
};
SearchForm in Header component:
const Header = ({ handleChange, handleClick }) => {
return (
...
<SearchForm handleChange={handleChange} handleClick={handleClick} />
...
);
};
const SearchForm = ({ style, handleChange, handleClick }) => {
return (
<div style={style}>
<form>
<input
type="text"
onChange={handleChange}
/>
<SearchButton onClick={handleClick} />
<small>ENTER</small>
</form>
</div>
);
};
const SearchButton = ({onClick }) => {
return (
<button type="button" onClick={onClick}>
<FontAwesomeIcon icon={faSearch} />
</button>
);
};
And part of Search in App component:
const App = () => {
...
// Search
const [search, setSearch] = useState('');
const [shouldFetch, setShouldFetch] = useState(false);
const handleChange = (e) => {
setSearch(e.target.value);
}
useEffect(() => {
if (shouldFetch) {
(async () => {
const response = await fetch(`http://127.0.0.1:8000/api/software/?search=${search}`);
const data = await response.json();
setShouldFetch(false);
})()
}
}, [shouldFetch]);
const handleClick = () => setShouldFetch(true);
return (
<div className="App">
<Header handleChange={handleChange} handleClick={handleClick} />
...
<Switch>
<Route path="/" exact render={props => <Softwares {...props} search={search} />} />
</Switch>
{/* Actually I'd like to use Paginator here, but it
throws the error: React Minified Error # 31 */}
...
</div>
);
}
So, how can this be done?
The problem is your useEffect dependencies (or lack thereof).
Here's the relevant section of the code:
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const fetchData = async ({ currentPage }) => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
useEffect(() => {
fetchData({ currentPage: 1 });
}, []);
The empty dependency array means that you are running the effect that calls fetchData one time when the component mounts. Clicks in the Pagination component will call the fetchData function directly. Changes to search do not cause fetchData to re-run. The data depends on the search so search should be a dependency.
The fetchData function is fine in this component. The state that I would recommend lifting up is to lift the currentPage up from Pagination into Softwares. The onMovePage callback can just update the currentPage state. That way you can call fetchData only through your effect and run the effect whenever either search or currentPage changes.
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
// defining the function inside of the useEffect
// lets eslint exhaustive dependency checks work their magic
const fetchData = async () => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
// need to define and call in separate steps when using async functions
fetchData();
}, [currentPage, search]);
return (
...
<Paginator page={currentPage} totalPages={totalPages} total={total} onMovePage={setCurrentPage} />
...
);
};

Categories

Resources