'this' keyword is undefined inside Mapping Statement (React) - reactjs

The this keyword inside the vidsAsHtml mapping function keeps returning undefined.
I read this, and a couple other SO questions about this but their solutions did not solve the problem. I'm already using es6 syntax arrow function for the map, but I've also tried putting in this as a second argument, which didn't solve the issue. Curious if anyone knows why 'this' keyword keeps coming up as undefined here.
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const VideoGrid = (props) => {
const [videos, setResource] = useState([])
const fetchVideos = async (amount, category) => {
const response = await axios.get('https://pixabay.com/api/videos/', {
params: {
key: '123456679',
per_page: amount,
category: category
}
})
console.log(response)
const vidsAsHtml = response.data.hits.map(vid => {
return (
<div className={`${props.page}--grid-content-wrapper`} key={vid.picture_id}>
<div className={`${props.page}--grid-video`}>
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
onMouseOver={this.play()}
onMouseOut={this.pause()}
src={`${vid.videos.tiny.url}#t=1`} >
</video>
</div>
<div className={`${props.page}--grid-avatar-placeholder`}></div>
<div className={`${props.page}--grid-title`}>{vid.tags}</div>
<div className={`${props.page}--grid-author`}>{vid.user}</div>
<div className={`${props.page}--grid-views`}>{vid.views}
<span className={`${props.page}--grid-date`}> • 6 days ago</span>
</div>
</div>
)
})
setResource(vidsAsHtml)
}
useEffect(() => {
fetchVideos(50, 'people')
}, [])
return (
<main className={`${props.page}--grid-background`}>
<nav className={`${props.page}--grid-nav`}>
<button
id='followButton'
className={`${props.page}--grid-nav-${props.titleOne}`}
>{props.titleOne}
</button>
<button
id='recommendedButton'
className={`${props.page}--grid-nav-${props.titleTwo}`}
>{props.titleTwo}
</button>
<button
id='subscriptionsButton'
className={`${props.page}--grid-nav-${props.titleThree}`}
>{props.titleThree}
</button>
<button className={`${props.page}--grid-nav-${props.titleFour}`}>{props.titleFour}</button>
<button className={`${props.page}--grid-nav-${props.titleFive}`}>{props.titleFive}</button>
<button className={`${props.page}--grid-nav-follow`}>FOLLOW</button>
</nav>
<hr className={`${props.page}--grid-hr-nav-grey`} />
<hr className={`${props.page}--grid-hr-nav-black`} />
<div className={`${props.page}--grid`} style={{marginTop: 'unset'}}>
{videos}
</div>
</main>
)
}
export default VideoGrid

Event handler props are expected to be passed a function. Currently you are trying to pass the return values of this.play() and this.pause() as event handlers, which wouldn't work anyway.
Also React doesn't make the element available to the event handler via this, but you can access it via event.target:
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
onMouseOver={event => event.target.play()}
onMouseOut={event => event.target.pause()}
src={`${vid.videos.tiny.url}#t=1`} >
</video>

You can use ref for this,
let vidRef = React.createRef();
You should create function separately,
const playVideo = () => {
// You can use the play method as normal on your video ref
vidRef.current.play();
};
const pauseVideo = () => {
// Pause as well
vidRef.current.pause();
};
provide ref to video,
<video
ref = {vidRef} //Provide ref here
poster="https://i.imgur.com/Us5ckqm.jpg"
onMouseOver={() => playVideo()}
onMouseOut={() => pauseVideo()}
src={`${vid.videos.tiny.url}#t=1`} >
</video>
Demo

Related

Using refs and .reduce scroll to the id of selected element with react with useState

