I'm fairly new to React development and hope someone can help me with this problem. I'm coding along with a YouTube video https://www.youtube.com/watch?v=XtMThy8QKqU&t=10138s (2:55:00 shows what it is supposed to do)and for some reason I can't find the mistake I'm making. When I test my app on localhost the window in which the trailer is supposed to play is only displayed when I click certain movie covers but not when I click on others. my other problem is that it will never actually play a trailer. The console displays the error you can hopefully see here [1]: https://i.stack.imgur.com/vC6Sh.jpg
import movieTrailer from "movie-trailer";
import React, { useEffect, useState } from "react";
import YouTube from "react-youtube";
import axios from "./axios";
import "./Row.css"
const base_url = "https://image.tmdb.org/t/p/original/";
function Row({ title, fetchUrl, isLargeRow }) {
const [movies, setMovies] = useState([]);
const [trailerUrl, setTrailerUrl] = useState("");
//A snippet of code which runs based on a specific condition or variable
useEffect(() => {
// if brackets are blank [] it means run once when row loads, and don't run again
async function fetchData() {
const request = await axios.get(fetchUrl);
// console.log(request.data.results);
setMovies(request.data.results)
return request;
// async function fetchData() {
// try{
// const request = await axios.get(fetchUrl);
// console.log(request);
// return request;
// }
// catch (error){
// console.log(error);
// }
}
fetchData();
}, [fetchUrl]);
const opts = {
height: '390',
width: '100%',
playerVars: {
// https://developers.google.com/youtube/player_parameters
autoplay: 1,
},
};
//console.log(movies);
const handleClick = (movie) => {
if (trailerUrl){
setTrailerUrl('');
} else {
movieTrailer(movie?.name || "")
.then ((url) => {
const urlParams = new URLSearchParams(new URL(url).search);
setTrailerUrl(urlParams.get("v"));
}).catch(error => console.log(error));
}
};
return(
<div className="row">
<h2>{title}</h2>
<div className="row__posters">
{movies.map(movie => (
<img
key={movie.id}
onClick={() => handleClick(movie)}
className= {`row__poster ${isLargeRow && "row__posterLarge"}`}
src={`${base_url}${isLargeRow ? movie.poster_path : movie.backdrop_path}`} alt={movie.name}/>
))}
</div>
{trailerUrl && <YouTube videoId="{trailerUrl}" opts={opts} /> }
</div>
)
}
export default Row
Invalid property name in movie
Taking a look at the tmdb docs it will show you what the properties of each object has. In this case, there is no name. Try using something like movie.title
In your handleClick() function you could use movie?.title.
Trying to use movie.name will give back a null value. Which errors out movieTrailer() and you get no YouTube url back.
Create handle function like this and the call it in your return function and use however you want... mainly should be used by using onClick method
Related
I am having a hard time getting my React App working properly.
The thing is that I tried to use UseEffect hooks only to run side effects in my app and this has brought me some problems.
In this simple component I have a chat that get data from Firebase and is capable of updating the Db. I have no problem with the Firebase side but on the front end, the first render is not able to get me the messages into state properly.
I feel that it has of course something to do with async behaviors.
I will try to explain you the flow of my component :
The message text is kept in a const in state call "inputText"; when the form is submited a const call "numberOfMessageSent" is incremented; I have a UseEffect Hook that has [numberOfMessageSent] in its depedency; so after the first mount of the component and when "NumberOfMessageSent" increments the callback will fire; this callback fires 2 async functions: one to fetch the current discussion from the db and another to create a discussion object or update an existing one into the Db. I have a condition :
"numberOfMessagesSent !== 0 && asyncWarperCreateDiscussionInDb()" in the UseEffect Hook so a new discussion empty discussion won't be created the first this component mount.
My problem is that no discussion is displayed (nor properly fetched and stored into state) BEFORE I send a first message. After I send this first message everything works properly.
Can someone help me to understand this better ?
Thank you very much
here is my code :
import React, { useContext, useEffect, useState } from "react";
import "./card-medium-message.style.scss";
import likeEmpty from "./like-empty.png";
import likeFull from "./like-full.png";
import cancel from "./cancel.png";
import send from "./send.png";
import back from "./back.png";
import { useNavigate, useParams } from "react-router-dom";
import { UsersListContext } from "../../context/usersList-context/users-list-context";
import { UserContext } from "../../context/user-context/user-context";
import {
createDiscussionInDb,
goFetchDiscussionInDb,
goFetchDisscussion,
} from "../../utils/firebase";
const CardMediumMessage = () => {
const params = useParams();
const { usersListCTX } = useContext(UsersListContext);
const { currentUserContext } = useContext(UserContext);
const currentUserClickedOn = usersListCTX.filter(
(user) => user.displayName === params.name
);
console.log(currentUserContext);
console.log(currentUserClickedOn[0]);
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState("");
const [numberOfMessagesSent, setNumberOfMessagesSent] = useState(0);
const asyncWarperFetchDiscussionInDb = async () => {
if (currentUserClickedOn[0]) {
const discussion = await goFetchDiscussionInDb(
currentUserContext.displayName,
currentUserClickedOn[0].displayName
);
setMessages(discussion.messages);
}
};
const asyncWarperCreateDiscussionInDb = async () => {
await createDiscussionInDb(
currentUserContext.displayName,
currentUserClickedOn[0].displayName,
inputText
);
resetField();
};
useEffect(() => {
numberOfMessagesSent !== 0 && asyncWarperCreateDiscussionInDb();
asyncWarperFetchDiscussionInDb();
console.log(
"this is written after first render of the component or numberOfMessagesSent was updated"
);
}, [numberOfMessagesSent]);
const messageSubmit = async (e) => {
e.preventDefault();
if (inputText == "") {
return;
}
setNumberOfMessagesSent(numberOfMessagesSent + 1);
};
const textChanged = (e) => {
setInputText(e.target.value);
};
const resetField = () => {
setInputText("");
};
const navigate = useNavigate();
messages && console.log(messages);
return (
<div className="card-medium-warp">
<div className="card-medium-message">
<div className="section1" onClick={() => navigate(-1)}>
<div className="profile-image-outer-circle">
{currentUserClickedOn[0] ? (
<img
src={`https://api.dicebear.com/5.x/micah/svg?seed=${currentUserClickedOn[0].displayName}`}
alt="avatar"
className="profile-image"
/>
) : undefined}
</div>
{currentUserClickedOn[0] ? (
<h2 className="name">{currentUserClickedOn[0].displayName} </h2>
) : undefined}
<div
className="back"
style={{ backgroundImage: `url(${back})` }}
></div>
</div>
<div className="section2">
{messages
? messages.map((messageObject, index) => (
<p
key={index}
className={
messageObject.by === currentUserContext.displayName
? "sender-message"
: "receiver-message"
}
>
{messageObject.message}
</p>
))
: undefined}
</div>
<form className="section3" onSubmit={messageSubmit}>
<input
type="text"
className="input"
placeholder="your message"
onChange={textChanged}
value={inputText}
autoFocus
/>
<div
className="send-message"
style={{ backgroundImage: `url(${send})` }}
></div>
</form>
</div>
</div>
);
};
export default CardMediumMessage;
I think I found the solution so I would like to share it :
My mistake was that I was calling functions that were async in themselves but I didn't chain them in an async/await manner.
This is what I am talking about :
const asyncWarperSequence = async () => {
numberOfMessagesSent !== 0 && (await asyncWarperCreateDiscussionInDb());
await asyncWarperFetchDiscussionInDb();
};
useEffect(() => {
console.log("UseEffect Fired");
asyncWarperSequence();
}, [numberOfMessagesSent]);
I'm writing tests for a React component Order that renders a user's order and allows the user to view items on the order by clicking on a "View items" button, which triggers an API call.
import { useState, useEffect } from "react";
import Skeleton from "react-loading-skeleton";
import { Customer } from "../../../api/Server";
import capitalise from "../../../util/capitalise";
import renderOrderTime from "../../../util/renderOrderTime";
const Order = props => {
// Destructure props and details
const { details, windowWidth, iconHeight, cancelOrder } = props;
const { id, createdAt, status } = details;
// Define server
const Server = Customer.orders;
// Define order cancel icon
const OrderCancel = (
<svg className="iconOrderCancel" width={iconHeight} height={iconHeight} viewBox="0 0 24 24">
<path className="pathOrderCancel" style={{ fill:"#ffffff" }} d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5 15.538l-3.592-3.548 3.546-3.587-1.416-1.403-3.545 3.589-3.588-3.543-1.405 1.405 3.593 3.552-3.547 3.592 1.405 1.405 3.555-3.596 3.591 3.55 1.403-1.416z"/>
</svg>
)
// Set state and define fetch function
const [items, setItems] = useState([]);
const [fetchItems, setFetchItems] = useState(false);
const [isLoadingItems, setIsLoadingItems] = useState(false);
const [error, setError] = useState(null);
const fetchOrderItems = async() => {
setIsLoadingItems(true);
try {
let order = await Server.getOrders(id);
setItems(order.items);
setIsLoadingItems(false);
} catch (err) {
setError(true);
console.log(err);
}
}
useEffect(() => {
if (items.length === 0 && fetchItems) fetchOrderItems();
// eslint-disable-next-line
}, [fetchItems]);
// Define function to view order items
const viewItems = e => {
e.preventDefault();
setFetchItems(fetchItems ? false : true);
const items = document.getElementById(`items-${id}`);
items.classList.toggle("show");
}
// RENDERING
// Order items
const renderItems = () => {
// Return error message if error
if (error) return <p className="error">An error occurred loading order items. Kindly refresh the page and try again.</p>;
// Return skeleton if loading items
if (isLoadingItems) return <Skeleton containerTestId="order-items-loading" />;
// Get total cost of order items
let total = items.map(({ totalCost }) => totalCost).reduce((a, b) => a + b, 0);
// Get order items
let list = items.map(({ productId, name, quantity, totalCost}, i) => {
return (
<div key={i} className="item" id={`order-${id}-item-${productId}`}>
<p className="name">
<span>{name}</span><span className="times">×</span><span className="quantity">{quantity}</span>
</p>
<p className="price">
<span className="currency">Ksh</span><span>{totalCost}</span>
</p>
</div>
)
});
// Return order items
return (
<div id={`items-${id}`} className={`items${items.length === 0 ? null : " show"}`} data-testid="order-items">
{list}
{
items.length === 0 ? null : (
<div className="item total" id={`order-${id}-total`}>
<p className="name">Total</p>
<p className="price">
<span className="currency">Ksh</span><span>{total}</span>
</p>
</div>
)
}
</div>
)
};
// Component
return (
<>
<div className="order-body">
<div className="info">
<p className="id">#{id}</p>
<p className="time">{renderOrderTime(createdAt)}</p>
<button className="view-items" onClick={viewItems}>{ fetchItems ? "Hide items" : "View items"}</button>
{renderItems()}
</div>
</div>
<div className="order-footer">
<p className="status" id={`order-${id}-status`}>{capitalise(status)}</p>
{status === "pending" ? <button className="cancel-order" onClick={cancelOrder}>{windowWidth > 991 ? "Cancel order" : OrderCancel}</button> : null}
</div>
</>
)
}
export default Order;
Below are the tests I'm writing for Order. I've run into some trouble writing tests on the API call.
import { render, fireEvent, screen } from "#testing-library/react";
import { act } from "react-dom/test-utils";
import Order from "../../../components/Primary/Orders/Order";
import { Customer } from "../../../api/Server";
import { orders } from "../../util/dataMock";
// Define server
const Server = Customer.orders;
// Define tests
describe("Order", () => {
describe("View items button", () => {
const mockGetOrders = jest.spyOn(Server, "getOrders");
beforeEach(() => {
const { getAllByRole } = render(<Order details={orders[2]} />);
let button = getAllByRole("button")[0];
fireEvent.click(button);
});
test("triggers API call when clicked", () => {
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", () => {
let skeleton = screen.getByTestId("order-items-loading");
expect(skeleton).toBeInTheDocument();
});
///--- PROBLEMATIC TEST ---///
test("renders error message if API call fails", async() => {
await act(async() => {
mockGetOrders.mockRejectedValue("Error: An unknown error occurred. Kindly try again.");
});
// let error = await screen.findByText("An error occurred loading order items. Kindly refresh the page and try again.");
// expect(error).toBeInTheDocument();
});
});
test("calls cancelOrder when button is clicked", () => {
const clickMock = jest.fn();
const { getAllByRole } = render(<Order details={orders[2]} cancelOrder={clickMock} />);
let button = getAllByRole("button")[1];
fireEvent.click(button);
expect(clickMock).toBeCalled();
});
});
On the test that I've marked as problematic, I'm expecting the mocked rejected value Error: An unknown error occurred. Kindly try again. to be logged to the console, but instead I'm getting a TypeError: Cannot read properties of undefined (reading 'items'). This indicates that my mock API call isn't working properly and the component is attempting to act on an array of items it's supposed to receive from the API. How should I fix my test(s) to get the desired result?
I was experimenting with a simplified example of your case and it seems the issue is with the use of the beforeEach hook in which you render the component and click the button before each test. This is not a good idea when it comes to RTL, in fact, there's an Eslint rule for this.
In order to avoid repeating yourself on each test, you can use a setup function to perform these actions on each one of the tests:
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import Order from "../../../components/Primary/Orders/Order";
import { Customer } from "../../../api/Server";
import { orders } from "../../util/dataMock";
// Define server
const Server = Customer.orders;
// Setup function
const setup = () => {
render(<Order details={orders[2]} />);
const button = screen.getAllByRole("button")[0];
userEvent.click(button);
}
// Define tests
describe("Order", () => {
describe("View items button", () => {
const mockGetOrders = jest.spyOn(Server, "getOrders");
test("triggers API call when clicked", () => {
setup()
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", async () => {
setup()
const skeleton = await screen.findByTestId("order-items-loading");
expect(skeleton).toBeInTheDocument();
});
test("renders error message if API call fails", async () => {
mockGetOrders.mockRejectedValue(new Error("An unknown error occurred. Kindly try again."));
setup()
const error = await screen.findByText("An error occurred loading order items. Kindly refresh the page and try again.");
expect(error).toBeInTheDocument();
});
});
test("calls cancelOrder when button is clicked", () => {
const clickMock = jest.fn();
render(<Order details={orders[2]} cancelOrder={clickMock} />);
const button = screen.getAllByRole("button")[1];
userEvent.click(button);
expect(clickMock).toBeCalled();
});
});
I also made some minor changes like: using userEvent instead of fireEvent, using screen instead of destructuring the query functions from render's result, etc. These changes are all recommended by the creator of the library.
Also, in the fetchOrderItems function, you should set the loading flag to false in case an exception is thrown.
Note: The act warning might be coming from tests #1 and #2. This is probably because the test cases are ending before all component's state updates have finished (e.g. the loading state toggling after clicking the fetch button). It's not recommended to use act on RTL, instead you can modify these tests and use the waitForElementToBeRemoved utility from RTL. This will still satisfy test #2, in which you check for the loading skeleton presence since it will throw anyways if it can't find this loading UI:
test("triggers API call when clicked", async () => {
setup()
await waitForElementToBeRemoved(() => screen.queryByTestId("order-items-loading"))
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", async () => {
setup()
await waitForElementToBeRemoved(() => screen.queryByTestId("order-items-loading"))
});
Let me know if this works for you.
Currently working on a stock project for my portfolio and I am using finnhub as the API.
I can log everything to my console. However I cannot render it as the "data" is not globally declared and must be inside of a certain function.
I tried rendering globally but had no luck...
So my question is how do I make 'data' global so that I can render it inside of the "StockHeader's" return ?
Heres what I have so far...
import React,{ useState, useEffect } from 'react';
const StockHeader = (data) => {
const [stocks, setStocks] = useState({});
const getStocks = () => {
//setting stocks
setStocks(stocks)
}
//calling it once
useEffect(()=> {
getStocks();
}, [])
//using finhubs ready made code from documentation
const finnhub = require('finnhub');
const api_key = finnhub.ApiClient.instance.authentications['api_key'];
api_key.apiKey = "my apikey"
const finnhubClient = new finnhub.DefaultApi()
finnhubClient.quote("AAPL", (error, data, response) => {
//I can log the data but I cant show it in my component
console.log(data.c)
});
return (
<>
{/* This says that data is not defined */}
<h1>{data.c}</h1>
</>
)
}
export default StockHeader
You just need a little bit of code reorganization so that the API request only happens once and so that you can use setStocks to store it:
const StockHeader = (data) => {
const [stocks, setStocks] = useState({});
useEffect(()=> {
//this could be separated into a `getStocks` function if you want
const finnhub = require('finnhub');
const api_key = finnhub.ApiClient.instance.authentications['api_key'];
api_key.apiKey = "my apikey"
const finnhubClient = new finnhub.DefaultApi()
finnhubClient.quote("AAPL", (error, data, response) => {
console.log(data.c);
setStocks(data.c);
});
}, []);
return (
<>
{/* You probably don't want to render `stocks` itself, but this shows you how to get access to the variable */}
<h1>{stocks}</h1>
</>
)
}
So I want to do a simple image fetch from API. My goal is to display random image from API. Now it says "Data" is not defined. I have no idea why it does that because my console.logs were working before trying to show it on page.
This is my App.js
import React,{useEffect, useState} from 'react';
import Dog from './doggo';
//Main component
function App() {
const [dogs, setDog] = useState();
useEffect(() => {
getDog();
}, []);
//Function to get data
const getDog = async () => {
//Fetch from url
const response = await fetch("https://dog.ceo/api/breeds/image/random");
//Store answer in data
const data = await response.json();
//Place data.message in setDog
setDog(data.message);
console.log(data.status);
//data.message contains image url
console.log(data.message);
};
return(
<div>
<h1>Press button to see your future dog!</h1>
<button type="submit">See your dog!</button>
<Dog
image={data.message}
/>
</div>
);
};
export default App;
I reformatted you code a bit to take care of some issues.
As other commenters have stated, data is out of scope where you're trying to access it. (It's only available inside of the getDog() function.)
export default function App() {
const [dog, setDog] = useState();
const getDog = async () => {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
const data = await response.json();
setDog(data.message);
};
return (
<div>
<h1>Press button to see your future dog!</h1>
<button
onClick={() => {
getDog();
}}
>
See your dog!
</button>
{dog ? <Dog image={dog} /> : null}
</div>
);
}
Working Codepen
use {dogs} instead of {data.message} in <Dog image={data.message}/> data is a variable only for the getDog() function.
I'm using OpenWeatherAPI, i got the fetch working fine. I'm able to useState to put that successful fetch data into the state. The console log shows up and i can see the request made in the network tab.
However there is something funny going on, my .map()ed data isn't rendering every time as i expect it to. I will write the code, press save and it will show up on the screen. However if i refresh page or restart server it just doesn't show up. Sometimes it shows up after a few refreshes.
I'm most likely doing something wrong the hooks system. Please point out what i'm doing incorrectly.
I can't just directly use the list i put in state after the promise is resolved, i need to filter out the response i just set in state and only get the keys/vals i need hence why you see the second state for filteredForecasts. Why is it only periodically working now and then? I feel like i have all the correct null check if statements yet it still doesn't work as expected...
import React from "react";
import WeatherCard from '../WeatherCard';
import "./WeatherList.scss";
const WeatherList = (props) => {
return (
<div className="weather-list-container">
<WeatherCard />
</div>
);
};
export default WeatherList;
import React, { useState, useEffect } from "react";
import "./WeatherCard.scss";
import { getForecast } from "../../api/GET/getForecast";
const WeatherCard = () => {
const [forecasts, setForecasts] = useState([]);
const [filteredForecasts, setFilteredForecasts] = useState([]);
useEffect(() => {
getForecast()
.then((res) => {
const { list } = res;
setForecasts(list);
})
.catch((err) => {
console.log(err);
});
}, []);
useEffect(() => {
if (forecasts.length) {
const uniqueForecasts = Array.from(
new Set(allRelevantData.map((a) => a.day))
).map((day) => {
return allRelevantData.find((a) => a.day === day);
});
setFilteredForecasts(uniqueForecasts);
}
}, []);
const allRelevantData = Object.entries(forecasts).map(([key, value]) => {
const dateTime = new Date(value.dt * 1000);
const day = dateTime.getDay();
const item = {
day: day,
temp: Math.round(value.main.temp),
weatherMetaData: value.weather[0],
};
return item;
});
return filteredForecasts && filteredForecasts.map(({ day, temp, weatherMetaData }) => {
return (
<div className="weather-card">
<div className="day-temperature-container">
<span className="day">{day}</span>
<span className="temperature">{temp}</span>
</div>
<div className="weather-description">
<span
className="icon weather"
style={{
background: `url(http://openweathermap.org/img/wn/${weatherMetaData.icon}.png)`,
}}
/>
<p>{weatherMetaData.description}</p>
</div>
</div>
);
});
};
export default WeatherCard;
import openWeatherConfig from '../../config/apiConfig';
const {baseUrl, apiKey, londonCityId} = openWeatherConfig;
export function getForecast(cityId = londonCityId) {
return fetch(`${baseUrl}/forecast?id=${cityId}&units=metric&appid=${apiKey}`)
.then(res => res.json());
}
PROBLEM
useEffect only runs on mount when it an empty array dependency in which case it might be highly likely the forecast is empty.
SOLUTION
filteredForecast is a derivative property of forecast state. Remove it from the state and use it without the React.useEffect.
const allRelevantData = Object.entries(forecasts).map(([key, value]) => {
const dateTime = new Date(value.dt * 1000);
const day = dateTime.getDay();
const item = {
day: day,
temp: Math.round(value.main.temp),
weatherMetaData: value.weather[0],
};
return item;
});
let filteredForecasts = null;
if (forecasts.length) {
filteredForecasts = Array.from(
new Set(allRelevantData.map((a) => a.day))
).map((day) => {
return allRelevantData.find((a) => a.day === day);
});
return /** JSX **/
You're passing an empty dependency array to your second (filtered forecasts) useEffect call, which means it will run only when the component mounts. If your first effect hasn't returned yet, your filtered forecasts will never see any data.
You probably don't need the second useEffect call at all. Just compute it when the forecasts come back in the first effect.