I am currently learning React, and I am trying to build a small weatherapp to practice with apis, axios and react generally. I built an input component where it's duty is getting the data from the API, and I am holding the data in the useState hook and I want to use the data in the main App component? I am able to pass data from parent App component to input component if I take the functionality in the app component, but this time I start to have problems with input text rendering problems. Here is the code:
this is the input component where I search and get the data from the API, and I am trying to pass the weatherData into the main App component and render it there. How is it possible to achieve this?
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const key = process.env.REACT_APP_API_KEY;
function SearchLocation() {
const [text, textChange] = useState('');
const [weatherData, setWeatherData] = useState([]);
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
fetchData();
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
console.log(weatherData);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
></input>
</form>
</div>
);
}
export default SearchLocation;
EDIT:
After moving the states to main component and passing them to children as props I receive 3 errors, GET 400 error from the API, createError.js:16 Uncaught (in promise) Error: Request failed with status code 400 and textChange is not a function error. Here are how components look like. This is the input component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const key = process.env.REACT_APP_API_KEY;
function SearchLocation({ weatherData, setWeatherData, text, textChange }) {
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
fetchData();
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
console.log(weatherData);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
></input>
</form>
</div>
);
}
export default SearchLocation;
this is the parent app component:
import React from 'react';
import { useState } from 'react';
import './App.css';
import './index.css';
import SearchLocation from './components/Input';
function App() {
const [weatherData, setWeatherData] = useState([]);
const [text, textChange] = useState('');
return (
<div className="App">
<SearchLocation
setWeatherData={setWeatherData}
lastData={weatherData}
inputText={text}
/>
</div>
);
}
export default App;
You'll still need to store the state in the parent component. Pass the setter down as a prop. This is a React pattern called Lifting State Up.
Example:
const App = () => {
const [weatherData, setWeatherData] = useState([]);
...
return (
...
<SearchLocation setWeatherData={setWeatherData} />
...
);
};
...
function SearchLocation({ setWeatherData }) {
const [text, textChange] = useState('');
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
"https://api.weatherapi.com/v1/current.json",
{
params: {
key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
// Only request weather data if `text` is truthy
if (text) {
fetchData();
}
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
/>
</form>
</div>
);
}
There are two solutions to your problem:-
Firstly you can create the states const [text, textChange] = useState('');
const [weatherData, setWeatherData] = useState([]);, inside your parent component and pass text, textChange, weatherData, setWeatherData as props to your child component.
I would recommend the second way, i.e, implement redux for this and store text, and weatherData into your redux and try to access them from redux.
redux reference:- https://react-redux.js.org/introduction/getting-started
Related
Here is a very basic search by title query on a public API. In my actual app my API calls are under services.js files so I'm trying to do this where my API call is not inside the react component.
https://codesandbox.io/s/elastic-pond-pghylu?file=/src/App.js
import * as React from "react";
import axios from "axios";
// services.js
const fetchPhotos = async (query) => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/photos?title_like=${query}`
);
return data;
};
export default function App() {
const [photos, setPhotos] = React.useState([]);
const [searchTerm, setSearchTerm] = React.useState("");
const fetch = React.useCallback(async () => {
const data = await fetchPhotos(searchTerm);
setPhotos(data);
}, [searchTerm]);
React.useEffect(() => {
fetch();
}, [fetch]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div>
{photos?.map((photo) => (
<div>{JSON.stringify(photo.title)}</div>
))}
</div>
</div>
);
}
The problem with my code is this (too many api calls while im typing):
My attempt to fix this
I tried cancelToken. This cancels my previous request. I was able to implement this but in my actual API the get request is so fast that it still manages to finish the request. So I'm trying to do this without cancelToken.
I recently came across debouncing and it seems to do what I need i'm just struggling to get it to work
for example, I tried this: (I wrapped debounce onto my fetchPhotos function)
import {debounce} from 'lodash';
// services.js
const fetchPhotos = debounce(async (query) => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/photos?title_like=${query}`
);
return data;
}, 500);
however now fetchphotos returns undefined always?
You can make use of useCallback so that the debounced (fetchPhotos) will have same function across re-renders
import * as React from "react";
import axios from "axios";
import { debounce } from "lodash";
// services.js
export default function App() {
const [photos, setPhotos] = React.useState([]);
const [searchTerm, setSearchTerm] = React.useState("");
async function fetchData(searchTerm) {
const data = await axios.get(
`https://jsonplaceholder.typicode.com/photos?title_like=${searchTerm}`
);
setPhotos(data.data);
}
const debounced = React.useCallback(debounce(fetchData, 500), []);
React.useEffect(() => {
// for the first render load
fetchData("");
}, []);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
debounced(e.target.value, 1000);
}}
/>
<div>
{photos?.map((photo) => (
<div key={photo.id}>{JSON.stringify(photo.title)}</div>
))}
</div>
</div>
);
}
I want to do a movie search with the oMdb api using React Hooks.
The result is not as expected. I seem to break some React Hooks rule that I don't understand.
Here is the code.
HOOK TO SEARCH
The Hook inside of a store.
(If I use searchMovies('star wars') in a console.log I can see the result of star wars movies and series.)
import React, { useState, useEffect } from "react";
const useSearchMovies = (searchValue) => {
const API_KEY = "731e41f";
const URL = `http://www.omdbapi.com/?&apikey=${API_KEY}&s=${searchValue}`
// Manejador del estado
const [searchMovies, setSearchMovies] = useState([])
//Llamar y escuchar a la api
useEffect(() => {
fetch(URL)
.then(response => response.json())
.then(data => setSearchMovies(data.Search))
.catch((error) => {
console.Console.toString('Error', error)
})
}, []);
return searchMovies;
};
THE INPUT ON A SANDBOX
Here i have the input to search with a console log to see the result.
import React, { useState } from "react";
import searchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('')
const onChangeHandler = e =>{
setSearch(e.target.value)
console.log('Search result', searchMovies(search))
}
const handleInput =()=> {
console.log('valor del input', search)
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput()}>search</button>
</div>
</div>
)
}
export default Sandbox;
Issue
You are breaking the rules of hooks by conditionally calling your hook in a nested function, i.e. a callback handler.
import searchMovies from "../store/hooks/useSearchMovies";
...
const onChangeHandler = e => {
setSearch(e.target.value);
console.log('Search result', searchMovies(search)); // <-- calling hook in callback
}
Rules of Hooks
Only call hooks at the top level - Don’t call Hooks inside loops,
conditions, or nested functions.
Solution
If I understand your code and your use case you want to fetch/search only when the search button is clicked. For this I suggest a refactor of your useSearchMovies hook to instead return a search function with the appropriate parameters enclosed.
Example:
const useSearchMovies = () => {
const API_KEY = "XXXXXXX";
const searchMovies = (searchValue) => {
const URL = `https://www.omdbapi.com/?apikey=${API_KEY}&s=${searchValue}`;
return fetch(URL)
.then((response) => response.json())
.then((data) => data.Search)
.catch((error) => {
console.error("Error", error);
throw error;
});
};
return { searchMovies };
};
Usage:
import React, { useState } from "react";
import useSearchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('');
const [movies, setMovies] = useState([]);
const { searchMovies } = useSearchMovies();
const onChangeHandler = e => {
setSearch(e.target.value)
};
const handleInput = async () => {
console.log('valor del input', search);
try {
const movies = await searchMovies(search);
setMovies(movies);
} catch (error) {
// handle error/set any error state/etc...
}
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput}>search</button>
</div>
<ul>
{movies.map(({ Title }) => (
<li key={Title}>{Title}</li>
))}
</ul>
</div>
);
};
export default Sandbox;
After pushing a route in NextJS the path seems to be valid for a split of a second http://localhost:3000/search?query=abc and then changes to http://localhost:3000/?. Not sure why this is happening.
I have tried it with both import Router from 'next/router' and import { useRouter } from 'next/router'. Same problem for both import types.
Here's my component and I use the route.push once user submits a search form.
import React, { useEffect, useState } from "react";
import Router from 'next/router';
const SearchInput = () => {
const [searchValue, setSearchValue] = useState("");
const [isSearching, setIsSearching] = useState(false);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isSearching) {
Router.push({
pathname: "/search",
query: { query: searchValue },
});
setIsSearching(false);
}
}, [isSearching, searchValue]);
const handleSearch = () => {
if (searchValue) {
setIsSearching(true);
}
};
return (
<form onSubmit={handleSearch}>
<input
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder="Search"
/>
</form>
);
};
The default behavior of form submissions to refresh the browser and render a new HTML page.
You need to call e.preventDefault() inside handleSearch.
const handleSearch = (e) => {
e.preventDefault()
if (searchValue) {
setIsSearching(true);
}
};
My React App is crashing when fetching searchResults from API, I have checked the API urls wise search queries and it works perfectly however when i try to send input via React and display results it crashes and even freezes my PC. I dont understand whats going on here. I have fetched results from the API in React without search query and it works. So the API works when used via Curl and React app can fetch and display all the data but unable to display specific data. Below is my code:
function Search() {
const [data, setData] = React.useState([]);
const [searchTerm, setSearchTerm] = React.useState("");
const handleChange = e => {
setSearchTerm(e.target.value);
};
React.useEffect(() => {
if (searchTerm) {
getData(searchTerm);
}
});
const getData = (searchTerm) => {
axios.get("http://localhost:8000/SearchPost/?search="+searchTerm)
.then(res => (setData(res.data)))
}
return (
<div className="App">
<input
type="text"
placeholder="Search"
value={searchTerm}
onChange={handleChange}
/>
<ul>
{data.map(item => (
<li>{item.co_N}</li>
))}
</ul>
</div>
);
}
export default Search;
One solution is to "debounce" setting searchTerm to minimize the request to the API:
we're going to use lodash package particularly it's debounce method (doc here), and useCallback from Hooks API (doc here) :
import React, { useState, useCallback, useRef } from "react";
import _ from "lodash";
import axios from "axios";
import TextField from "#material-ui/core/TextField";
const SearchInputComponent = ({ label }) => {
const [value, setValue] = useState("");
const [data, setData] = useState([]);
const inputRef = useRef(null);
const debounceLoadData = useCallback(
_.debounce((value) => {
getData(value);
}, 500), // you can set a higher value if you want
[]
);
const getData = (name) => {
axios.get(`https://restcountries.eu/rest/v2/name/${name}`).then((res) => {
console.log(res);
setData(res.data);
});
};
const handleSearchFieldChange = (event) => {
const { value } = event.target;
setValue(value);
debounceLoadData(value);
};
return (
<>
<TextField
inputRef={inputRef}
id="searchField"
value={value}
label={"search"}
onChange={handleSearchFieldChange}
/>
{data &&
<ul>
{data.map(country=> (
<li key={country.alpha3Code}>{country.name}</li>
))
}
</ul>
}
</>
);
};
export default SearchInputComponent;
with this code the front end will wait 500 ms before fetching api with the search input value.
here a sandBox example.
Possible Feature: Make search field generic
If in the future you will need a search component you can make it generic with Context:
first create a context file named for example SearchInputContext.js and add:
SearchInputContext.js
import React, {
createContext,
useState
} from 'react';
export const SearchInputContext = createContext({});
export const SearchInputContextProvider = ({ children }) => {
const [value, setValue] = useState('');
return (
<SearchInputContext.Provider
value={{ searchValue: value, setSearchValue: setValue }}
>
{children}
</SearchInputContext.Provider>
);
};
Next create a generic searchField component named for example SearchInput.js and add in it :
SearchInput.js
import React, {
useState,
useCallback,
useRef,
useContext
} from 'react';
import _ from 'lodash';
import TextField from "#material-ui/core/TextField";
import { SearchInputContext } from './SearchInputContext';
const SearchInputComponent = () => {
const [value, setValue] = useState('');
const { setSearchValue } = useContext(SearchInputContext);
const inputRef = useRef(null);
const debounceLoadData = useCallback(
_.debounce((value) => {
setSearchValue(value);
}, 500),
[]
);
const handleSearchFieldChange = (event) => {
const { value } = event.target;
setValue(value);
debounceLoadData(value);
};
return (
<>
<TextField
inputRef={inputRef}
id="searchField"
value={value}
label={"search"}
onChange={handleSearchFieldChange}
/>
</>
);
};
export default SearchInputComponent;
After in your App.js (or other component page where you want a searchField) add your ContextProvider like this:
App.js
import {ListPage} from "./searchPage";
import {SearchInputContextProvider} from './SearchInputContext';
import "./styles.css";
export default function App() {
return (
<SearchInputContextProvider>
<ListPage/>
</SearchInputContextProvider>
);
}
And finally add your searchComponent where you need a search feature like in the ListPage component :
SearchPage.js:
import React, { useState,useContext, useEffect } from "react";
import axios from "axios";
import SearchInputComponent from './SearchInput';
import {SearchInputContext} from './SearchInputContext'
export const ListPage = () => {
const [data, setData] = useState([]);
const { searchValue } = useContext(SearchInputContext);
useEffect(() => {
if(searchValue){
const getData = (name) => {
axios.get(`https://restcountries.eu/rest/v2/name/${name}`).then((res) => {
console.log(res);
setData(res.data);
});
};
return getData(searchValue)
}
}, [ searchValue]);
return (
<>
<SearchInputComponent />
{data &&
<ul>
{data.map(country=> (
<li key={country.alpha3Code}>{country.name}</li>
))
}
</ul>
}
</>
);
};
here a sandbox link of this example
I thought had a better grasp of hooks but I've clearly got something wrong here. Not all of the character objects will have what I'm trying to get but it wont work with those that do.
I cna't even build in a check for character.comics.available. Same errors appear. I'm presuming I'm getting them before the state is set? But {character.name} always works.
CharacterDetail.js
import React from "react";
import { useParams } from "react-router-dom";
import useCharacter from "../hooks/useCharacter";
const CharacterDetail = () => {
// from the Route path="/character/:id"
const { id } = useParams();
// custom hook. useCharacter.js
const [character] = useCharacter(id);
// this only works sometimes but errors if i refresh the page
// console.log(character.comics.available);
return (
<div>
<h2 className="ui header">Character Details</h2>
<p>Works every time: {character.name}</p>
<div className="ui segment"></div>
<pre></pre>
</div>
);
};
export default CharacterDetail;
Custom hook useCharacter.js
import { useState, useEffect } from "react";
import marvel from "../apis/marvel";
const useCharacter = (id) => {
const [character, setCharacter] = useState({});
useEffect(() => {
loadItem();
return () => {};
}, [id]);
const loadItem = async (term) => {
const response = await marvel.get(`/characters/${id}`);
console.log(response.data.data.results[0]);
setCharacter(response.data.data.results[0]);
};
return [character];
};
export default useCharacter;
error when console is uncommented
Uncaught TypeError: Cannot read property 'available' of undefined
at CharacterDetail (CharacterDetail.js:11)
...
Here is the character object.
thanks to #Nikita for the pointers. Settled on this...
CharacterDetail.js
import React from "react";
import { useParams } from "react-router-dom";
import useCharacter from "../hooks/useCharacter";
const CharacterDetail = () => {
const { id } = useParams();
// custom hook. useCharacter.js
const { isLoading, character } = useCharacter(id);
const isArray = character instanceof Array;
if (!isLoading && isArray === false) {
console.log("isLoading", isArray);
const thumb =
character.thumbnail.path +
"/portrait_uncanny." +
character.thumbnail.extension;
return (
<div>
<h2 className="ui header">{character.name}</h2>
<img src={thumb} />
<div className="ui segment">{character.comics.available}</div>
<div className="ui segment">{character.series.available}</div>
<div className="ui segment">{character.stories.available}</div>
</div>
);
}
return <div>Loading...</div>;
};
export default CharacterDetail;
useCharacter.js
import { useState, useEffect } from "react";
import marvel from "../apis/marvel";
function useCharacter(id) {
const [character, setCharacter] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const fetchData = async () => {
setIsLoading(true);
await marvel
.get(`/characters/${id}`)
.then((response) => {
/* DO STUFF WHEN THE CALLS SUCCEEDS */
setIsLoading(false);
setCharacter(response.data.data.results[0]);
})
.catch((e) => {
/* HANDLE THE ERROR (e) */
});
};
fetchData();
}, [id]);
return {
isLoading,
character,
};
}
export default useCharacter;