sorry if the title doesn't make much sense.
I've been refactoring my code from this.state to useState, and I finally got things working except for the refs...
In my original code I was making individual axios calls and using this.state along with this refs code:
const refs = response.data.reduce((acc, value) => {
acc[value.id] = createRef();
return acc;
}, {});
but now I refactored my axios call to .all:
const getData = () => {
const getSunSigns = axios.get(sunSignAPI);
const getDecans = axios.get(decanAPI);
const getNums = axios.get(numbersAPI);
axios.all([getSunSigns, getDecans, getNums, refs]).then(
axios.spread((...allData) => {
const allSunSigns = allData[0].data;
const getAllDecans = allData[1].data;
const getAllNums = allData[2].data;
setSunSigns(allSunSigns);
setDecanSign(getAllDecans);
setNumerology(getAllNums);
})
);
};
useEffect(() => {
getData();
}, []);
so the response.data.reduce doesn't work cuz I'm not using 'response'.
I've tried several things but none worked.. unfortunately I deleted all the previous code but this is what I currently have, which works but obviously only takes one api:
const refs = sunSigns.reduce((acc, value) => {
acc[value.id] = createRef();
return acc;
}, {});
onClick = (id) => {
refs[id].current.scrollIntoView({
behavior: "smooth",
});
};
from the research I've done and the code I've tried I'm sure I'd have to map through the apis and then maybe use the reduce(???).. but I'm really not entirely sure how to go about it or how to rephrase my google search to get more accurate results.
what I'm trying to do specifically: on certain pages an extra nav bar appears with the symbol of a specific sign/number. the user can click on one and it'll scroll to that specific one. I'm going to have several pages with this kind of feature so I need to dynamically set refs for each api.
any help or guidance will be highly appreciated!!
edit**
the above codes are in my Main component and this is where I'm setting the refs:
return (
<div className='main'>
<div className='main__side-container'>
<SideBar />
<div className='main__card-container'>
<Card
sunSigns={sunSigns}
numerology={numerology}
decanSign={decanSign}
refs={refs}
/>
</div>
</div>
<div className='main__bottom-container'>
<BottomBar
sunSigns={sunSigns}
numerology={numerology}
onClick={onClick}
/>
</div>
</div>
);
}
then this is the card:
export default function Card({ sunSigns, decanSign, refs, numerology }) {
return (
<>
<div className='card'>
<Switch>
<Route path='/astrology/western/zodiac'
render={(routerProps) => <Zodiac refs={refs} sunSigns={sunSigns} />}
/>
<Route path='/numerology/pythagorean/numbers'
render={(routerProps) => <NumberPage refs={refs} numerology={numerology} />}
/>
</Switch>
</div>
</>
);
}
and then this is the Zodiac page:
export default function Zodiac({ sunSigns, refs }) {
return (
<>
<div className='zodiac__container'>
<TitleBar text='ZODIAC :' />
<div className='card-inset'>
<div className='container-scroll'>
<SunSignsList sunSigns={sunSigns} refs={refs} />
</div>
</div>
</div>
</>
);
}
and the SunSignsList component:
export default function SunSignsList({ sunSigns, refs }) {
return (
<>
<div className='sunsignsitemlist'>
<ul>
{sunSigns.map(sign => {
return (
<SunSigns
refs={refs}
key={sign.id}
id={sign.id}
sign={sign.sign}
/>
);
})}
</ul>
</div>
</>
);
}
and the SunSigns component:
export default function SunSigns({
id,
sign,
decans,
refs
}) {
return (
<li ref={refs[id]}>
<section className='sunsigns'>
<div className='sunsigns__container'>
<div className='sunsigns__header'>
<h3 className='sunsigns__title'>
{sign}
{decans}
</h3>
<h4 className='sunsigns__symbol'>{symbol}</h4>
</section>
</li>
);
}
the above code is where my ref code is currently accessing correctly. but the end goal is to use them throughout several pages and comps in the same manner.
You can create three different objects holding the ref data for each list or if the id is same you can generate a single object which holds all the list refs.
const generateAllRefsObj = (...args) => {
const genSingleListRefsObj = (acc, value) => {
acc[value.id] = createRef();
return acc;
}
return args.reduce((acc, arg) => ({ ...arg.reduce(genSingleListRefsObj, acc), ...acc }), {})
}
Usage
const allRefs = generateAllRefsObj(sunSigns,decanSign,numerology)

State Doesn't calculate the new value : (

