Unable to test a component with useLocation hook - reactjs

I have a Headercomponent as follows:
const Header = () => {
const data = useDataContext().header;
return <div data-testid="header" className="w-full h-[80px] bg-white">
<div className="h-1 bg-green-400 w-full"/>
<div className="flex items-center w-full h-[80px] shadow-sm">
<div className="flex items-center flex-1 h-full">
<Navigation applications={data.apps}/>
</div>
<div className="flex items-center pl-4 h-full">
<Account />
</div>
</div>
</div>
}
The Navigation component is as follows:
import React, {useState} from 'react';
import {App} from "../../../../interfaces/interfaces";
import {map, find, filter} from "lodash";
import NavigationItem from "./NavigationItem";
import { useLocation, useNavigate } from 'react-router-dom';
interface NavigationProps {
applications: App[]
}
const Navigation:React.FC<NavigationProps> = ({applications}: NavigationProps) => {
const [expanded, setExpanded] = useState<boolean>(false)
const location = useLocation();
const navigate = useNavigate();
const activeApplication = find(applications, application => application.url === location.pathname);
const inactiveApplications = filter(applications, application => application.url !== location.pathname)
return (
<div className="flex h-full">
<div className="z-[2]">
<NavigationItem application={activeApplication}
handleClick={() => setExpanded(expanded => !expanded)}
expanded={expanded}
/>
</div>
<div className={`flex transform transition transition-transform ${expanded ? 'translate-x-0' : '-translate-x-full'}`}>
{map(inactiveApplications, application => {
return <NavigationItem application={application}
handleClick={() => {
setExpanded(false);
navigate(application.url);
}}
/>
})}
</div>
</div>
);
};
export default Navigation;
Thus the Navigation component uses the useLocation hook from react-router-dom.
I want to write some tests for Header component and the header.test.tsx is as follows:
import Header from "./Header";
import {MemoryRouter} from "react-router-dom";
import {describe, it} from "#jest/globals";
import {render, screen} from "#testing-library/react";
describe("<Header/>", () => {
it("renders the header component with Home link", () => {
render(<MemoryRouter initialEntries={["/home"]}>
<Header/>
</MemoryRouter>);
const textElement = screen.getByText(/Home/i);
expect(textElement).toBeInTheDocument();
const headerElement = screen.getByTestId("header");
expect(headerElement).toBeInTheDocument()
// expect(screen.getByRole('link')).toHaveAttribute('href', 'https://www.test.com');
});
});
Thus I wrap the header component with MemoryRouter but it still gives me an error as follows:
useLocation() may be used only in the context of a <Router> component.
What do I do wrong?
Thanks in advance.

Your render should look like this
render(
<MemoryRouter initialEntries={["/home"]}>
<Routes>
<Route path="/home" element={<Header/>} />
</Routes>
</MemoryRouter>
)

Related

Getting Invalid Hook Error in React App after trying to wrap app in provider

