So this is my code, testing out a chatbot. useEffect is not working when I refresh, the automatic scroll doesn't work when new message is being received or sent.. what am I missing?
import './App.css';
import './normal.css';
import { useState, useRef, useEffect } from 'react';
function App() {
const messagesEndRef = useRef(null);
const [input, setInput] = useState("");
const [chatLog, setChatLog] = useState([{
user: "gpt",
message: "Hello World"
}])
function clearChat(){
setChatLog([]);
}
useEffect(() => {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
}, [chatLog]);
async function handleSubmit(e){
e.preventDefault();
let chatLogNew = [...chatLog, { user: "me", message: `${input}` } ]
await setInput("");
setChatLog(chatLogNew)
const messages = chatLogNew.map((message) => message.message).join("\n")
const response = await fetch("http://localhost:3080/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: messages
})
});
const data = await response.json();
setChatLog([...chatLogNew, { user: "gpt", message: `${data.message}`}])
}
return (
<div className="App">
<aside className ="sidemenu">
<div className="title">
<h1>Title</h1>
</div>
<div className="side-menu-button" onClick={clearChat}>
<span>+</span>
New chat
</div>
</aside>
<section className="chatbox">
<div className="chat-log">
{chatLog.map((message, index) => (
<ChatMessage key={index} message={message} />
))}
</div>
<div ref={messagesEndRef} />
<div className="chat-input-holder">
<form onSubmit={handleSubmit}>
<input
rows="1"
value={input}
onChange={(e)=> setInput(e.target.value)}
className="chat-input-textarea">
</input>
</form>
</div>
</section>
</div>
);
}
const ChatMessage = ({ message }) => {
return (
<div className={`chat-message ${message.user === "gpt" && "chatgpt"}`}>
<div className="chat-message-center">
<div className={`avatar ${message.user === "gpt" && "chatgpt"}`}>
{message.user === "gpt" && "AI"}
{message.user === "me" && "Me"}
</div>
<div className="message">
{message.message}
</div>
</div>
</div>
)
}
export default App;
Defined the messagesEndRef and inserted the useEffect and put in the dummy div to the last rendered message.
Any ideas? Am I formatting it wrong?
EDIT:
It's working now but I have to have chat-log set to "overflow:scroll"
otherwise it doesn't kick in.. for example "overflow:auto" doesn't
work.
When it DOES work, it also doesn't scroll to the very end of the box
but slightly above.
Any solution to this?
Instead of using chatLog in the second argument array of useEffect(), use [JSON.stringify(chatLog)] or chatLog.length
useEffect(() => {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
}, [JSON.stringify(chatLog)]);
scrollIntoViewOptions are:
behavior (Optional)
Defines the transition animation. One of auto or smooth. Defaults to auto.
block (Optional)
Defines vertical alignment. One of start, center, end, or nearest. Defaults to start.
inline (Optional)
Defines horizontal alignment. One of start, center, end, or nearest. Defaults to nearest.
So, you will need to add block option to end.
useEffect(() => {
messagesEndRef.current.scrollIntoView({ behavior: "smooth", block: "end" })
}, [chatLog]);
Solved it by adding this code:
useEffect(() => {
const chatLogElement = messagesEndRef.current;
const currentScrollTop = chatLogElement.scrollTop;
const targetScrollTop = chatLogElement.scrollHeight - chatLogElement.clientHeight;
const scrollDiff = targetScrollTop - currentScrollTop;
let startTime;
function scroll(timestamp) {
if (!startTime) {
startTime = timestamp;
}
const elapsedTime = timestamp - startTime;
const progress = elapsedTime / 200;
chatLogElement.scrollTop = currentScrollTop + (scrollDiff * progress);
if (progress < 1) {
window.requestAnimationFrame(scroll);
}
}
window.requestAnimationFrame(scroll);
}, [chatLog.length]);
Now it works, even when overflow is set to "auto"
Related
Here is my App()
import React, { useState, useEffect } from "react";
import { useParams } from "react-router";
const ComponentTest = () => {
const { _sid } = useParams();
const [sid, setsid] = useState(_sid);
const [myData, setmyData] = useState({
message: "",
file: "",
createTime: "",
});
const onClick = async () => {
const resopnse = await fetch("http://127.0.0.1:5100/api/get?_sid=" + sid);
const resopnseJson = await resopnse.json();
setmyData({
...myData,
message: resopnseJson.message,
file: resopnseJson.file,
});
};
return (
<div>
<button
className="btn btn-outline-primary form-control"
data-bs-toggle="modal"
data-bs-target="#myModal"
onClick={onClick}
>
Test
</button>
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">...</div>
</div>
</div>
</div>
);
};
The problem is when the button is clicked, the modal appears then data is loaded.
What I want is: First fetch data, then show up the modal.
Do I need to use useEffect? and how? Thanks!
Any good ways to learn hooks?
The first issue which needs to be resolved is the modal showing up without a condition. So go ahead and wrap the modal container div with a condition which will always render the modal (will change this in later):
const YourComponent = () => {
....
return (
<div>
<button>Test</button>
{true === true ? <div class="modal" id="myModal">...</div> : null}
</div>
)
}
This way, you are not just rendering the modal, you are also controlling its render. Next you have to figure out a way to set the value of the condition placeholder (true === true). This can be done with state, but looking at your code, I think that it is easier and more efficient to add a flag in your data state and set it to true whenever you get a response. Then use that flag to see if the modal should or should not render:
const ComponentTest = () => {
const { _sid } = useParams();
const [sid, setsid] = useState(_sid);
const [myData, setmyData] = useState({
message: "",
file: "",
createTime: "",
loaded: false
});
const onClick = async () => {
const resopnse = await fetch("http://127.0.0.1:5100/api/get?_sid=" + sid)
const resopnseJson = await resopnse.json();
setmyData(
{ ...myData, message: resopnseJson.message, file: resopnseJson.file, loaded: true }
)
}
return (
<div>
<button className="btn btn-outline-primary form-control" data-bs-toggle="modal" data-bs-target="#myModal" onClick={onClick}>Test</button>
{myData.loaded === true ? <div class="modal" id="myModal">...</div> : null}
</div>
)
}
const [showModal,setShowModal] = useState(false);
when you click on the button to fetch data you set showModal to true
const onClick = async () => {
//...
setShowModal(true); // add this line
}
Now in your jsx you check if showModal is true so you display it :
return(
//...
{showModal ? (
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">...</div>
</div>
</div>
) : ''}
);
don't forget to create a button in your modal to close it
<button
onClick={() => {
setShowModal(false);
}}>
Close Modal
</button>
I want to update render when a special property changes. This property income from parents. I Made a useState called loader to handle codes when I have data or not. if the loader is false, my code calls API and if it is true render data.
First of all I use useEffect this way. It didn't update render
useEffect(() => {
callApi();
}, []);
After that I used useEffect this way. props.coordinates is a property that my code should update after it changes.
useEffect(() => {
callApi();
setLoader(false);
}, [props.coordinates]);
But my codes are in loops, and my API key was blocked.
Could you let me know what my mistake is ?
This my component:
import React, { useEffect, useState } from "react";
import axios from "axios";
import ForcastHour from "./ForcastHour";
import "./WeatherHourlyForcast.css";
const WeatherHourlyForcast = (props) => {
const [loader, setLoader] = useState(false);
const [hourlyForcastData, setHourlylyForcastData] = useState(null);
useEffect(() => {
callApi();
setLoader(false);
}, [props.coordinates]);
const showHourlyForcast = (response) => {
console.log("showHourlyForcast", response.data.hourly);
setHourlylyForcastData(response.data.hourly);
setLoader(true);
};
function callApi() {
let latitude = props.coordinates.lat;
let longitude = props.coordinates.lon;
const apiKey = "23422500afd990f6bd64b60f46cf509a";
let units = "metric";
let apiUrl = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}`;
axios.get(apiUrl).then(showHourlyForcast);
console.log("hourly", apiUrl);
}
if (loader) {
return (
<div className="row">
<div className="col-md-6">
<div className="row">
{hourlyForcastData.map(function (hourlyforcast, index) {
if (index < 4 && index > 0) {
return (
<div
className="col-4 box-weather my-auto text-center"
key={index}
>
<ForcastHour data={hourlyforcast} />
</div>
);
}
})}
</div>
</div>
<div className="col-md-6">
<div className="row">
{hourlyForcastData.map(function (hourlyforcast, index) {
if (index < 7 && index > 3) {
return (
<div
className="col-4 box-weather my-auto text-center"
key={index}
>
<ForcastHour data={hourlyforcast} />
</div>
);
}
})}
</div>
</div>
</div>
);
} else {
callApi();
return null;
}
};
export default WeatherHourlyForcast;
While adding dependencies array to the end of useEffect (or any other hook...), each render if the value is not equal to the prev one, the hook will run again.
Because props.coordinates is an object, and in JS objA != objA == true, even if the properties didn't change, React can't know that.
My suggestion is to use the values themselves (assuming they're strings either numbers and so on)
useEffect(() => {
(async () => {
await callApi();
setLoader(false);
})()
}, [props.coordinates.lat, props.coordinates.lon]);
Another thing that you might encounter is setLoader(false) will be called before callApi will be finished, therefore added async behaviour to the hook
You can write your component likes this and call the APIs when the component mount. The API calls happens when the lat, lon values are changed.
import React, { useEffect, useState } from "react";
import axios from "axios";
import ForcastHour from "./ForcastHour";
import "./WeatherHourlyForcast.css";
const WeatherHourlyForcast = (props) => {
const { coordinates : { lat, lon } } = props;
const [loader, setLoader] = useState(false);
const [hourlyForcastData, setHourlylyForcastData] = useState(null);
useEffect(() => {
callApi();
}, [lat, lon]); //It's call the API's when the lat, lon values are changed
const callApi = () => {
setLoader(true);
const latitude = lat;
const longitude = lon;
const apiKey = "23422500afd990f6bd64b60f46cf509a";
const units = "metric";
const apiUrl = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}`;
axios.get(apiUrl).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
console.log("showHourlyForcast", response.data.hourly);
setHourlylyForcastData(response.data.hourly);
setLoader(false);
});
};
if (loader) {
return (
<div>
<h1>Loading...</h1>
</div>
);
}
return (
<div className="row">
<div className="col-md-6">
<div className="row">
{hourlyForcastData.map(function (hourlyforcast, index) {
if (index < 4 && index > 0) {
return (
<div
className="col-4 box-weather my-auto text-center"
key={index}
>
<ForcastHour data={hourlyforcast} />
</div>
);
}
})}
</div>
</div>
<div className="col-md-6">
<div className="row">
{hourlyForcastData.map(function (hourlyforcast, index) {
if (index < 7 && index > 3) {
return (
<div
className="col-4 box-weather my-auto text-center"
key={index}
>
<ForcastHour data={hourlyforcast} />
</div>
);
}
})}
</div>
</div>
</div>
);
};
export default WeatherHourlyForcast;
I have dynamic routes based on search results. How do I go back and see my previously rendered search results & search term in input field versus and empty Search page?
I've started looking into useHistory/useLocation hooks, but I'm lost.
1. Search page
export default function Search() {
const [searchValue, setSearchValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [noResults, setNoResults] = useState(false);
const [data, setData] = useState([]);
const fetchData = async () => {
const res = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key={API_KEY}&query=${searchValue}`
);
const data = await res.json();
const results = data.results;
if (results.length === 0) setNoResults(true);
setData(results);
setIsLoading(false);
};
function handleSubmit(e) {
e.preventDefault();
setIsLoading(true);
fetchData();
// setSearchValue("");
}
return (
<div className="wrapper">
<form className="form" onSubmit={handleSubmit}>
<input
placeholder="Search by title, character, or genre"
className="input"
value={searchValue}
onChange={(e) => {
setSearchValue(e.target.value);
}}
/>
</form>
<div className="page">
<h1 className="pageTitle">Explore</h1>
{isLoading ? (
<h1>Loading...</h1>
) : (
<div className="results">
{!noResults ? (
data.map((movie) => (
<Result
poster_path={movie.poster_path}
alt={movie.title}
key={movie.id}
id={movie.id}
title={movie.title}
overview={movie.overview}
release_date={movie.release_date}
genre_ids={movie.genre_ids}
/>
))
) : (
<div>
<h1 className="noResults">
No results found for <em>"{searchValue}"</em>
</h1>
<h1>Please try again.</h1>
</div>
)}
</div>
)}
</div>
</div>
);
}
2. Renders Result components
export default function Result(props) {
const { poster_path: poster, alt, id } = props;
return (
<div className="result">
<Link
to={{
pathname: `/results/${id}`,
state: { ...props },
}}
>
<img
src={
poster
? `https://image.tmdb.org/t/p/original/${poster}`
: "https://www.genius100visions.com/wp-content/uploads/2017/09/placeholder-vertical.jpg"
}
alt={alt}
/>
</Link>
</div>
);
}
3. Clicking a result brings you to a dynamic page for that result.
export default function ResultPage(props) {
const [genreNames, setGenreNames] = useState([]);
const {
poster_path: poster,
overview,
title,
alt,
release_date,
genre_ids: genres,
} = props.location.state;
const date = release_date.substr(0, release_date.indexOf("-"));
useEffect(() => {
const fetchGenres = async () => {
const res = await fetch(
"https://api.themoviedb.org/3/genre/movie/list?api_key={API_KEY}"
);
const data = await res.json();
const apiGenres = data.genres;
const filtered = [];
apiGenres.map((res) => {
if (genres.includes(res.id)) {
filtered.push(res.name);
}
return filtered;
});
setGenreNames(filtered);
};
fetchGenres();
}, [genres]);
return (
<div className="resultPage">
<img
className="posterBackground"
src={
poster
? `https://image.tmdb.org/t/p/original/${poster}`
: "https://www.genius100visions.com/wp-content/uploads/2017/09/placeholder-vertical.jpg"
}
alt={alt}
/>
<div className="resultBackground">
<div className="resultInfo">
<h1> {title} </h1>
</div>
</div>
</div>
);
}
4. How do I go back and see my last search results?
I'm not sure how to implement useHistory/useLocation with dynamic routes. The stuff I find online mentions building a button to click and go to last page, but I don't have a button that has to be clicked. What is someone just swipes back on their trackpad?
One way you could do this would be to persist the local component state to localStorage upon updates, and when the component mounts read out from localStorage to populate/repopulate state.
Use an useEffect hook to persist the data and searchValue to localStorage, when either updates.
useEffect(() => {
localStorage.setItem('searchValue', JSON.stringify(searchValue));
}, [searchValue]);
useEffect(() => {
localStorage.setItem('searchData', JSON.stringify(data));
}, [data]);
Use an initializer function to initialize state when mounting.
const initializeSearchValue = () => {
return JSON.parse(localStorage.getItem('searchValue')) ?? '';
};
const initializeSearchData = () => {
return JSON.parse(localStorage.getItem('searchData')) ?? [];
};
const [searchValue, setSearchValue] = useState(initializeSearchValue());
const [data, setData] = useState(initializeSearchData());
I've created a static React website and hosted it on Github pages.
As you can see, whenever you click on a film or a TV series, a modal will appear with the film/tv poster(loaded from the OMDb API) and some metadata. The problem is that the content loads too slowly. It takes a second(sometimes more) before before the content appears.
I get that I can't expect it to load that much faster, but I would like to not show the modal at all before everything looks nice(i.e is perfectly loaded). Perhaps by having a "Loading.." appear while we wait. It doesn't have to be anything fancy, as it's only gonna be on the screen for 1-2 seconds at most.
Do you have any advice for a React beginner?
Relevant code:
function ImdbInfo(props) {
const [data, setData] = useState({ imdbData: [] });
useEffect(() => {
const imdbId = getImdbId(props.url);
const fetchData = async () => {
const result = await axios(
`https://www.omdbapi.com/?i=${imdbId}&apiKey=${apiKey}`,
);
setData(result.data);
};
fetchData();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="modal-content">
<div className="metadata" onClick={props.handleClose}>
<h1 className="modal-header">{data.Title}</h1>
<img className="modal-poster" src={data.Poster} alt="poster" />
<p className="modal-info">{getDirectorOrWriter(data)}</p>
<p className="modal-info">IMDb Rating: {getImdbScore(data.Ratings)}</p>
</div>
{createImdbLink(props.url)}
</div>
);
}
And:
const MediaModal = ({ handleClose, show, data }) => {
const showHideClassName = show ? 'modal display-block' : 'modal display-none';
const imdbData = show ? <ImdbInfo url={data.imdbLink} handleClose={handleClose} /> : <div />;
return (
<div className={showHideClassName} onClick={handleClose}>
<section className='modal-main'>
{imdbData}
</section>
</div>
);
};
export default MediaModal;
Set loading to true before making the api call and
set loading false after api returns success/failure.
Check updated code below
function ImdbInfo(props) {
const [data, setData] = useState({ imdbData: [] });
const [loading, setLoading] = useState(false);
useEffect(() => {
const imdbId = getImdbId(props.url);
const fetchData = async () => {
setLoading(true);
try {
const result = await axios(
`https://www.omdbapi.com/?i=${imdbId}&apiKey=${apiKey}`
);
setData(result.data);
setLoading(false);
} catch (err) {
setLoading(false);
}
};
fetchData();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div>
{loading ? (
<div>Loading...</div>
) : (
<div className="modal-content">
<div className="metadata" onClick={props.handleClose}>
<h1 className="modal-header">{data.Title}</h1>
<img className="modal-poster" src={data.Poster} alt="poster" />
<p className="modal-info">{getDirectorOrWriter(data)}</p>
<p className="modal-info">
IMDb Rating: {getImdbScore(data.Ratings)}
</p>
</div>
{createImdbLink(props.url)}
</div>
)}
</div>
);
}
If I understand your code correctly, you create a MediaModal component, that has a child component ImdbInfo, where you fetch some data. I guess this MediaModal has a parent component where you toggle and use your modal: you didn't provide the code, but let's call it MainComponent
Instead of fetching data inside ImdbInfo you could fetch them in MainComponent and pass the result as props:
Inside MainComponent:
// Onclick to toggle modal:
// - fetch data just like you did in ImdbInfo
// - then set show=true
// Then use your modal
<MediaModal handleClose={plop} show={plip} infos={fetchedData} url={imdbLink} />
MediaModal:
const MediaModal = ({ handleClose, show, infos, url}) => {
const showHideClassName = show ? 'modal display-block' : 'modal display-none';
const imdbData = show ? <ImdbInfo infos={infos} url={url} handleClose={handleClose} /> : <div />;
return (
<div className={showHideClassName} onClick={handleClose}>
<section className='modal-main'>
{imdbData}
</section>
</div>
);
};
export default MediaModal;
ImdbInfo:
function ImdbInfo({infos, handleClose}) {
return (
<div className="modal-content">
<div className="metadata" onClick={handleClose}>
<h1 className="modal-header">{infos.Title}</h1>
<img className="modal-poster" src={infos.Poster} alt="poster" />
<p className="modal-info">{getDirectorOrWriter(infos)}</p>
<p className="modal-info">IMDb Rating: {getImdbScore(infos.Ratings)}</p>
</div>
{createImdbLink(url)}
</div>
);
}
I have a problem when using the information of a specific item from a JSON of an API.
When wanting to place the fields that I need, it throws the error:
TypeError: Cannot read property 'item' of undefined -.
This happenes when I want to put the field inside
return (
*value*
).
but if I put it out of the return and show it in console it works.
import React, { useState, useEffect } from 'react';
function Platillos_Detalle({ match }) {
useEffect(() => {
fetchItem();
}, []);
const [items, setItem] = useState({
item: {}
});
const fetchItem = async () => {
const fetchItem = await fetch(
`https://fortnite-api.theapinetwork.com/item/get?id=${match.params.id
}`
);
const items = await fetchItem.json();
setItem(items);
console.log(items.data.item.description)
}
return (
<div className="container">
<div>{items.data.item.description}</div>
</div>
)
}
export default Platillos_Detalle;
Result of console.log (items.data.item.description)
I'd also like to mention that this same code is used to do something similar but with several items and it works there fine.
update 1:
Focus on the "React Hooks useEffect" warning.
As I use a parameter sent from another page (The id of the specific item) I had to define it in: []of useEffect. This solved the warning and now if it received the data from the JSON.
Other problems were because of the JSON structure that looked like this:
JSON example
data: {
itemID: "iditem",
item: {
name: "nombre",
imagen: {
inforamtion:"url"
}
}
}
so inside useState add the properties I needed (If I did not do this it would mark the error of
"undefined item") and with that solve the error
function Platillos_Detalle({ match }) {
useEffect(() => {
fetchItem();
}, [
match.params.id
]);
const fetchItem = async () => {
const fetchItem = await fetch(
`https://fortnite-api.theapinetwork.com/item/get?id=${match.params.id
}`
);
const item = await fetchItem.json();
setItem(item.data)
};
const [item, setItem] = useState({
item: {
images: {
}
}
});
return (
<div className="center-block">
<br />
<div className="col-md-4 offset-md-4" >
<div className="card">
<img src={item.item.images.information} className="card-img-top" alt="..." />
<div className="card-body">
<h5 className="card-title">{item.item.name} </h5>
<p className="card-text">{item.item.description}</p>
</div>
<div className="card-footer ">
<button className="btn btn-primary btn-lg btn-block">Pedir :v</button>
</div>
</div>
<br></br>
</div>
</div>
)
}
export default Platillos_Detalle;
It’s probably not yet ready since its the result of the async code. Maybe try adding a condition checking if it exists.
Items.data ? Items .data.anything : []