I need help
I have a state depending on another state and want to update the second state based on the first state in the code below the setTotalPrice Doesn't get the value of ingredientsPrice + pizzaPrice when I update the ingredientsPrice
import React, { useState } from "react";
import classes from "../../styles/Pages/Product.module.scss";
import Image from "next/image";
import axios from "axios";
function Product({ pizza }) {
// The State of Pizza Size
const [size, setSize] = useState(0);
const [ingredientsPrice, setIngredientsPrice] = useState(0);
const [pizzaPrice, setPizzaPrice] = useState(pizza.price[size]);
const [totalPrice, setTotalPrice] = useState(pizza.price[size]);
const handleIngredients = async (e, ingPrice) => {
// add ingredients Price on every change and call total handler fn();
if (e.target.checked) {
setIngredientsPrice((prevIngPrice) => prevIngPrice + ingPrice);
handleTotalPrice();
} else {
setIngredientsPrice((prevIngPrice) => prevIngPrice - ingPrice);
handleTotalPrice();
}
};
const handleTotalPrice = () => {
// Calc the pizza price + ing price and update total
setTotalPrice(pizzaPrice + ingredientsPrice);
};
return (
<div className={classes.Container}>
<div className={classes.Left}>
<div className={classes.ImgContainer}>
<Image
alt={pizza.title}
src={pizza.image}
layout="fill"
objectFit="contain"
/>
</div>
</div>
<div className={classes.Right}>
<h1 className={classes.Title}>{pizza.title}</h1>
<span className={classes.Price}>${totalPrice}</span>
<p className={classes.Description}>{pizza.description}</p>
<h3 className={classes.Choose}>Choose Your Size</h3>
<div className={classes.Sizes}>
<div
className={classes.SizeItem}
onClick={() => setSize(0)}
>
<Image
src={"/Images/size.png"}
alt="Small Size"
layout="fill"
/>
<span className={classes.Size}>Small</span>
</div>
<div
className={classes.SizeItem}
onClick={() => setSize(1)}
>
<Image
src={"/Images/size.png"}
alt="Small Size"
layout="fill"
/>
<span className={classes.Size}>Medium</span>
</div>
<div
className={classes.SizeItem}
onClick={() => setSize(2)}
>
<Image
src={"/Images/size.png"}
alt="Small Size"
layout="fill"
/>
<span className={classes.Size}>Large</span>
</div>
</div>
<h3 className={classes.Choose}>
Choose Additional Ingredients
</h3>
<div className={classes.Ingredients}>
{pizza.extraOptions.map((cur, index) => {
const trimedName = cur.extra.trim();
const ingPrice = cur.price;
return (
<div
className={classes.IngredientOption}
key={"Extra" + index}
>
<input
type={"checkbox"}
name={trimedName}
id={trimedName}
className={classes.Checkbox}
onChange={(e) =>
handleIngredients(e, ingPrice)
}
/>
<label htmlFor={trimedName}>{cur.extra}</label>
</div>
);
})}
</div>
<div className={classes.Quentity}>
<input type={"number"} defaultValue={1} max={5} min={1} />
<button>Add to Cart</button>
</div>
</div>
</div>
);
}
export default Product;
export async function getServerSideProps({ params }) {
const pizza = await axios.get(
"http://localhost:3000/api/products/" + params.id
);
return {
props: { pizza: pizza.data },
};
}
I expect the totalPrice will update automatically when ingredientsPrice updates
The problem is with this:
setIngredientsPrice((prevIngPrice) => prevIngPrice - ingPrice);
handleTotalPrice();
setIngredientsPrice actually changes the ingredientsPrice in an async manner, so it is surely possible that the code inside handleTotalPrice execute when ingredientsPrice is not yet updated thus resulting with the wrong totalPrice value.
To overcome this common problem in React, you need to move your handleTotalPrice function call to a useEffect hook, like this:
//update totalPrice when ingredientsPrice or pizzaPrice change
useEffect(() => {
setTotalPrice(ingredientsPrice + pizzaPrice);
//or handleTotalPrice();
},[ingredientsPrice, pizzaPrice]);
The function that we pass in useEffect hook will run whenever any of the variables listed inside dependency array (the array which we also pass into the useEffect) change.
State updates in React are asynchronous, that means they are not applied immediately but queued.
I would suggest that you put dependent state in one single object which you then update. ingredientsPrice and totalPrice are dependent. If you update one, you need to update the other as well (at least from my understanding), so put them together in an object state and update just that one state.
Be aware that you must provide a new object when you update state as only a no deep comparism is performed.
Have a look at this blog post for more details.

