React: don't show modal until all content has been properly loaded - reactjs

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>
);
}

Related

What's the best practise for showing up a modal after fetch data

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>

How can I default category through api

I have written a project which receives data through an api. Clicking on each button displays corresponding news. For example, when you press the sports button, sports news comes. However, I want the All category to be active when the page is first opened. In other words, those news should have arrived without pressing the all button. How can I do this?
Not - The function inside useffect returns every time it renders and it doesn't work for me. For example, when you refresh the page while reading sports news, all news comes
import React, { useEffect, useState } from "react";
import SpinnerLoad from './components/SpinnerLoad'
import NewsItem from "./components/NewsItem";
import Category from "./components/data/Category"
const App = () => {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false)
const fetchValue = (category) => {
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
setLoading(false);
};
const CategoryButton = ({ category }) => (
<button onClick={() => fetchValue(category)} style={{ textTransform: 'capitalize' }}>{category}</button>
);
useEffect(() => {
fetchValue('all')
}, [])
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value, index) => {
return <CategoryButton category={value} key={index} />;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad/>
:
state.map((data,index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
key={data.id}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default App;
import React from 'react'
import clock from "../components/assets/img/Clock.svg"
import user from "../components/assets/img/User.svg"
const NewsItem = (props) => {
const {imageUrl, title, content, date, author} = props
return (
<div class="col-lg-4 col-md-6 col-12 p-2">
<div className="newsItem">
<img src={imageUrl} alt=''/>
<div className="itemBody">
<p className='title'>{title}</p>
<div className="line"></div>
<p className='content'>{content}</p>
<div className="itemfooter">
<h6><img src={clock} alt='clock'/>{date}</h6>
<h6><img src={user} alt='user'/>{author}</h6>
</div>
</div>
</div>
</div>
)
}
export default NewsItem
In react, if you refresh the app , the state values will reinitialise.
From your question , it seems like you want to store the category value and even after refresh , you want to persist the category value..
For that you can store category value in local or sessionStorage..
const fetchValue = (category) => {
localStorage.setItem("category", category);
// your code
}
// in useEffect , you can check for the category value in the local Storage
useEffect(() => {
// check value in localStorage, if does not exist use "all" as default value
let categoryValue = localStorage.getItem("category") || "all" ;
fetchValue(categoryValue)
},[]);

how to show a new todo-item without refreshing the page?

I tried a lots of things , and this problem does not seem to go away , can someone help me with this ??
this is my app component :
function App() {
const [todo, setTodo] = useState([]);
async function getTodo() {
try {
const todo = await axios.get("http://localhost:5000/api/todos");
// console.log(todo.data)
setTodo(todo.data);
} catch (error) {
console.log("something is wrong");
}
}
useEffect(() => {
// Update the document title using the browser API
getTodo();
}, []);
return (
<div className="App">
<h1>My Todo List</h1>
<h2>My Todo List</h2>
<Task Todor={todo} />
<Write />
</div>
);
}
export default App;
and this is my todos component :
function Todos({ Todor }) {
return (
<div className="Todos">
{Todor.map(T => <Todo post={T} />)}
</div>
);
}
export default Todos;
and this is my todo component :
function Todo({ post }) {
return (
<div className="Todo">
<h2>{post.title}</h2>
</div>
);
}
export default Todo ;
and this my add component :
export default function Write() {
const [inputText, setInputText] = useState({
title: ""
});
function handleChange(e) {
setInputText({
...inputText,
[e.target.name]: e.target.value,
});
}
const [status, setStatus] = useState(false);
async function addItem(e) {
e.preventDefault();
const res = await axios.post("http://localhost:5000/api/todos", inputText);
setInputText(inputText)
console.log("response:", res)
setStatus(true);
setInputText("");
}
return (
<div className="container">
<div className="form">
<input onChange={handleChange} type="text" name="title" />
<button onClick={addItem}>
<span>Add</span>
</button>
</div>
</div>
);
}
the new items dont show until I refresh the page , how to do that without refreshing ?
because obviously that defeats the purpose of React !!
useEffect(() => {
// Update the document title using the browser API
getTodo();
}, []);
The code inside useEffect with empty dependencies array [] only runs on the first render, to run it on every render you should remove the empty array dependencies.
useEffect(() => {
// Update the document title using the browser API
getTodo();
});
Note: It is not a best practice because your component will invoke getTodo() every time rerendered. In your case, you can use a state variable to control where to re-run the getTodo funtion e.g:
const [isAddedSuccess, setIsAddedSuccess] = useState(false)
Everytime you add new item successfully, just setIsAddedSuccess(true) and your useEffect should look like below:
useEffect(() => {
// Update the document title using the browser API
if (isAddedSuccess) getTodo();
}, [isAddedSuccess]);

React Router: Navigate back to Search results

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());

How can I test this in Jest?

I have the following code:
const HeaderMenu = ({ location }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const dispatch = useDispatch();
const handleLogout = doLogout(dispatch);
const handleLogoutOrder = async ({ stage }) => {
setIsModalOpen(true);
await dispatch(
fetchRejectionReasons({
siteId: 'USDJD',
serviceId: 'CAR',
stage: stage || EXIT_REASONS.EXIT
})
);
};
return(
<>
<Popup
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
logout
/>
<div className={styles.container}>
<div>
<Link data-testid="link" to="/">
<img src={logoUrl} />
</Link>
</div>
<div
role="button"
tabIndex="0"
onClick={
ORDER_ROUTES.includes(location.pathname)
? handleLogoutOrder
: handleLogout
}
data-testid="headermenu-logout-btn"
>
Logout
</div>
</div>
</>
);
};
)
...
I don't have any idea how to test handleLogoutOrder function in Jest using react testing library.. if anyone with experience on this can help me, will be greatly appreciated.
I've tested the Popup render.. I need to test setIsModalOpen(true) and the dispatch fetch function.

Resources