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);
}
};
Related
I am new to tests in react and I don't know how to properly mock the useState value to properly cover the lines that uses the boolean as a parameter to return the component
React Code
import React from "react";
import { InputGroup, Input, Button, Spinner, Center } from "#chakra-ui/react";
import movieAPI from "../../services/movieAPI";
import { useNavigate } from "react-router-dom";
import styles from "./index.module.css";
export const Search = () => {
const navigate = useNavigate();
const [movie, setMovie] = React.useState("");
const [isLoading, setLoading] = React.useState(false);
const handleClick = async () => {
setLoading(true);
const data = await movieAPI.fetchMovieByTitle(movie);
setLoading(false);
navigate(`/movie/${data.Title}`, { state: data });
};
return isLoading ? (
<Center>
<Spinner
data-testid="spinner"
className={styles.verticalCenter}
thickness="6px"
speed="1.0s"
emptyColor="gray.200"
color="green.500"
size="xl"
/>
</Center>
) : (
<InputGroup m={2} p={2}>
<Input onChange={(e) => setMovie(e.target.value)} placeholder="Search" />
<Button onClick={handleClick}>Search</Button>
</InputGroup>
);
};
How can I mock the property loading in order to cover the specific line of the spinner component?
down below is my attempt to test the Spinner code
import { render, fireEvent, RenderResult } from "#testing-library/react";
import { Search } from "../../components/search/search";
import { BrowserRouter as Router, useNavigate } from "react-router-dom";
import React from "react";
describe("search.tsx", () => {
let isLoading = false;
let setLoading = jest.fn();
let container: RenderResult<
typeof import("#testing-library/dom/types/queries"),
HTMLElement,
HTMLElement
>;
jest.mock("react", () => {
return {
...jest.requireActual("react"),
useState: () => ({
isLoading: isLoading,
setLoading: setLoading,
}),
};
});
beforeEach(() => {
container = render(
<Router>
<Search />
</Router>
);
});
it("should render spinner", async () => {
setLoading.mockImplementation((data) => {
isLoading = data;
});
setLoading(true);
console.log(await container.findByTestId("spinner"));
});
});
A component is like a black box for testing.
It has two inputs: props and user interaction. Based on those it renders something. you should not mock useState. Your test would look like this:
You can mock other dependencies like localStorage and or rest api calls. But no internal component implementation.
Your test should look like this, written in pseudo code
it("Should show loader while searching for movies", () => {
// mock the API to return promise which never resolves
// render component
// input some search data
// click the search button
// expect the loader to be visible
});
it("Should reflect text base on user input,", () => {
// render component
// input some search data "Start"
// expect searchInput.text to have value = "Start"
})
In following codes, eslint will give a warning.
Line 24:6: React Hook useEffect has a missing dependency: 'fetchPosts'. Either include it or remove the dependency array react-hooks/exhaustive-deps
import { useState, useEffect } from 'react';
import { useLocation } from "react-router-dom";
import { Layout } from './Layout';
import { TwitterPost, reloadTwitterEmbedTemplate } from '../TwitterPost';
import '../../styles/pages/TimelinePage.css'
import axios from 'axios';
export const TimelinePage = () => {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const location = useLocation();
const fetchPosts = async () => {
const res = await axios.get('/api/posts', { params: { page: page } });
setPosts(posts.concat(res.data));
reloadTwitterEmbedTemplate();
setPage(page + 1);
};
useEffect(() => {
if (location.pathname !== '/') return;
fetchPosts();
}, [location]);
const postTemplates = posts.map((post: any) => {
if (post.media_name === 'twitter') return <TwitterPost mediaUserScreenName={post.media_user_screen_name} mediaPostId={post.media_post_id} />;
return null;
});
return(
<Layout body={
<div id="timeline">
<div>{postTemplates}</div>
<div className="show-more-box">
<button type="button" className="show-more-button" onClick={fetchPosts}>show more</button>
</div>
</div>
} />
);
};
I fixed the warning by adding fetchPosts. Then I followed eslint instructions using useCallback and adding variables used in fetchPosts to deps. This change causes a loop. How should I fix the loop and eslint warning?
import { useState, useEffect, useCallback } from 'react';
import { useLocation } from "react-router-dom";
import { Layout } from './Layout';
import { TwitterPost, reloadTwitterEmbedTemplate } from '../TwitterPost';
import '../../styles/pages/TimelinePage.css'
import axios from 'axios';
export const TimelinePage = () => {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const location = useLocation();
const fetchPosts = useCallback(async () => {
const res = await axios.get('/api/posts', { params: { page: page } });
setPosts(posts.concat(res.data));
reloadTwitterEmbedTemplate();
setPage(page + 1);
}, [page, posts]);
useEffect(() => {
if (location.pathname !== '/') return;
fetchPosts();
}, [location, fetchPosts]);
const postTemplates = posts.map((post: any) => {
if (post.media_name === 'twitter') return <TwitterPost mediaUserScreenName={post.media_user_screen_name} mediaPostId={post.media_post_id} />;
return null;
});
return(
<Layout body={
<div id="timeline">
<div>{postTemplates}</div>
<div className="show-more-box">
<button type="button" className="show-more-button" onClick={fetchPosts}>show more</button>
</div>
</div>
} />
);
};
I highly recommend this article to really understand what's going on when you use the useEffect hook. It talks, among other things, about your exact problem and ways to solve it. That said, you should move the function inside the useEffect callback, something like:
export const TimelinePage = () => {
/* ... */
useEffect(() => {
if (location.pathname !== '/') return;
const fetchPosts = async () => {
const res = await axios.get('/api/posts', { params: { page: page } });
setPosts(posts.concat(res.data));
reloadTwitterEmbedTemplate();
setPage(page + 1);
}
fetchPosts();
}, [location]);
/* ... */
};
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
I'm trying to make a page to show the details of each video.
I fetched multiple video data from the back-end and stored them as global state.
This code works if I go to the page through the link inside the app. But If I reload or open the URL directory from the browser, It can not load the single video data.
How should I do to make this work?
Thanx
Single Video Page
import { useState, useEffect, useContext } from "react";
import { useParams } from "react-router-dom";
import { VideoContext } from "../context/videoContext";
const SingleVideo = () => {
let { slug } = useParams();
const [videos, setVideos] = useContext(VideoContext);
const [video, setVideo] = useState([]);
useEffect(() => {
const result = videos.find((videos) => {
return videos.uuid === slug;
});
setVideo((video) => result);
}, []);
return (
<>
<div>
<h1>{video.title}</h1>
<p>{video.content}</p>
<img src={video.thumbnail} alt="" />
</div>
</>
);
};
export default SingleVideo;
Context
import React, { useState, createContext, useEffect } from "react";
import Axios from "axios";
import { AxiosResponse } from "axios";
export const VideoContext = createContext();
export const VideoProvider = (props) => {
const [videos, setVideos] = useState([]);
const config = {
headers: { "Access-Control-Allow-Origin": "*" },
};
useEffect(() => {
//Fetch Vidoes
Axios.get(`http://localhost:5000/videos`, config)
.then((res: AxiosResponse) => {
setVideos(res.data);
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<VideoContext.Provider value={[videos, setVideos]}>
{props.children}
</VideoContext.Provider>
);
};
I think the reason is because when you refresh the app, you fetch the video data on context and the useEffect on your single video page component runs before you receive those data.
To fix you can simply modify slightly your useEffect in your single video component to update whenever you receive those data:
useEffect(() => {
if (videos.length) {
const result = videos.find((videos) => {
return videos.uuid === slug;
});
setVideo((video) => result);
}
}, [videos]);
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