TypeError: .map is not a function - React application

I created this React application to practice the fetch API.
However, while writing the code to display the data on the browser via the map method, I got the error message "TypeError: profile.map is not a function". Below is the code:
import React, { Fragment, useEffect, useState } from "react";
import "./App.css";
function App() {
// https://reqres.in/api/users
const [profile, setProfile] = useState([]);
const [loading, setLoading] = useState(false);
const getProfile = async () => {
setLoading(true);
const response = await fetch("https://reqres.in/api/users");
const data = await response.json();
setProfile(data);
setLoading(false);
};
useEffect(() => {
getProfile();
}, []);
return (
<Fragment>
<h1>React fetch</h1>
<div className="main">
<section className="section">
<h2>Get database</h2>
<div>
{loading ? (
<Fragment>loading..</Fragment>
) : (
profile.map(i => {
<Fragment>
<ul>
<li>{i.id}</li>
<li>{i.email}</li>
<li>{i.first_name}</li>
<li>{i.last_name}</li>
<li>
<image src={i.avatar} />
</li>
</ul>
</Fragment>;
})
)}
</div>
</section>
<form className="section">
<h2>Post data</h2>
<input type="text" placeholder="enter detail" />
<button type="submit">Post</button>
</form>
<form className="section">
<h2>Update data</h2>
<select>
<option>Choose data</option>
</select>
<input type="text" placeholder="enter detail" />
<button type="submit">Update</button>
</form>
</div>
</Fragment>
);
}
export default App;
Why isn't map being recognized?
I believe it's because .map is a method for Array prototypes, not for Objects (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
You can return the array from the object by using data.data instead of just data:
...
const getProfile = async () => {
setLoading(true);
const response = await fetch("https://reqres.in/api/users");
const data = await response.json();
setProfile(data.data); // probably a safer way to do this, but if you console.log(data) you'll see an object is being returned, not an array.
setLoading(false);
};
...
So, const data = await response.json(); After this line is executed the result we are getting inside the data constant is an Object. We can use MAP function only on Array's, not on Objects. And also the Profile data which you are actually searching is inside the "data" key of the data "constant". So while setting profile data, just use setProfile(data.data);.
A suggestions: Use this Chrome Extension for viewing the API data. It indents the json objects automatically
Map needs to return a value.
{loading ? (
<Fragment>loading..</Fragment>
) : (
profile.map(i => {
return (
<Fragment>
<ul>
<li>{i.id}</li>
<li>{i.email}</li>
<li>{i.first_name}</li>
<li>{i.last_name}</li>
<li>
<image src={i.avatar} />
</li>
</ul>
</Fragment>;
)
})
)}
Also, you cannot use the map function on an object. It looks like your response is an object, what you are looking for is the data from the response. Try this...
setProfile(data.data);

react all classNames are affected in map()

