I'm starting using React Testing Library to make tests for a React Application, but i'm struggling to mock the data in a component that makes API calls using a Hook as service.
My component is just a functional component, with nothing extraordinary:
import React, { useEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import Skeleton from '#material-ui/lab/Skeleton'
import usePanelClient from '../../../clients/PanelClient/usePanelClient'
import useUtils from '../../../hooks/useUtils'
import IconFile from '../../../assets/img/panel/ico-file.svg'
import IconExclamation from '../../../assets/img/panel/ico-exclamation.svg'
import IconPause from '../../../assets/img/panel/awesome-pause-circle.svg'
import './PendingAwards.scss'
const PendingAwards = () => {
const pendingAwardsRef = useRef(null)
const location = useLocation()
const [loading, setLoading] = useState(true)
const [pendingAwards, setPendingAwards] = useState({})
const panelClient = usePanelClient()
const { formatCurrency } = useUtils()
useEffect(() => {
panelClient()
.getPendingAwards()
.then((response) => {
setLoading(false)
setPendingAwards(response.data)
})
}, [panelClient])
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
if (searchParams.get('scrollTo') === 'pendingAwards') {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
}, [location])
return (
<div
id="pendingAwards"
className="pending-awards-container"
ref={pendingAwardsRef}
>
<span className="pending-awards-container__title">Prêmios Pendentes</span>
{loading && (
<div className="skeleton-box">
<Skeleton width="100%" height="70px" />
<Skeleton width="100%" height="70px" />
</div>
)}
{!loading && (
<div className="pending-awards-values">
<div className="pending-awards-container__quantity">
<div className="pending-awards-container__quantity-container">
<span className="pending-awards-container__quantity-container-title">
Quantidade
</span>
<span className="pending-awards-container__quantity-container-content">
<div className="pending-awards-container__quantity-container-content__icon-box">
<img src={IconFile} alt="Ícone de arquivo" />
</div>
{pendingAwards.quantity ? pendingAwards.quantity : '0'}
</span>
</div>
</div>
<div className="pending-awards-container__amount">
<div className="pending-awards-container__amount-container">
<span className="pending-awards-container__amount-container-title">
Valor Pendente
</span>
<span className="pending-awards-container__amount-container-content">
<div className="pending-awards-container__amount-container-content__icon-box">
<img src={IconPause} alt="Ícone Pause" />
</div>
{pendingAwards.amount
? formatCurrency(pendingAwards.amount)
: 'R$ 0,00'}
</span>
</div>
</div>
<div className="pending-awards-container__commission">
<div className="pending-awards-container__commission-container">
<span className="pending-awards-container__commission-container-title">
Comissão Pendente
</span>
<span className="pending-awards-container__commission-container-content">
<div className="pending-awards-container__commission-container-content__icon-box">
<img src={IconExclamation} alt="Ícone exclamação" />
</div>
{pendingAwards.commission
? formatCurrency(pendingAwards.commission)
: 'R$ 0,00'}
</span>
</div>
</div>
</div>
)}
</div>
)
}
export default PendingAwards
My service that makes the API calls it is written like this:
import { useCallback } from 'react'
import axios from 'axios'
const usePanelClient = () => {
const getQuotationCard = useCallback(() => axios.get('/api/cards/quotation'), [])
const getCommissionCard = useCallback(() => axios.get('/api/cards/commission'), [])
const getPendingAwards = useCallback(() => axios.get('/api/premium/pending'), [])
return useCallback(() => ({
getQuotationCard,
getCommissionCard,
getPendingAwards,
}), [
getQuotationCard,
getCommissionCard,
getPendingAwards,
])
}
export default usePanelClient
In my current test I've tried mocking the hook like this, but I did not have success:
import React from 'react'
import { render } from '#testing-library/react'
import { Router } from 'react-router-dom'
import { createMemoryHistory } from 'history'
import PendingAwards from './PendingAwards'
describe('PendingAwards Component', () => {
beforeEach(() => {
jest.mock('../../../clients/PanelClient/usePanelClient', () => {
const mockData = {
quantity: 820,
amount: 26681086.12,
commission: 5528957.841628,
}
return {
getPendingAwards: jest.fn(() => Promise.resolve(mockData)),
}
})
})
it('should render the PendingAwards', () => {
const history = createMemoryHistory()
history.push = jest.fn()
const { container } = render(
<Router history={history}>
<PendingAwards />
</Router>,
)
expect(container).toBeInTheDocument()
})
it('should render the PendingAwards', () => {
const history = createMemoryHistory()
history.push({
search: '&scrollTo=pendingAwards',
})
window.scrollTo = jest.fn()
render(
<Router history={history}>
<PendingAwards />
</Router>,
)
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 })
})
})
May someone help me resolving this? I don't feel like this is something hard, but I've tried several things, and nothing seems to resolve it.
Thanks in advance.
You must call jest.mock at the top level of the module and for mocking ES6 modules with a default export you should use __esModule: true
jest.mock('../../../clients/PanelClient/usePanelClient', () => {
const mockData = {
quantity: 820,
amount: 26681086.12,
commission: 5528957.841628,
}
return {
__esModule: true,
default: ()=> ({
getPendingAwards: jest.fn(() => Promise.resolve({data: mockData})),
}),
}});
Related
On my component render, my useEffects hooks called, a function. the function updates the state status depending on the condition within the useEffects produce.
So in this case how to test the `mobileMenu` and how to set different condition in useEffect to test it?
I hope both my useEffects and useState need to mocked. I am in learning process with react. I could not get any correct answer upon searching, any one help me please?
here is my app.tsx
my ts file:
import { Footer, Header, ProductCart, ProductPhotoGallery, Tabs } from '#mcdayen/components';
import { Cart, Logo, MobileMenu, NaviLinks, QuickSearch, User } from '#mcdayen/micro-components';
import { initialNaviLinksProps, initialPhotoProps, initialTabsProps, NaviLinksProps, sizeProps } from '#mcdayen/prop-types';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchCartDetails, sizeHandler } from './store/cart.slice';
import { AppDispatch, RootState } from './store/store.config';
export function App() {
const dispatch:AppDispatch = useDispatch();
dispatch(fetchCartDetails());
const {product} = useSelector((state:RootState) => state.cartStore)
const [mobileMenu, setMobileMenu] = useState<boolean>(false);
const [linkProps, setLinkProps] = useState<NaviLinksProps | null>(null);
function mobileMenuHandler() {
setMobileMenu((current: boolean) => !current);
}
useEffect(() => {
setLinkProps(initialNaviLinksProps);
const mobileId = document.getElementById('mobileMenu');
if (mobileId?.offsetParent) {
mobileMenuHandler();
}
}, []);
useEffect(() => {
setLinkProps((props) => {
return mobileMenu ? { ...initialNaviLinksProps } : { ...initialNaviLinksProps, classProps: props?.classProps + ' hidden' }
})
}, [mobileMenu]);
function onSizeSelect(selectedSize: sizeProps) {
dispatch(sizeHandler(selectedSize));
}
return (
<section className="box-border m-auto flex flex-col pl-[18px] py-6 min-h-screen flex-wrap px-5 md:container md:w-[1440px] md:pl-[70px] pr-5 ">
<Header>
<Logo />
{linkProps && <NaviLinks passNaviLinks={linkProps} />}
<div className="flex gap-3">
<QuickSearch />
<Cart />
<User />
<MobileMenu menuHandler={mobileMenuHandler} />
</div>
</Header>
<main className='flex flex-col justify-between lg:flex-row'>
<div className='hidden lg:block w-[325px]'>
<div>
<Tabs tabProps={initialTabsProps} />
</div>
</div>
<div className='grow-0 flex-auto' >
{initialPhotoProps.length && <ProductPhotoGallery gallery={initialPhotoProps} />}
</div>
<div className='flex bg-white'>
{product && <ProductCart sizeSelect={onSizeSelect} passCartProps={product} />}
</div>
</main>
<Footer />
</section>
);
}
export default App;
My spec:
import { configureStore } from '#reduxjs/toolkit';
import { render } from '#testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import App from './app';
import cartReducer from './store/cart.slice';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
export function createTestStore() {
const store = configureStore({
reducer: {
cartStore:cartReducer,
}
})
return store;
}
describe('App', () => {
const setMobileMenu = jest.fn();
const useStateMock = (initState: boolean) => [initState, setMobileMenu];
jest.spyOn(React, 'useState').mockImplementation(useStateMock);
afterEach(() => {
jest.clearAllMocks();
});
const store = createTestStore();
it('should render successfully', () => {
const { baseElement } = render(
<BrowserRouter>
<Provider store={store}>{<App />}</Provider>
</BrowserRouter>
);
expect(baseElement).toBeTruthy();
useStateMock(true);
expect(setMobileMenu).toHaveBeenCalledWith(true);
});
});
I am getting an error at: `
jest.spyOn(React, 'useState').mockImplementation(useStateMock);
`
as : Argument of type '(initState: boolean) => (boolean | jest.Mock<any, any>)[]' is not assignable to parameter of type '() => [unknown, Dispatch<unknown>]'.
and my test failing.
Need help for:
test the useEffect hook on anonymous function ( mocking )
fixing the error highlighted
testing the state on setMobileMenu
Any one please help me with the correct way?
Try to declare useStateMock as:
const useStateMock = (initState: any) => [initState, setMobileMenu];
I just started with React and this is my first project. I added a delete icon. I just want when press it a console log will show some text just for testing and knowing how the props are passing between components. The problem is this text is not showing in the console. Please if anyone can help with that, I would appreciate it.
I have user components, allUser component, home component which included in the app.js
User.js component
import "./User.css";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faTimes } from "#fortawesome/free-solid-svg-icons";
function User(props) {
return (
<div className="singleUser">
<div className="user">
<div>{props.id}</div>
<div>{props.name}</div>
<div>{props.phone}</div>
<div>{props.email}</div>
</div>
<div className="iconClose">
<FontAwesomeIcon icon={faTimes} onClick={() => props.onDelete} />
</div>
</div>
);
}
import User from "./user";
import { useState, useEffect } from "react";
function Allusers({ onDelete }) {
const [isLoading, setIsLoading] = useState(false);
const [actualData, setActualData] = useState([""]);
useEffect(() => {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
// const finalUsers = [];
// for (const key in data) {
// const u = {
// id: key,
// ...data[key],
// finalUsers.push(u);
// }
setIsLoading(false);
setActualData(data);
});
}, []);
if (isLoading) {
return (
<section>
<p>Loading ... </p>
</section>
);
}
return actualData.map((singlUser) => {
for (const key in singlUser) {
// console.log(singlUser.phone);
return (
<div className="userCard" key={singlUser.id}>
<User
id={singlUser.id}
name={singlUser.name}
email={singlUser.email}
phone={singlUser.phone}
key={singlUser.id}
onDelete={onDelete}
/>
</div>
);
}
});
}
export default Allusers;
import Navagation from "../components/Navagation";
import Allusers from "../components/Allusers";
import Footer from "../components/Footer";
function Home() {
const deleteHandler = () => {
console.log("something");
};
return (
<section>
<Navagation />
<Allusers onDelete={deleteHandler} />
</section>
);
}
export default Home;
You aren't actually calling the function with () => props.onDelete in User.js-- it needs to be () => props.onDelete() (note the parens added after props.onDelete).
<FontAwesomeIcon icon={faTimes} onClick={() => props.onDelete} />
...should be:
<FontAwesomeIcon icon={faTimes} onClick={() => props.onDelete()} />
I am new to testing react-redux. I have a component named OTARequestDetails where the state of reducers is used and I am trying to access that state but getting the following error:TypeError: Cannot read property 'otaRequestItem' of undefined. I have tried many different ways but I am not able to understand why I can't access otaRequestItem. I have tested action and reducers individually and both tests are passed now I want to test the component.
Component:
import React, { useEffect, useState } from 'react'
import OTARequestCommandDetails from '../OTARequestCommandDetails'
import { otaStatusEnum } from '../../../constants'
import OTACommonHeader from '../OTACommonHeader'
import {
SectionTitle,
OTAStatusIcon,
} from '../../../common/components/SDKWrapper'
import { useSelector } from 'react-redux'
import { useAuth0 } from '#auth0/auth0-react'
import OTAModal from '../../../common/components/OTAModal'
import constants from '../../../constants'
function OTARequestDetails(props) {
const { isAuthenticated } = useAuth0()
const [isModalOpen, setIsModalOpen] = useState(false)
const otaItem = useSelector((state) => state.otaReducer.otaRequestItem)
const _constants = constants.OTAModal
useEffect(() => {
if (!isAuthenticated) {
const { history } = props
history.push({
pathname: '/dashboard/ota',
})
}
}, [])
const onQuit = () => {
setIsModalOpen(false)
}
const onAbortClick = () => {
setIsModalOpen(true)
}
let abortInfo
if (otaItem && otaStatusEnum.IN_PROGRESS === otaItem.status) {
abortInfo = (
<div>
<span className="headerLabel"></span>
<span className="infoLabel">
OTA request is in-process of being delievered.
<br />
In-progress OTAs can be aborted. Please note this is irrevertible.
<span className="link" onClick={() => onAbortClick()}>
Abort in-progress OTA
</span>
</span>
</div>
)
}
return (
<div className="otaRequestDetails">
{otaItem && (
<div>
<OTACommonHeader title={'OTA Details'} />
<div className="container">
<div className="marginBottom20px">
<SectionTitle text={constants.Forms.iccId.title} />
</div>
<div>
<span className="headerLabel">Request ID:</span>
<span>{otaItem.id}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">ICCID to OTA:</span>
<span>{otaItem.iccId}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">Date Created:</span>
<span>{otaItem.createdDate}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">Date Delivered:</span>
<span>{otaItem.createdDate}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">Created By:</span>
<span>{otaItem.requestedBy}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">OTA Status:</span>
<span>
<OTAStatusIcon otaStatus={otaItem.status} />
{otaItem.status}
</span>
</div>
{abortInfo}
<hr className="seperateLine" />
<OTARequestCommandDetails otaItem={otaItem} />
</div>
</div>
)}
{isModalOpen && (
<OTAModal
_title={_constants.title}
_description={_constants.description}
_confirmText={_constants.confirmText}
_onQuit={onQuit}
isModalOpen={isModalOpen}
/>
)}
</div>
)
}
export default OTARequestDetails
Test:
import React from 'react'
import { Provider } from 'react-redux'
import { render, cleanup } from '#testing-library/react'
import thunk from 'redux-thunk'
import OTARequestDetails from '.'
import configureStore from 'redux-mock-store'
afterEach(cleanup)
const mockStore = configureStore([thunk])
describe('OTA Request Details', () => {
test('should render', () => {
const store = mockStore({
otaRequestItem: {
id: '20af3082-9a56-48fd-bfc4-cb3b53e49ef5',
commandType: 'REFRESH_IMSIS',
iccId: '789',
status: 'DELIEVERED',
requestedBy: 'testuser#telna.com',
createdDate: '2021-04-29T07:08:30.247Z',
},
})
const component = render(
<Provider store={store}>
<OTARequestDetails />
</Provider>,
)
expect(component).not.toBe(null)
})
})
Can anyone help me where I am wrong and why can't I access reducers? Thanks in advance.
With selector:
const otaItem = useSelector((state) => state.otaReducer.otaRequestItem);
You are accessing otaRequestItem from a state.otaReducer object.
In the test your mock store has no otaReducer property in its object. Nest otaRequestItem object within a otaReducer object.
const store = mockStore({
otaReducer: {
otaRequestItem: {
id: '20af3082-9a56-48fd-bfc4-cb3b53e49ef5',
commandType: 'REFRESH_IMSIS',
iccId: '789',
status: 'DELIEVERED',
requestedBy: 'testuser#telna.com',
createdDate: '2021-04-29T07:08:30.247Z',
},
},
});
Basic gist is... the mock store just needs a valid object shape for what a consuming component will attempt to select from it.
I am trying the send axios get request to server.js, which send a GET request to contentful website. I am getting no data in Home.js and getting following error in console. Could someone please help me to identify the issue here ?
I could see data displaying in setSearchResults while setting a break point, please refer screenshot attached.
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
in Home (created by Context.Consumer)
in Route (at App.js:18)
in Switch (at App.js:17)
in Router (created by BrowserRouter)
in BrowserRouter (at App.js:15)
in App (at src/index.js:11)
in Router (created by BrowserRouter)
in BrowserRouter (at src/index.js:10)
Home.js
import React, { useRef, useState, useEffect, Component } from 'react';
import { usePosts } from "../custom-hooks/";
import Moment from 'moment';
import { Wave } from "react-animated-text";
import axios from "axios";
export default function Home() {
const [posts, isLoading] = usePosts();
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [showColor, setShowColor] = useState("");
const [findTag, setFindTag] = useState("");
//const isMounted = useRef(false);
/* In the Home tab, system displays all the published blogs from contentful website.
We can search for the blogs in the search area provided. Also on click on the tags should filter
the blogs records.
*/
const handleChange = (e) => {
setSearchTerm(e.target.value);
}
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get('http://localhost:5000/service/blogpost');
setSearchResults(res.data.items);
} catch (e) {
console.log(e);
}
}
fetchData();
}, []);
useEffect(() => {
const results = searchResults.filter(blog =>
blog.fields.title.toLowerCase().includes(searchTerm) || blog.fields.title.toUpperCase().includes(searchTerm) || blog.fields.shortDescription.toLowerCase().includes(searchTerm)
|| blog.fields.shortDescription.toUpperCase().includes(searchTerm)
);
setSearchResults(results);
}, [searchTerm, searchResults]);
const getFilterTags = (event) => {
const tagText = event.target.innerText;
console.log("Print tag of a:"+tagText);
const results = searchResults.filter(blog =>
blog.fields.title.toLowerCase().includes(tagText) || blog.fields.title.toUpperCase().includes(tagText)
);
setSearchResults(results);
}
const renderPosts = () => {
if (isLoading) return(<div className="loadingIcon"> <p className="noSearchData">Loading...</p> </div>);
return (
<div className="wrap">
<div className="row">
<div className="column left" >
<h3>Search:</h3>
<label>
<div className="playerSearch_Home">
<div className="playerSearch_Icon">
<input type="text" className="playerSearch_Home_Input" placeholder="search posts..." value={searchTerm} onChange={handleChange} />
</div>
</div>
</label>
<h3>Tags:</h3>
<label>
{
searchResults.map(({ fields: { id, tags } }) => (
<div key={id} className="techtags">
{
Array.isArray(tags) ? (
tags.map((tag) => (
<a onClick={getFilterTags} className="grouptechtags" style={{backgroundColor: `${showColor}`},{ marginRight: "10px" }} key={tag}>{tag}</a>
))
) : (
<a onClick={getFilterTags} style={{backgroundColor: `${showColor}`}} className="grouptechtags">{tags}</a>
)
}
</div>
))
}
</label>
<div className="twitterlink">
Follow me on twitter
</div>
<div className="reactStunning">
🛠️ Built with react...!
</div>
<div>
<small className="copyright">© 2020 Soccerway</small>
</div>
</div>
<div className="column right" >
{!searchResults.length && (<div> <p className="noSearchData"><Wave text="No results available...!"/></p> </div>)}
<div className="container">
{
searchResults.map(({ sys: { id, createdAt}, fields: { title, image, shortDescription, description, tags, skillLevel, duration, slug } }) => (
<div key={id} className="column-center">
<article key={id} className="blogmaintile">
<div className="blogtitle">
<span key={title}>{title}</span>
</div>
<section>
<p className="blogdescription" key={shortDescription}>{shortDescription}</p>
<span className="blogcreateddate" key={createdAt}>{Moment(createdAt).format('MMM DD YYYY')}</span>
<span style={{display:"none"}} key={tags}>{tags}</span>
</section>
<section>
<p className="bloglongdescription" key={description}>{description}</p>
</section>
<section className="col1">
{
<span className="difftags" key={skillLevel} >{skillLevel}</span>
}
</section>
<span className="blogduration" key={duration} >{duration} min</span>
<section className="col2">
<a href={slug}>...more {'>'}{'>'}</a>
</section>
</article>
</div>
))
}
</div>
</div>
</div>
</div>
)
};
return (
<div className="posts__container">
<div className="posts">{renderPosts()}</div>
</div>
);
}
server.js
const express = require('express');
const bodyParser = require("body-parser");
const axios = require('axios');
const path = require('path');
const cors = require("cors");
const { get } = require('http');
const app = express()
const port = 5000
app.use(cors({
origin: "http://localhost:3000"
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/service/blogpost', async(req, res) => {
try {
const blogposts = await axios({
url: 'https://cdn.contentful.com/spaces/some_space_id/entries?access_token=some_token&limit=1000&skip=0',
method:"GET"
});
res.status(200).send(blogposts.data);
} catch (e) {
res.status(500).json({ fail: e.message });
}
})
app.listen(port, () => {
console.log(`Listening at http://localhost:${port}`)
})
App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from "react-router-dom";
import "./cssmodules/home.css";
import "./cssmodules/tutorialslist.css"
import "./cssmodules/singlepost.css";
import Home from "./components/Home";
import Tutorials from "./components/Tutorials";
import Navigation from './components/Navigation';
import TutorialsList from './components/TutorialsList';
import SinglePost from './components/SinglePost';
function App() {
return (
<BrowserRouter>
<Navigation/>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/tutorials" component={Tutorials} />
<Route path="/tutorialslist" component={TutorialsList} />
<Route path="/:id" component={SinglePost} />
</Switch>
</BrowserRouter>
);
};
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
serviceWorker.unregister();
I think that your problem is there:
useEffect(() => {
const results = searchResults.filter(blog =>
blog.fields.title.toLowerCase().includes(searchTerm) || blog.fields.title.toUpperCase().includes(searchTerm) || blog.fields.shortDescription.toLowerCase().includes(searchTerm)
|| blog.fields.shortDescription.toUpperCase().includes(searchTerm)
);
setSearchResults(results);
}, [searchTerm, searchResults]);
Method filter returns a new array, you save it with setSearchResults(), React calls rerender, this useEffect detect a new searchResults, run its callback... and again and again.
Maybe you need in useMemo to calculate filtered results or calculate it in imminently after receiving from the server?
Up
Maybe something like this:
// Initiate a state for fetched posts.
const [posts, setPosts] = useState([]);
// Fetch data from server on mount.
useEffect(() => {
const fetchData = async () => {
try {
const { data: { items } } = await axios.get('http://localhost:5000/service/blogpost');
setPosts(items);
} catch (e) {
console.log(e);
}
}
fetchData();
}, []);
// Extract Tags from Posts with memoization.
const tags = useMemo(() => {
return posts.reduce((result, post) => {
const { fields: { tags } } = post;
const normalizedTags = Array.isArray(tags) ? tags : [tags];
return [
...result,
...normalizedTags,
];
}, []);
}, [posts]);
// Filter Posts with memoization.
const filteredPosts = useMemo(() => {
const term = searchTerm.toLowerCase();
return posts.filter((post) => {
const title = post.fields.title.toLowerCase();
const description = post.shortDescription.title.toLowerCase();
return [title, description].includes(term); // You can extend the condition with a check of selected tags here.
});
}, [posts, searchTerm]);
// Render `tags` and `filteredPosts` in your template.
Sorry if I don't understand your task
Description of problem:
Changing the id (numbers only) of this url via the link tag does not update the page (but does change the url in the adress bar). Hitting refresh afterward will show the updated page.
http://localhost:8080/video/id/7564
Right clicking to open the link in a new tab, or changing the link path to a completely different page works as expected.
My app.js file
import React from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import RenderHomepage from '../components/homePage/RenderHomepage'
import RenderChannelPage from '../components/channelPage/RenderChannelPage'
import RenderVideoPage from '../components/videoPage/RenderVideoPage'
import RenderSearchPage from '../components/searchPage/RenderSearchPage'
import PageNotFound from '../components/PageNotFound'
import history from '../history'
const App = () => {
return (
<div>
<Router history={history}>
<Switch>
<Route path="/" exact component={RenderHomepage} />
<Route path="/channel" component={RenderChannelPage} />
<Route path="/video/id" component={RenderVideoPage} />
<Route path="/search" component={RenderSearchPage} />
<Route path="/404" exact component={PageNotFound} />
<Route component={PageNotFound} />
</Switch>
</Router>
</div>
)
}
export default App
Link tag in UpNextVideos component:
import React from 'react'
import { Link } from 'react-router-dom'
...
<Link to={{pathname: vid.id}}>
<h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
</Link>
...
How the components in question are nested:
<RenderVideoPage>
<VideoPage>
<UpNextVideos>
RenderVideoPage component:
import React from 'react'
import VideoPage from './VideoPage'
import Header from '../Header'
import HeaderMobile from '../HeaderMobile'
import FooterMobile from '../FooterMobile'
import ActivityFeed from '../ActivityFeed'
const RenderVideoPage = () => {
return (
<div className="videoPage-body">
<HeaderMobile />
<Header />
<ActivityFeed page={'home'} />
<VideoPage />
<FooterMobile page={'video'} />
</div>
)
}
export default RenderVideoPage
VideoPage component:
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import history from '../../history'
import handleMediaQueries from './containers/handleMediaQueries'
import setDislikes from './containers/setDislikes'
import NewSubscribers from './NewSubscribers'
import CommentSection from './CommentSection'
import UpNextVideos from './UpNextVideos'
import DescriptionBox from './DescriptionBox'
import VideoNotFound from './VideoNotFound'
import { fetchVideoFromID, fetchPictureFromID } from '../../containers/api'
import { thumbsUp, thumbsDown } from '../svgs'
import {
abbreviateNumber,
capitalizeFirstLetter,
randomDate } from '../../containers/helperFunctions'
const VideoPage = () => {
const [p, setPrefix] = useState("videoPage")
const [state, setState] = useState({
loading: true,
error: false
})
useEffect(() => {
if (state.loading) extractDataFromUrl()
else handleMediaQueries()
}, [state.loading])
const fetchVideo = async (id, picAuthorID) => {
let response = await fetchVideoFromID(id)
if (!response) setState(prevState => ({...prevState, error: true}))
else mapVideoResponseToHTML(response.data.hits, picAuthorID)
}
const mapVideoResponseToHTML = (response, picAuthorID) => {
let responseAsHtml = response.map(vid => {
return {
video:
<div className={`${p}-video-wrapper posRelative`} key={vid.id}>
<a className={`${p}-pixabay-src`} href={vid.pageURL}>?</a>
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
className={`${p}-video clickable`}
src={vid.videos.large.url}
controls autoPlay>
</video>
<div className={`${p}-video-info-wrapper`}>
<div className={`${p}-video-title-box`}>
<h1 className={`${p}-video-title`}>{capitalizeFirstLetter(vid.tags)}</h1>
<span className={`${p}-video-views`}>{abbreviateNumber(Number(vid.downloads).toLocaleString())} views</span>
<span className={`${p}-video-date`}>{randomDate()}</span>
</div>
<div className={`${p}-video-options`}>
<div className="thumbs">
<div className={`${p}-video-options-thumbsUp`}>{thumbsUp(20)}
<span className={`${p}-video-options-thumbsUp-text`}>{abbreviateNumber(vid.likes)}</span>
</div>
<div className={`${p}-video-options-thumbsDown`}>{thumbsDown(20)}
<span className={`${p}-video-options-thumbsDown-text`}>{setDislikes(vid.likes)}</span>
</div>
<div className={`${p}-video-options-likebar`}></div>
</div>
<span className={`${p}-video-options-share`}>Share</span>
<span className={`${p}-video-options-save`}>Save</span>
<span className={`${p}-video-options-ellipses`}>...</span>
</div>
</div>
</div>,
authorFollowers: vid.views,
vidAuthorID: vid.id,
author: picAuthorID ? 'Loading' : vid.user,
authorAvatar: picAuthorID ? null : vid.userImageURL,
views: vid.downloads
}
})
responseAsHtml = responseAsHtml[0]
setState(prevState => ({...prevState, ...responseAsHtml, loading: false}))
if (picAuthorID) fetchAuthorAvatar(picAuthorID)
}
const extractDataFromUrl = () => {
const currentURL = window.location.href
const urlAsArray = currentURL.split('/')
const urlID = urlAsArray[5].split('-')
const videoID = urlID[0]
const picAuthorID = urlID[1]
// Author avatars are random except on the home page.
// if url isnt from homepage, then use videoID
// if url is from homepage, send that avatarID
if (urlID.includes('000')) {
fetchVideo(videoID)
} else {
setState(prevState => ({...prevState, picAuthorID: picAuthorID}))
fetchVideo(videoID, picAuthorID)
}
}
const fetchAuthorAvatar = async (id) => {
const response = await fetchPictureFromID(id)
const authorName = response.data.hits[0].user
const authorAvatar = response.data.hits[0].previewURL
setState(prevState => ({
...prevState,
authorAvatar: authorAvatar,
author: capitalizeFirstLetter(authorName)
}))
}
return (
<div>
{ state.error ? <VideoNotFound /> : null}
{ state.loading === true ? null
:
<div className={`${p}-page-wrapper`}>
<main className={`${p}-main`}>
{state.video}
<DescriptionBox props={state} />
<div className={`${p}-suggested-videos-mobile`}></div>
<div className={`${p}-new-subscribers-wrapper`}>
<h2 className={`${p}-new-subscribers-text`}>{`New Subscribers to ${state.author}`}</h2>
<NewSubscribers />
</div>
<div className={`${p}-comment-section`}>
<CommentSection views={state.views}/>
</div>
</main>
<aside className={`${p}-sidebar`}>
<UpNextVideos />
</aside>
</div>
}
</div>
)
}
export default VideoPage
UpNextVideos component:
import React, { useEffect, useState, useRef, useCallback } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import { videoQuery } from '../../words'
import { fetchVideos } from '../../containers/api'
import {
capitalizeFirstLetter,
uuid,
getRandom,
abbreviateNumber
} from '../../containers/helperFunctions'
const UpNextVideos = () => {
const [p, setPrefix] = useState("videoPage")
const [nextVideos, setNextVideos] = useState([])
useEffect(() => {
fetchUpNextVideos(15, getRandom(videoQuery))
}, [])
// INFINITE SCROLL
const observer = useRef()
const lastUpNextVideo = useCallback(lastVideoNode => {
// Re-hookup observer to last post, to include fetch data callback
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver(entries => {
const lastVideo = entries[0]
if (lastVideo.isIntersecting && window.innerWidth <= 1000) {
document.querySelector('.videoPage-show-more-button').classList.add('show')
}
else if (lastVideo.isIntersecting && window.innerWidth > 1000) {
document.querySelector('.videoPage-show-more-button').classList.remove('show')
fetchUpNextVideos(20, getRandom(videoQuery))
}
})
if (lastVideoNode) observer.current.observe(lastVideoNode)
})
const fetchUpNextVideos = async (amount, query) => {
let response = await fetchVideos(amount, ...Array(2), query)
response = response.data.hits
const responseAsHtml = response.map((vid, index) => {
return (
<div className={`${p}-sidebar-grid-video-wrapper`} key={uuid()} ref={response.length === index + 1 ? lastUpNextVideo : null}>
<div className={`${p}-sidebar-grid-video`}>
<a href={`/video/id/${vid.id}-000`}>
<video
className={`${p}-upnext-video`}
onMouseOver={event => event.target.play()}
onMouseOut={event => event.target.pause()}
src={`${vid.videos.tiny.url}#t=1`}
muted >
</video>
</a>
</div>
<a href={`/video/id/${vid.id}`}>
<h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
</a>
<a href={`/channel/000${vid.id}`}>
<p className={`${p}-sidebar-grid-video-author`}>{vid.user}</p>
</a>
<p className={`${p}-sidebar-grid-video-views-text`}>{abbreviateNumber(vid.downloads)} views</p>
</div>
)
})
setNextVideos(prevState => ([...prevState, ...responseAsHtml]))
}
return (
<div>
<div className={`${p}-sidebar-text-top`}>
<span className={`${p}-sidebar-text-upnext`}>Up next</span>
<span className={`${p}-sidebar-text-autoplay`}>Autoplay</span>
</div>
<div className={`${p}-sidebar-grid-wrapper`}>
{nextVideos}
</div>
<button
className={`${p}-show-more-button`}
onMouseDown={() => fetchUpNextVideos(15, getRandom(videoQuery))}>
Show More
</button>
</div>
)
}
export default UpNextVideos
What I've tried:
Wrapping the <Link> tag with <Router history={history} />
Wrapping the <Link> tag with <BrowserRouter>
Wrapping the export statement withRouter(UpNextVideos)
Using a plain string instead of an object, as described in react-router-docs
Ok, I believe this issue lies in your VideoPage component.
useEffect(() => {
if (state.loading) extractDataFromUrl()
else handleMediaQueries()
}, [state.loading]);
You only ever have state.loading true once, when the component mounts. This only processes your URL once, so when the URL changes this component isn't aware of it.
This is your route currently
<Route path="/video/id" component={RenderVideoPage} />
now assuming your URLs are shaped "/video/id/" then you can define your route to have a parameter
<Route path="/video/id/:videoId" component={RenderVideoPage} />
If you wrap this component with react-router-dom's withRouter HOC you can easily get the id path param and add it to an effect to recompute all the video data.
export default withRouter(VideoPage)
withRouter injects the location, match, and history props from the closest Route ancestor. Here's an example of getting the id param and triggering an effect when its value updates.
const VideoPage = ({ match }) => {
const { params } = match;
useEffect(() => { /* do something with new id */ }, [params.videoId]);
}