I am working on a react app that shortens a URL after a button is clicked. I am having a hard time figuring out the best logic to do so. The loading message does not show then when I click the button. The loading button appears and then disappears when the message loads.
The current behavior with the code below is the following.
When it renders "Loading..." does not show.
I click my "Shorten it" button and "Loading..." shows.
The shortened url appears below the "Loading..." button.
Shorten.js
import { useEffect, useState } from "react";
const Shorten = () => {
const [shortLink, setShortLink] = useState(null);
const [isPending, setIsPending] = useState(false);
const [input, setInput] = useState('example.org/very/long/link.html');
const url = "https://api.shrtco.de/v2/shorten?url=";
const fullUrl = (url.concat(input));
console.log(fullUrl);
useEffect(() => {
fetch(fullUrl)
.then(res => {
return res.json();
})
.then(data => {
setShortLink(data.result.short_link);
// setIsPending(false);
})
}, [fullUrl ])
// input
// value={input}
const loadMsg = () =>{
setIsPending(true);
}
return (
<main>
<section className="purple-card">
<input onInput={e => setInput(e.target.value)} type="text" placeholder="Shorten a link here..." className="shorten-input"/>
<button className="shorten-it" onClick={() => loadMsg()}>Shorten It!</button>
</section>
<section className="white-card">
{isPending && <div className="loading-text">Loading...</div>}
{shortLink && <div className="shorten-text">{shortLink}</div>}
<hr></hr>
<button className="shorten-it" >Copy</button>
</section>
</main>
);
}
export default Shorten;
There seem to be a few issues here. The reason the loading message and the shortened URL display at the same time is that their render conditions are not mutually exclusive. Fixing that is as simple as not showing the shortened URL while the component is loading.
Also something that can cause issues is that the "Shorten It!" button does not control actually performing the shortening action, it just sets the loading (pending) state to true. The shortening action runs whenever the input's value changes. Basically the loading state and the shortening action are fairly independent of each other.
To fix this you should only run the shortening action when the user clicks the button and at that point set the isPending state to true at the same time (then set back to false when done).
Instead of useEffect, you can just use a function that gets called when the button is clicked.
For example:
import { useState, useCallback } from "react";
const Shorten = () => {
const [shortLink, setShortLink] = useState(null);
const [isPending, setIsPending] = useState(false);
const [input, setInput] = useState('example.org/very/long/link.html');
const shortenUrl = useCallback(() => {
setIsPending(true);
const baseUrl = "https://api.shrtco.de/v2/shorten?url=";
const fullUrl = baseUrl + input;
fetch(fullUrl)
.then(res => res.json())
.then(data => {
setShortLink(data.result.short_link)
})
.finally(() => setIsPending(false));
}, [input]);
return (
<main>
<section className="purple-card">
<input onInput={e => setInput(e.target.value)} type="text" placeholder="Shorten a link here..." className="shorten-input"/>
<button className="shorten-it" onClick={shortenUrl}>Shorten It!</button>
</section>
<section className="white-card">
{isPending && <div className="loading-text">Loading...</div>}
{!isPending && shortLink && <div className="shorten-text">{shortLink}</div>}
<hr></hr>
<button className="shorten-it" >Copy</button>
</section>
</main>
);
}
export default Shorten;
Alternatively, to make the loading/result more clearing mutually exclusive, you can define a single variable with a value of either the loading message, the result URL, or nothing. For example:
const result = useMemo(() => {
if (isPending) {
return <div className="loading-text">Loading...</div>;
}
if (shortLink) {
return <div className="shorten-text">{shortLink}</div>;
}
return null;
}, [isPending, shortLink]);
Then render like so:
<section className="white-card">
{result}
</section>
Related
Using React & Axios. My post request response is an array of arrays, the second element of nested array is string I am trying to load onto a div through using map. Error is 'undefined is not iterable' I am trying to use useState to use the array outside of post request. The entire section opens with useState via a button and by default is closed/not loaded. There is also a user input which the post request uses to get it data, all of that works fine. I am just unable to map the string from the array into a div. I tried to use window.var to access it but this was unsuccessful as well. Appreciate any help!
import './Turnersdiscoverybody.css'
import axios from 'axios'
import { useState, useEffect } from 'react';
import React, { Component } from 'react'
import Turnershomenav from "../../../components/homepage/homepage-components/Turnershomenav.js";
import orangegtr1 from './turnersdiscovery-images/orangegtr-1.jpg'
import searchicon from './turnersdiscovery-images/searchicon.png'
export default function Turnersdiscoverybody() {
const [showSearchForm, setShowSearchForm] = useState('noForm')
const [input, setInput] = useState['']
//functions for opening and closing search feature
const handleClick = () => {
setShowSearchForm('showForm')
}
const handleClickBack = () => {
setShowSearchForm('noForm')
}
//axios post request starts
//function that handles searching the documents with the user input, using axios
const handleSearch = (e) => {
let userQuery = document.getElementById('userInput').value
e.preventDefault()
axios.post(`http://localhost:8081/getDocumentdata/${userQuery}`)
.then(res => {
setInput(res.data)
console.log(res.data)
})
}
//axios post request ends
return (
<div>
<div className="turnersdiscoverynav">
<Turnershomenav />
</div>
<div className='backgroundimg-container'>
<img src={orangegtr1} alt="background-img" className='turnersdiscovery-backgroundimg'></img>
</div>
{showSearchForm === 'showForm' &&
<>
<img className="img-btn-search" alt="search icon" src={searchicon} onClick={handleClickBack}></img>
<div className='form-search-container'>
<div className='form-search-container-top'>
<input
id="userInput"
required
type="text"
placeholder='enter your query'
></input>
<button onClick={handleSearch}>hello click me for stuff</button>
</div>
<div className='form-search-container-bottom'>
<div className='form-search-container-bottom-content'>
{input.map((data) => (
<div>{data[1]}</div>
)
)}
</div>
</div>
</div>
</>
}
{showSearchForm === "noForm" && <img className="img-btn-search" alt="search icon" src={searchicon} onClick={handleClick}></img>}
</div>
)
}
You have some errors in your code:
//1 => const [input, setInput] = useState([])
//2 => check if inputs is not empty then map!
Now your code must be like this:
export default function Turnersdiscoverybody() {
const [showSearchForm, setShowSearchForm] = useState('noForm')
const [input, setInput] = useState([]);
const [empty, setEmpty] = useState(false);
//functions for opening and closing search feature
const handleClick = () => {
setShowSearchForm('showForm')
}
const handleClickBack = () => {
setShowSearchForm('noForm')
}
//axios post request starts
//function that handles searching the documents with the user input, using axios
const handleSearch = (e) => {
let userQuery = document.getElementById('userInput').value
e.preventDefault()
axios.post(`http://localhost:8081/getDocumentdata/${userQuery}`)
.then(res => {
if(res && res.length > 0){
setInput(res.data)
setEmpty(false)
}else{
setEmpty(true)
}
console.log(res.data)
})
}
return (
<div>
<div className="turnersdiscoverynav">
<Turnershomenav />
</div>
<div className='backgroundimg-container'>
<img src={orangegtr1} alt="background-img"
className='turnersdiscovery-backgroundimg'></img>
</div>
{showSearchForm === 'showForm' &&
<>
<img className="img-btn-search" alt="search icon" src={searchicon} onClick={handleClickBack}></img>
<div className='form-search-container'>
<div className='form-search-container-top'>
<input
id="userInput"
required
type="text"
placeholder='enter your query'
></input>
<button onClick={handleSearch}>hello click me for stuff</button>
</div>
<div className='form-search-container-bottom'>
<div className='form-search-container-bottom-content'>
{empty ? <p>Nothing Found </p> :
<>
{input.length > 0 && input.map((data, index) => (
<div>{data[1]}</div>
)
)}
</>
}
</div>
</div>
</div>
</>
}
{showSearchForm === "noForm" && <img className="img-btn-search" alt="search icon" src={searchicon} onClick={handleClick}></img>}
</div>
)
}
const [input, setInput] = useState['']
should be
const [input, setInput] = useState('')
P.s. if you run into more issues, a minimal code reproduction would be good, somewhere like codepen or codesandbox.
const [input, setInput] = useState['']
should have been
const [input, setInput] = useState([])
So I'm building my portfolio in next.js, and in order to avoid showing partly loaded gallery images, I wanted to use a 'loading' state, which will turn off (become 0) once all the photos are loaded perfectly. It works just fine unless I refresh the page. The loading doesn't become 0 (false) when I refresh the page. When I change the category and come back, it again starts working fine. Only stops on the page whenever I refresh the page. What can be the problem?
P.S. When 'loading' is 1 (true) the opacity of the grid is 0.
Thanks a lot in advance!
import { GalleryLayoutStyle } from '../../style/componentStyles/GalleryLayoutStyle'
import { useContext, useEffect, useState } from 'react'
import Masonry from 'react-masonry-css'
const Gallery = ({ data }) => {
const [loading, setLoading] = useState(1)
const [count, setCount] = useState(0)
const allImagesCount = data.length
const increaseCount = () => {
setCount((prev) => prev + 1)
}
useEffect(() => {
setLoading(1)
}, [])
useEffect(() => {
if (count == allImagesCount) {
setLoading(0)
}
}, [count])
return (
<GalleryLayoutStyle loading={loading}>
{loading == 1 ? <h1>Loading</h1> : null}
<div className="grid">
<Masonry className="my-masonry-grid" columnClassName="my-masonry-grid_column">
{data.map((item) => (
<div className="gallery-box" key={item.id}>
<img src={item.url} onLoad={increaseCount} />
<div className="overlay">
<div className="title">
<h4>{item.title}</h4>
</div>
<div className="zoom">
<img src="/icons/loupe.svg" alt="" />
</div>
</div>
</div>
))}
</Masonry>
</div>
</GalleryLayoutStyle>
)
}
export default Gallery
It looks (nearly) all fine to me.
I would change only one little thing. Don't set loading initially to 1, but 0. Make it depending only on count.
Maybe give it a try with this:
const Gallery = ({ data }) => {
const [loading, setLoading] = useState(0)
const [count, setCount] = useState(0)
const allImagesCount = data.length
const increaseCount = () => {
setCount((prev) => prev + 1)
}
useEffect(() => {
if (count === allImagesCount) {
setLoading(0)
}else{
setLoading(1)
}
}, [count])
....
I am working on one React infinite scroll component and its working fine and perfectly, only one small issue is that for page number 1, it makes twice the API request, which I do not want and for other pages ex 2,3,4 it makes request only once.
I tried everything but i am unable to modify the code so that for page number 1,it makes only once the request.
How can i make only once the request for page number 1 also ?
here is the working code.
import React, { useState, useEffect } from "react";
import axios from "axios";
function Lists() {
const [posts, setPosts] = useState([]);
const [newposts, setNewposts] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [page, setPage] = useState(1);
const LIMIT = 7;
const getPosts = () => {
axios.get(`https://jsonplaceholder.typicode.com/posts?_limit=${LIMIT}&_page=${page}`)
.then(res => {
setNewposts(res.data);
setPosts([...posts, ...res.data]);
setIsFetching(false);
})
};
const getMorePosts= () => {
setPage(page + 1);
getPosts();
}
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop !==
document.documentElement.offsetHeight
) return;
setIsFetching(true);
}
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
getPosts();
},[]);
useEffect(() => {
if (!isFetching){
return;
}
if( newposts.length > 0 ){
getMorePosts();
}
}, [isFetching]);
return (
<div className="App">
{posts.map((post, index) => (
<div key={index} className="post">
<div className="number">{post.id}</div>
<div className="post-info">
<h2 className="post-title">{post.title}</h2>
<p className="post-body">{post.body}</p>
</div>
</div>
))}
{isFetching && newposts.length > 0 && (
<div style = {{display: "flex", justifyContent:"center"}}>
<div className="spinner-border" role="status">
<span className="sr-only">Loading...</span>
</div>
</div>
)}
</div>
);
}
export default Lists;
The issue is that setPage, like any state setter, works asynchronously - it doesn't update the value of page immediately like it appears you might be expecting it to. The value of page will only be updated in a subsequent render which is triggered by the state change.
That means that in getMorePosts() you're not really getting page + 1, you're just getting page.
That means on first render you're calling getPosts() in that other useEffect, with page set to 1, then calling it again when you scroll and call getPosts() inside the getMorePosts() call.
Subsequent calls only happen once because getMorePosts() increments page in every subsequent render.
As for a fix, a quick one might be to just take page as an arg to getPosts, then you can statically call getPosts(1) on first render and keep the rest of the logic the same but init the page state to 2 and change the call to getPage(page) inside getMorePosts().
I am trying to implement load more button for my small project GiF generator. First I thought of appending next set of 20 response at the bottom, but failed to do.
Next, I thought of implementing loading the next set of 20 results by simply removing the current one. I tried to trigger a method on click of button, but I failed to do so. Its updating the state on second click of load more and then never updating it again.
Please help me find what I am missing, I have started learning React yesterday itself.
import React, { useEffect, useState } from 'react';
import './App.css';
import Gif from './Gif/Gif';
const App = () => {
const API_KEY = 'LIVDSRZULELA';
const [gifs, setGif] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('random');
const [limit, setLimit] = useState(20);
const [pos, setPos] = useState(1);
useEffect(() => {
getGif();
}, [query])
const getGif = async () => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${pos}`);
const data = await response.json();
setGif(data.results);
console.log(data.results)
}
const updateSearch = e => {
setSearch(e.target.value);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const reload = () => {
setQuery('random')
}
const loadMore = () => { // this is where I want my Pos to update with 21 on first click 41 on second and so on
let temp = limit + 1 + pos;
setPos(temp);
setQuery(query);
}
return (
<div className="App">
<header className="header">
<h1 className="title" onClick={reload}>React GiF Finder</h1>
<form onSubmit={getSearch} className="search-from">
<input className="search-bar" type="text" value={search}
onChange={updateSearch} placeholder="type here..." />
<button className="search-button" type="submit">Search</button>
</form>
<p>showing results for <span>{query}</span></p>
</header>
<div className="gif">
{gifs.map(gif => (
<Gif
img={gif.media[0].tinygif.url}
key={gif.id}
/>
))}
</div>
<button className="load-button" onClick={loadMore}>Load more</button>
</div>
);
}
export default App;
Please, help me find, what I am doing wrong, As I know the moment I will update setQuery useEffect should be called with new input but its not happening.
Maybe try something like this:
// Fetch gifs initially and then any time
// the search changes.
useEffect(() => {
getGif().then(all => setGifs(all);
}, [query])
// If called without a position index, always load the
// initial list of items.
const getGif = async (position = 1) => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${position}`);
const data = await response.json();
return data.results;
}
// Append new gifs to existing list
const loadMore = () => {
let position = limit + 1 + pos;
setPos(position);
getGif(position).then(more => setGifs([...gifs, ...more]);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const updateSearch = e => setSearch(e.target.value);
const reload = () => setQuery('random');
Basically, have the getGifs method be a bit more generic and then if loadMore is called, get the next list of gifs from getGift and append to existing list of gifs.
I have an application that adds GitHub users to a list. When I put input in the form, a user is returned and added to the list. I want the user to be added to the list only if I click on the user when it shows up after the resource request. Specifically, what I want is to have a click event in the child component trigger the root component’s triggering of the hook, to add the new element to the list.
Root component,
const App = () => {
const [cards, setCards] = useState([])
const addNewCard = cardInfo => {
console.log("addNewCard called ...")
setCards([cardInfo, ...cards])
}
return (
<div className="App">
<Form onSubmit={addNewCard}/>
<CardsList cards={cards} />
</div>
)
}
export default App;
Form component,
const Form = props => {
const [username, setUsername] = useState('');
const chooseUser = (event) => {
setUsername(event.target.value)
}
const handleSubmit = event => {
event.persist();
console.log("FETCHING ...")
fetch(`http://localhost:3666/api/users/${username}`, {
})
.then(checkStatus)
.then(data => data.json())
.then(resp => {
console.log("RESULT: ", resp)
props.onSubmit(resp)
setUsername('')
})
.catch(err => console.log(err))
}
const checkStatus = response => {
console.log(response.status)
const status = response.status
if (status >= 200 && status <= 399) return response
else console.log("No results ...")
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Gitbub username"
value={username}
required
onChange={chooseUser}
onKeyUp={debounce(handleSubmit, 1000)}
/>
<button type="submit">Add card</button>
</form>
)
}
export default Form;
List component,
const CardsList = props => {
return (
<div>
{props.cards.map(card => (
<Card key={card.html_url} {... card}
/>
))}
</div>
)
}
export default CardsList
and the Card Component,
const Card = props => {
const [selected, selectCard] = useState(false)
return (
<div style={{margin: '1em'}}>
<img alt="avatar" src={props.avatar_url} style={{width: '70px'}} />
<div>
<div style={{fontWeight: 'bold'}}><a href={props.html_url}>{props.name}</a></div>
<div>{props.blog}</div>
</div>
</div>
)
}
export default Card
Right now, my Form component has all the control. How can I give control over the addNewCard method in App to the Card child component?
Thanks a million in advance.
One solution might be to create a removeCard method in App which is fired if the click event you want controlling addNewCard doesn't happen.
// App.js
...
const removeCard = username => {
console.log("Tried to remove card ....", username)
setCards([...cards.filter(card => card.name != username)])
}
Then you pass both removeCard and addNewCard to CardList.
// App.js
...
<CardsList remove={removeCard} cards={cards} add={addNewCard}/>
Go ahead and pass those methods on to Card in CardsList. You will also want some prop on card assigned to a boolean, like, "selected".
// CardsList.js
return (
<div>
{props.cards.map(card => (
<Card key={card.html_url} {... card}
remove={handleClick}
add={props.add}
selected={false}
/>
))}
</div>
Set up your hook and click event in the child Card component,
// Card.js
...
const [selected, selectCard] = useState(false)
...
and configure your events to trigger the hook and use the state.
// Card.js
...
return (
<div style={{margin: '1em', opacity: selected ? '1' : '0.5'}}
onMouseLeave={() => selected ? null : props.remove(props.name)}
onClick={() => selectCard(true)}
>
...
This doesn't really shift control of addNewCard from Form to Card, but it ultimately forces the UI to follow the state of the Card component.