I am building a mock e-commerce store using react and after laying the groundwork for the website, I installed the package react-use-cart (https://www.npmjs.com/package/react-use-cart) to handle the items being added to the cart and to be used in a checkout page. I wrapped my app in the Cart Provider that was given by the react-use-cart package and set up the cart functions on my product pages but now my app does not load anything on startup and gives me an invalid hook call error. I have tried wrapping the app in the index.js file with the provider as well and that gives the same error.
import React, { useState } from "react";
import './css/App.css';
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
import NavBar from "./components/NavBar"
import ProductsScreen from "./screens/ProductsScreen"
import ProductDetailScreen from "./screens/ProductDetailScreen"
import HomeScreen from "./screens/HomeScreen"
import LoginScreen from "./screens/LoginScreen"
import RegisterScreen from "./screens/RegisterScreen";
import CartScreen from "./screens/CartScreen"
import { CartProvider } from "react-use-cart";
const App = () => {
return (
<>
<BrowserRouter>
<CartProvider>
<NavBar />
<Routes>
<Route path='/' element={<HomeScreen/>}/>
<Route path='/products' element={<ProductsScreen/>}/>
<Route path='/product/:id' element={<ProductDetailScreen/>}/>
<Route path='/login' element={<LoginScreen/>}/>
<Route path='/register' element={<RegisterScreen/>}/>
<Route path='/cart' element={<CartScreen/>}/>
</Routes>
</CartProvider>
</BrowserRouter>
</>
)
}
export default App;
import { useState, useEffect }from 'react'
import { Link, useParams } from 'react-router-dom'
import axios from 'axios'
import "../css/ProductDetailScreen.css"
import { useCart } from "react-use-cart";
// import Slideshow from '../components/SlideShow'
const { addItem, updateItemQuantity } = useCart();
const ProductDetailScreen = ( {match} ) => {
const [product, setProduct] = useState({})
const { id } = useParams()
useEffect(() => {
const getProduct = async () => {
const { data } = await axios.get(`/api/products/${id}`)
setProduct(data)
}
getProduct()
}, [])
return (
<>
<Link to ='/products'>Go Back</Link>
<div className="wrapper">
<div className="detail-grid">
<div className='imgContainer'>
<img className='cover_img1' src={product.cover_img} alt='Videogame Cover Art'/>
<div className='carousel'>
{product.images?.map((image, index) => (
<img key={index} src={image} alt="Videogame Gameplay"></img>
))}
</div>
{/*Replace carousel div with actual image slideshow*/}
</div>
<div>
<div className='textContainer'>
<h1 className='title1'>{product.title}</h1>
<h3 className='descriptionHeader'>Description:</h3>
<p className='description1'>{product.description}</p>
<h3 className='developer1'>Developer: {product.developer}</h3>
<h3 className='publisher1'>Publisher: {product.publisher}</h3>
<h3 className='releaseDate1'>Release Date: {product.releaseDate}</h3>
<h3 className='genre1'>Genre: {product.genre}</h3>
</div>
<div className='checkOutContainer'>
<h3 className='price1'>Price: ${product.price}</h3>
<button onClick={() => updateItemQuantity(product.id, product.quantity - 1)}> - </button>
<span>{product.quantity}</span>
<button onClick={() => updateItemQuantity(product.id, product.quantity + 1)}> + </button>
<button onClick={() => addItem(product)} className='addCart' type='button'>Add to Cart</button>
</div>
</div>
</div>
</div>
</>
)
}
export default ProductDetailScreen

React Context and React Custom hook with state not re-rendering after update

I am trying to toggle sidebar from navbar toggle button but unfortunately my custom react hook or react context is not triggering re render but the state is updating and also logging to the console but the it is not re rendering. I know this question is been asked so many times but I tried many things but couldn't able to work as expected.
Please help thankyou!
sidebar component -
import React from 'react';
import {useAuth} from '../contexts/auth';
import {useSider} from '../contexts/sider';
import Avatar from './Avatar';
import SideItem from './SideItem';
const Sider: React.FC = (): JSX.Element => {
const {auth} = useAuth();
const {expanded} = useSider();
return (
<>
<aside className='h-full border-r border-violet-200 bg-white'>
<ul className='p-1'>
<SideItem
to='#!'
key='avatar'
element={
<Avatar
username={auth?.user.username ?? 'username'}
src={auth?.user.avatar ?? undefined}
expanded={expanded}
/>
}
/>
<SideItem
to='/'
icon='fa-tachometer-alt'
title='Dashboard'
key='dashboard'
/>
<SideItem
to='/subscriptions'
icon='fa-file-export'
title='Subscriptions'
key='subscriptions'
/>
<SideItem
to='/sheets'
icon='fa-file-lines'
title='Sheets'
key='sheets'
/>
</ul>
</aside>
</>
);
};
export default React.memo(Sider);
Navbar component -
import React from 'react';
import {NavLink} from 'react-router-dom';
import {useAuth} from '../contexts/auth';
import {useSider} from '../contexts/sider';
import Brand from './Brand';
import NavItem from './NavItem';
const Nav: React.FC = (): JSX.Element => {
const auth = useAuth();
const {toggleSider} = useSider();
function signOut() {
// TODO : Sign out logic
}
function subscribe() {
// TODO : subscribe logic
}
return (
<>
<nav className='flex items-center justify-between px-6 py-1 bg-blue-50 border-b border-violet-200 sticky top-0'>
<button
className='border flex aspect-square p-1 text-violet-500'
onClick={() => toggleSider()}
>
<i className='fa-solid fa-bars' />
</button>
<NavLink to='/'>
<Brand />
</NavLink>
<ul className='w-full flex-grow justify-end flex items-center'>
{!auth ? (
<>
<NavItem to='/signin' title='Sign In' icon='fa-sign-in' />
<NavItem
to='/register'
variant='warning'
title='Register'
icon='fa-edit'
/>
</>
) : (
<>
<NavItem
to='#new-subscription'
onClick={subscribe}
variant='success'
title='New Subscription'
icon='fa-user-plus'
/>
<NavItem
to='#sign-out'
onClick={signOut}
variant='danger'
title='Sign Out'
icon='fa-sign-out'
/>
</>
)}
</ul>
</nav>
</>
);
};
export default React.memo(Nav);
SideItem component -
import React from 'react';
import {NavLink, NavLinkProps} from 'react-router-dom';
import {useSider} from '../contexts/sider';
interface SideItemProps extends NavLinkProps {
icon?: string;
element?: JSX.Element;
}
const SideItem: React.ExoticComponent<SideItemProps> = React.memo(
({to, title, icon, element, ...rest}) => {
const {expanded} = useSider();
return (
<li className='border-b border-violet-200 block text-violet-500 hover:text-violet-800 active:text-violet-500 p-1 lg:pl-0'>
{element ?? (
<NavLink
to={to}
{...rest}
className='flex p-2 lg:p-1 lg:pl-0 text-base lg:text-lg font-medium justify-center lg:justify-start items-center hover:bg-blue-50 active:bg-inherit lg:rounded-r-full rounded-md flex-col lg:flex-row'
>
<i className={'lg:mx-2 lg:w-5 fa-solid ' + icon}></i>
{expanded && <span className='hidden md:block'>{title}</span>}
</NavLink>
)}
</li>
);
}
);
export default SideItem;
App component -
import React from 'react';
import {Routes, Route} from 'react-router-dom';
import Nav from './components/Nav';
import Sider from './components/Sider';
import PrivateOutlet from './hoc/PrivateOutlet';
import Dashboard from './pages/dashboard';
import Register from './pages/register';
import Signin from './pages/signin';
import Subscriptions from './pages/subscriptions';
import Sheets from './pages/sheets';
import useSiderVisibility from './hooks/useSiderVisibility';
import {SiderProvider} from './contexts/sider';
const App: React.FC = (): JSX.Element => {
const showSider = useSiderVisibility();
return (
<>
<div className='App bg-zinc-100 overflow-hidden'>
<SiderProvider>
<Nav />
</SiderProvider>
<main className='flex h-full'>
{showSider && (
<div className='hidden sm:block relative p-2 pt-0 pl-0 lg:w-1/5 sm:w-1/6'>
<SiderProvider>
<Sider />
</SiderProvider>
</div>
)}
<div className='overflow-y-auto p-2 relative w-full'>
<Routes>
<Route path='/signin' element={<Signin />} />
<Route path='/register' element={<Register />} />
<Route path='/' element={<PrivateOutlet />}>
<Route index element={<Dashboard />} />
<Route path='subscriptions' element={<Subscriptions />} />
<Route path='sheets' element={<Sheets />} />
</Route>
<Route element={'404 Not Found'} />
</Routes>
</div>
</main>
</div>
</>
);
};
export default React.memo(App);
Sider context -
import React, {
createContext,
PropsWithChildren,
useCallback,
useContext,
useState,
} from 'react';
interface ISiderContext {
expanded: boolean;
toggleSider: () => void;
}
const initialValue = {
expanded: true,
toggleSider: () => {},
};
const SiderContext = createContext<ISiderContext>(initialValue);
/**
* A hook to get the information about whether the sider component is expanded or collapsed stored in Sider Context.
* #returns expanded: true if sider is expanded and toggleSider: a function toggle between expanded and collapsed.
*/
const useSider = () => {
return useContext(SiderContext);
};
const SiderProvider: React.ExoticComponent<PropsWithChildren> = React.memo(
({children}): JSX.Element => {
const [expanded, setExpanded] = useState<ISiderContext['expanded']>(
initialValue.expanded
);
const toggleSider = useCallback(() => {
setExpanded((prev) => !prev);
}, []);
return (
<SiderContext.Provider value={{expanded, toggleSider}}>
{children}
</SiderContext.Provider>
);
}
);
export {SiderProvider, useSider};
This was the original code in which i had tried different things but didn't work.
Thankyou!

How do I use the Use Navigate from React Router on a button that links to different pages?

I have a button that represents shop now that upon click it redirects you on different pages and the card elements have been mapped on external data.
I want this button to navigate to different pages kindly help.
const data =[
{
id:"Extensions",
Title:"Electrical Collections",
img:"/Assets/Electricals/Extlogo.png",
},
{
id:"Phone Accesorries",
Title:"Phone Accessories",
img:"/Assets/Phone/phoneacc.png",
},
{
id:"Homeware",
Title:"Homeware Collections",
img:"/Assets/Home/home.png",
},
]
function Home() {
let Navigate =useNavigate();
const handleElectricalPage = () =>{
Navigate("/extensionproduct")
}
<div className='cardContainer'>
{data.map((item )=>(
<card>
<div className='imageContainer'>
<img src={item.img} alt='Extension logo'/>
</div>
<div className='contentsContainer'>
<h2>{item.Title}</h2>
<div className='iconButtonContainer'>
<button onClick={handleElectricalPage}>Shop Now</button>
<ArrowForwardIcon className='arrowIcon'/>
</div>
</div>
Example from the react router https://reactrouter.com/docs/en/v6/api#usenavigate
navigate("../success", { replace: true });
You need to make an onClick handler (handleElectricalPage) dynamically, consider something like this.
function Home() {
let navigate =useNavigate();
return (
<div className='cardContainer'>
{data.map((item) => (
<card>
<button
onClick={() => navigate(`/externalProduct/${item.id}`)}
>
Shop Now
</button>
</card>
)}
</div>
}
You can also use Link which handles everything by itself
<Link to={`/externalProduct/${item.id}`}>
<button>Shop now</button>
</Link>
App.jsx
import { Routes, Route, Navigate } from "react-router-dom";
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />}>
<Route path="post/:id" element={<Detail />} />
</Route>
</Routes>
</div>
);
}
export default App;
Post.jsx
import React, { useState } from "react";
import {useNavigate} from 'react-router-dom'
export default function Post(props) {
const navigate = useNavigate()
{props.posts.map((post) => {
return (
<div className="post__container" key={post.id}>
<h4>
{post.id} {post.title}
</h4>
<p>{post.body}</p>
<button onClick={() => {return navigate(`${post.id}`)}}>Detail</button>
</div>
);
})}
}
Detail.jsx
import axios from 'axios'
import React, {useEffect, useState } from 'react'
import {useParams} from 'react-router'
export default function Detail (props) {
const params = useParams()
const [posts, setPosts] = useState({})
async function getById(id) {
const response = await axios.get("https://jsonplaceholder.typicode.com/posts/" + id)
setPosts(response.data)
}
useEffect(() => {
getById(params.id)
}, [params.id])
return (
<div>
<h2>Detail page: {params.id}</h2>
<h4>{posts.title}</h4>
<h5>{posts.body}</h5>
</div>
)
}

How to pass props through Link

Fairly new to react here. I'm making a small recipe finder app with an api. After getting the data, I'm mapping through the results and displaying them in a component. What I want to do is display the details of each recipe through another component in another route. I'm not sure how to do this. I thought I could pass the mapped recipe through Link, but it's not working. Here is what I have so far.
Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById('root')
);
App.js
import React, { useState, useEffect} from "react";
import axios from "axios";
import { BrowserRouter as Router, Routes, Route, useNavigate} from "react-router-dom";
import "./App.css";
import RecipeList from "./RecipeList";
import Recipe from "./Recipe";
import Header from "./Header";
function App() {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
const [query, setQuery] = useState("");
const [search, setSearch] = useState("");
const APP_ID = "XXXXXXXXX";
const APP_KEY = "XXXXXXXXXXXXXXXXXXXXXX";
const url = `https://api.edamam.com/api/recipes/v2?type=public&q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`;
const getRecipes = async () => {
const res = await axios(url);
const data = await res.data.hits;
console.log(data);
setRecipes(data);
};
useEffect(() => {
getRecipes();
}, [query]);
const updateSearch = (e) => {
setSearch(e.target.value);
console.log(search);
};
const getSearchQuery = (e) => {
e.preventDefault();
setQuery(search);
setSearch("");
navigate("/recipes");
};
return (
<div className="App">
<Header />
<div>
<div className="container">
<form className="search-form" onSubmit={getSearchQuery}>
<input
className="search-input"
type="text"
value={search}
onChange={updateSearch}
placeholder="search by food name"
/>
</form>
</div>
</div>
<Routes>
<Route path="/recipes" element={<RecipeList recipes={recipes} />} />
<Route path="/recipes/:id" element={<Recipe recipes={recipes} />}/>
</Routes>
</div>
);
}
export default App;
RecipeList.jsx
import React from "react";
import { Link } from "react-router-dom";
const RecipeList = ({ recipes }) => {
return (
<div className="container">
<div className="grid-container">
{recipes.map(({ recipe }) => (
<Link to={`/recipes/${recipe.label}`}>
<img key={recipe.image} src={recipe.image} alt="" />
<p key={recipe.label}>{recipe.label}</p>
<p>{recipe.id}</p>
</Link>
))}
</div>
</div>
);
};
export default RecipeList;
Recipe.jsx
const Recipe = ({recipe}) => {
return (
<div>
<h1>{recipe.label}</h1>
</div>
)
}
export default Recipe
Am I even close???
You are passing the entire recipes array to both routed components.
<Routes>
<Route path="/recipes" element={<RecipeList recipes={recipes} />} />
<Route path="/recipes/:id" element={<Recipe recipes={recipes} />}/>
</Routes>
So Recipe can use the entire array and the id route match param to search the passed array and render the exact recipe by matching label.
import { useParams } from 'react-router-dom';
const Recipe = ({ recipes }) => {
const { id } = useParams();
const recipe = recipes.find(recipe => recipe.label === id); // *
return recipe ? (
<div>
<h1>{recipe.label}</h1>
</div>
) : null;
};
* Note: Since you call the route param id it may make more sense to us the recipe.id for the link.
{recipes.map(({ recipe }) => (
<Link to={`/recipes/${recipe.id}`}>
<img key={recipe.image} src={recipe.image} alt="" />
<p key={recipe.label}>{recipe.label}</p>
<p>{recipe.id}</p>
</Link>
))}
...
const recipe = recipes.find(recipe => recipe.id === id);

React hook issue. Is not re-render one component

I have created a global provider with a reducer that is holding the user state.
When the user login successfully I dispatch and update the state, setting login=true. It is working perfectly in all components except for the top component where is not updating. I don't see any problem. Any idea what might be the problem?
// index.js
import './styles/tailwind.css';
import './styles/index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import {StateProvider } from "./store";
import App from './App';
import * as serviceWorker from './serviceWorker';
const app = (
<StateProvider>
<App />
</StateProvider>
);
ReactDOM.render(app, document.getElementById('root'));
//store.js - the provider that update the state
import React, {createContext, useReducer} from 'react';
const initialState = {
login:false,
token:'',
refreshToken:'',
user:{}
};
const store = createContext(initialState);
const { Provider } = store;
const StateProvider = ( { children } ) => {
const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'login user':
state.login = true;
state.token = action.token;
state.refreshToken = action.refreshToken;
return state;
default:
throw new Error();
};
}, initialState);
return <Provider value={{ state, dispatch }}>{children}</Provider>;
};
export { store, StateProvider }
//App.js ---- in this page is not working, the rerendering doesn't happend and login remain false only in this component
import React, {useState, useContext, useEffect} from 'react';
import { store } from "./store";
import logo from './svg/book.svg';
import { BrowserRouter, Route, NavLink } from "react-router-dom";
import Classes from './components/school/Classes.js';
import Students from './components/school/Students.js';
import Home from './components/school/Home.js';
import Cards from './components/Cards.js';
import Login from "./components/Login";
function App() {
const globalState = useContext(store);
const login = globalState.state.login;
console.log(globalState);
const message = (login) ? "Welcome user" : "You are not loged in";
return (
<div>
<p>{message}</p>
<BrowserRouter>
<header className="grid grid-cols-1 sm:grid-cols-2">
<div className="flex align-middle"><img src={logo} className="h-12 p-1"></img>Bucharest Hi School
</div>
<div className="flex flex-wrap mr-5">
<NavLink to="/" className="w-full sm:w-1/4 text-center sm:text-right p-2" href="#" >Home</NavLink>
<NavLink to="/classes" className="w-full sm:w-1/4 text-center sm:text-right p-2" href="#" >Classes </NavLink>
<NavLink to="/students" className="w-full sm:w-1/4 text-center sm:text-right p-2" href="#">Students</NavLink>
<NavLink to="/statistics" className="w-full sm:w-1/4 text-center sm:text-right p-2" href="#">Statistics</NavLink>
<NavLink to="/logout" className="w-full sm:w-1/4 text-center sm:text-right bg-black text-white p-2" href="#">Logout</NavLink>
<NavLink to="/login" className="w-full sm:w-1/4 text-center sm:text-center bg-white rounded text-center p-2" href="#">Login</NavLink>
</div>
</header>
<div className="flex w-full flex-wrap justify-center my-10">
<Route exact path="/" component={Home} />
<Route path="/classes" component={Classes} />
<Route path="/students" component={Students} />
<Route path="/login" component={Login} />
</div>
</BrowserRouter>
</div>
);
}
export default App;
Thanks
You are mutating the state and returning same object in the reducer function, due to which your component assumes the data has never been changed.
Please make sure you return new object whenever you update your state, in your case you could use return { ...state }; instead of return state; in your reducer.

Resources