import React from 'react'
import { useState, useEffect } from 'react'
import axios from 'axios'
const Home = () => {
const getSongs = () => {
axios.get('http://localhost:8000/api/songs/')
.then(res => setSongs(res.data))
}
let [songs, setSongs] = useState([])
let [paused, setPause] = useState(true)
useEffect(() => {
getSongs()
}, [])
const toggleSong = (id) => {
const x = document.getElementById(id)
if (x.paused){
x.play()
setPause(false)
} else {
x.pause()
setPause(true)
}
}
// Got rid of the functions that are not needed
return (
<>
{
songs.map(song =>
(
<div className='music-controller' key={song.id}>
<div id={'songDiv'} style={{cursor: 'pointer'}} onClick={(e) => changeSongTime(e, song.id)}>
<div id={`songTime-${song.id}`}></div>
</div>
<div className="music-controller-body">
<div className="music-controller-header">
<h2>{song.title}</h2>
<p><small>{song.genre}</small></p>
</div>
<div className="controls">
// here <----------------------
<i unique={song.id} className={`fas fa-${paused ? 'play' : 'pause'}`} onClick={() => toggleSong(song.id)}></i>
<audio id={song.id} onTimeUpdate={() => songTime(song.id)}>
<source src={`http://localhost:8000/api/songs/audio/${song.id}`} />
</audio>
</div>
</div>
</div>
))}
</>
)
}
export default Home
Whenever I click on a specific i element all of the i elements that were not clicked on get changed too.. to put it simply when I click on the 1st i element only its className should change, but all of the i elements classNames are affected, what is causing this?
I think you should use event.target
const handlePlay = (song) => {
song.play();
};
const handlePause = (song) => {
song.pause();
};
...
<div className="controls">
<i
onMouseOver={(e) => handlePlay(e.target)}
onMouseLeave={(e) => handlePause(e.target)}
className={`fas fa-${paused ? 'play' : 'pause'}`}
onClick={() => toggleSong(song.id)}>
</i>
<audio id={song.id} onTimeUpdate={() => songTime(song.id)}>
<source src={`http://localhost:8000/api/songs/audio/${song.id}`} />
</audio>
</div>
I don't think Toggle would work in this case, an action should happen so it knows when it should stop.
Can you put console in toggleSong function at top and check if you are getting correct id. If you are not getting single Id then work is needed with onClick. So, after that also try passing id like this
onClick={(song?.id) => toggleSong(song?.id)}
then see console again and look for correct id if it is displayed or not. I think your className is not updating due to this issue.
One thing more you can try at end is replacing with this
const x = id; //without document.getElementById
const toggleSong = (e, id) => {
const x = document.getElementById(id)
const button = e.currentTarget
if (x.paused){
x.play()
button.className = 'fas fa-pause'
} else {
x.pause()
button.className = 'fas fa-play'
}
}
<i unique={song.id} className='fas fa-play' onClick={(e) => toggleSong(e, song.id)}></i>
I fixed this by just getting the current target with event.currentTarget and change its className accordingly!

actual props not render (react)

I'm trying to render actual data in child component, but data does not render. What is wrong?
Parent component
const UserPanelContainer = ({ currentUser }) => {
const [initUsersData, setinitUsersData] = useState(currentUser);
useEffect(() => {
console.log('useEffect')
setinitUsersData(()=>getnewData())
}, [setinitUsersData, currentUser])
const getnewData = () =>{
console.log('getnewData')
setinitUsersData(currentUser)
}
return (
<UserPanel currentUser={initUsersData} hanleOnClickOut={hanleOnClickOut} >{console.log('usepanContainerRender')}</UserPanel>
);
};
export default UserPanelContainer;
child
const UserPanel = ({ currentUser, hanleOnClickOut }) => {
console.log(currentUser);
return (
<div className="dropdown">
{console.log('userPanelRender')}
<button
className="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<img
className="avatar"
src={currentUser.photoURL}
alt="avatar"
/>
{currentUser.displayName}
</button>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
<div className="dropdown-item">
Вошел как {currentUser.displayName}
</div>
<div className="dropdown-item" onClick={hanleOnClickOut}>
Выйти
</div>
</div>
</div>
);
};
export default UserPanel;
In console in child I can see correct actual data in props, but they are not rendered.
Actual data contains "currentUser" prop. But on Browser page i cant see data....
(if i delete currentUser from useEffect depencity i can see data from previus API call)
I see you are passing the setinitUsersData in the useEffect dependency array whereas you need to pass the actual state variable
try this,
useEffect(() => {
...
}, [initUsersData, currentUser])
instead of current,
useEffect(() => {
...
}, [setinitUsersData, currentUser])
I think by actual data you mean some api response.
Try this :-
useEffect(() => {
console.log('useEffect')
getnewData(currentUser)
}, [currentUser])
const getnewData = (currentUser) =>{
console.log('getnewData')
axios.get("/pathToData").then((res) => {
console.log(res);
setinitUsersData(res);
})
}
Replace parent component with the following code. You don't need to use useEffect as per the code you've posted. Since parent is already receiving currentUser and you have already updated state with that
const UserPanelContainer = ({ currentUser }) => {
const [initUsersData, setinitUsersData] = useState(currentUser);
return (<UserPanel
currentUser={initUsersData}
hanleOnClickOut={hanleOnClickOut}>{console.log('usepanContainerRender')}</UserPanel>
);
};
export default UserPanelContainer;

Resources