React - I can't render the correct amount of items in from the map function - javascript

Image of the unwanted behaviourI'm using DaisyUi / Tailwindcss to render a Carousel and it's child item. The item are images that are fetched from a firebase storage.
The issue is when I try to map the image urls, which is a state of an array of strings, I only get 1 image instead of all the items.
There's 2 items in my storage and it's complete when I log it's content / arrays' length but the state(imageUrls) is not filled and it only shows 1 item.
Ask for any clarification.
Here is my code for my carousel component.
const MyCarousel = () => {
const [imageUrls, setImageUrls] = useState<string[]>([]);
const storage = useStorage();
const getImages = async () => {
const storageRef = ref(storage, 'Tech/');
const files = await listAll(storageRef);
const imageAppender: string[] = Array(files.items.length);
console.log('img', imageAppender.length);
files.items.forEach(async (item, key) => {
const imageRef = ref(storage, item.fullPath);
getDownloadURL(imageRef).then((url) => {
imageAppender[key] = url;
setImageUrls(imageAppender);
});
});
};
useEffect(() => {
getImages();
}, []);
return (
<>
<div className="carousel w-full">
{imageUrls.map((image, key) => {
return (
<div key={key} id={'item' + key} className="carousel-item w-full">
<img src={image} className="w-full" />
</div>
);
})}
</div>
<div className="flex justify-center w-full py-2 gap-2">
{imageUrls.map((_, key) => {
return (
<a key={key} href={'#item' + key} className="bth btn-ghost btn-circle text-center">
{key}
</a>
);
})}
</div>
</>
);
};
export default MyCarousel;

Problem is with the way you update your imageUrls state. You always pass same imageAppender in your setImageUrls, so in that case DOM is re-rendered only after first setImageUlrs state update.
files.items.forEach(async (item, key) => {
const imageRef = ref(storage, item.fullPath);
getDownloadURL(imageRef).then((url) => {
setImageUrls(currentUlrs => {
const updatedUrls = [...currentUlrs];
updatedAppender[key] = url;
return updatedAppender;
});
});
})
Please read useState functional updates - to get latest state on update.
So basically you need to pass new reference to new array (create updatedAppender) to trigger re-render.
In addition
It is better to not use forEach with async tasks. Please read this thread for alternatives What is the difference between async/await forEach and Promise.all + map

Related

How to get the first elements of an array and then get the next few elements?

I'm building my blog page for my website and I have a posts folder with markdown files of my blogs. I'm just figuring out a way to display all the blogs on a page, but I want to optimize it a bit so it doesn't try to load all blog posts at once but only the first 6 for example. And then when you click on a Load More button the next 6 get loaded and displayed.
This is the code I'm using to get the data from my blog posts:
async function getBlogPosts(n: number) {
const files = fs.readdirSync('posts');
const posts = files.slice(0, n).map((fileName) => {
const slug = fileName.replace('.md', '');
const readFile = fs.readFileSync(`posts/${fileName}`, 'utf-8');
const { data: frontmatter } = matter(readFile);
return {
slug,
frontmatter,
};
});
return posts;
}
And then display the title of the posts:
export default async function Blogs() {
const posts = await getBlogPosts(6);
return (
<div className="mx-auto flex">
{posts.map(({ slug, frontmatter }) => (
<div
key={slug}
className="m-2 flex flex-col overflow-hidden rounded-xl border border-gray-200 shadow-lg"
>
<Link href={`/blog/${slug}`}>
<h3 className="p-4">{frontmatter.title}</h3>
</Link>
</div>
))}
</div>
);
}
How would one go about implementing this?
Because I think if I were to call GetBlogPosts(12) it would load 12 posts but also the first 6 which have already been loaded.
You code is perfect.
Just implement pagination during slice method, increase page number as user clicks show more. and slice REQUIRED FILES data only.
Your code would look like this:
async function getBlogPosts(blogsPerPage:number, pageNumber:number) {
const files = fs.readdirSync('posts');
!IMPORTANT. sort files array here before slicing, so we wont get repeated posts...
const posts = files.slice((pageNumber-1)* blogsPerPage,pageNumber*blogsPerPage).map((fileName) => {
const slug = fileName.replace('.md', '');
const readFile = fs.readFileSync(`posts/${fileName}`, 'utf-8');
const { data: frontmatter } = matter(readFile);
return {
slug,
frontmatter,
};
});
return posts;
}
Now you can implement a state in your functional component and call this function somewhat like this:
export default async function Blogs() {
const [pageNumber, setPageNumber] = useState(0);
const posts = await getBlogPosts(6,pageNumber);
......
}
import { useState } from 'react';
export default function Blogs() {
const [numPosts, setNumPosts] = useState(6); // number of posts to display
const [posts, setPosts] = useState([]); // array of blog post data
const loadMore = () => {
setNumPosts(numPosts + 6); // increase the number of posts to display by 6
};
// fetch the blog post data when the component mounts or when the number of posts to display changes
useEffect(() => {
async function fetchData() {
const files = fs.readdirSync('posts');
const data = files.map((fileName) => {
const slug = fileName.replace('.md', '');
const readFile = fs.readFileSync(`posts/${fileName}`, 'utf-8');
const { data: frontmatter } = matter(readFile);
return {
slug,
frontmatter,
};
});
setPosts(data);
}
fetchData();
}, [numPosts]);
return (
<div className="mx-auto flex flex-wrap">
{posts.slice(0, numPosts).map(({ slug, frontmatter }) => (
<div
key={slug}
className="m-2 flex flex-col overflow-hidden rounded-xl border border-gray-200 shadow-lg"
>
<Link href={`/blog/${slug}`}>
<h3 className="p-4">{frontmatter.title}</h3>
</Link>
</div>
))}
{numPosts < posts.length && (
<button onClick={loadMore} className="my-4 mx-auto bg-blue-500 text-white px-4 py-2 rounded">
Load More
</button>
)}
</div>
);
}

Dynamically Delete Elements

I'm having trouble deleting elements. Instead of deleting a specific element, it only deletes the last newly created element. I'm not sure where I'm going wrong here. I referenced this tutorial that shows what I kinda want to do. (I'm new to React)
import React,{useState, useRef} from "react";
const Body = () => {
const [list, setList] = useState([]);
const AddInput = () => {
setList([...list, {placeholder:"Class Name"}]);
};
const DeleteInput = (index) => {
const l = [...list];
l.splice(index,1);
setList(l);
};
const InputChangeHandler = (event, index) => {
const l = [...list];
(l[index]).value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key)=>
<div key={key}>
<input type={"text"} id={key} placeholder={item.placeholder} onChange={e=>InputChangeHandler(e, key)}/>
<button id={key} onClick={() => DeleteInput(key)}>Delete</button>
</div>
)}
</div>
);
}
export default Body;
Element (input fields + button):
Deletes Last Created:
I think the main problem is the key of items that you set as react doc says:
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a
key. If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
As in this Article says:
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
-If your list is static and will not change.
-The list will never be re-ordered.
-The list will not be filtered (adding/removing items from the list).
-There are no ids for the items in the list.
If you set an reliable key in your items with some counter or id generator your problem would solve.
something like this:
export default function App() {
const [list, setList] = useState([]);
const id = useRef({ counter: 0 });
const AddInput = () => {
console.log(id);
setList([...list, { placeholder: "Class Name", id: id.current.counter++ }]);
};
const DeleteInput = (id) => {
setList(list.filter((item, i) => item.id !== id));
};
const InputChangeHandler = (event, index) => {
const l = [...list];
l[index].value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key) => (
<div key={item.id}>
<input
type={"text"}
id={key}
placeholder={item.placeholder}
onChange={(e) => InputChangeHandler(e, key)}
/>
<button id={item.id} onClick={() => DeleteInput(item.id)}>
Delete
</button>
</div>
))}
</div>
);
}
Use filter.
const DeleteInput = (index) => {
const l = list.filter((_, i) => i !== index);
setList(l);
};
Pass id to your DeleteInput function and for remove just filter the item list with id
const DeleteInput = (id) => {const filterItemList = list.filter((item) => item.id!== id);setList(filterItemList ); };

Get the downloadURL for each file from firebase storage

I'm trying to get the downloadURL for each file from the firebase storage. Currently I'm displaying the metadata (in this case the name of each file).
const [fileNames, setFileNames] = useState([]);
useEffect(() => {
const getFiles = async () => {
const storage = await firebase.storage().ref("/recursos");
const list = await storage.listAll();
const items = list.items.map((ref) => ref.getMetadata());
const results = await Promise.all(items);
setFileNames(results);
};
getFiles();
}, []);
return (
<div className="home">
<SinapseNav />
<Container className="d-flex align-items-center justify-content-center mt-4 mb-4">
<Card className="home-card">
<Card.Body>
<h3 className="text-center mb-4">Pesquisar Recursos</h3>
<Form.Control placeholder='Ex: "Trigonometria"' />
{fileNames.map((file) => {
return (
<Container key={file.name}>
<ResourceCard title={file.name} />
</Container>
);
})}
</Card.Body>
</Card>
</Container>
</div>
);
}
As explained in the doc, you can call getDownloadUrl() on the items of the ListResult you got through listAll(). Exactly the same way than you do with getMetadata().
So the following should do the trick:
const getUrls = async () => {
const storage = firebase.storage().ref("/recursos");
const list = await storage.listAll();
const items = list.items.map((ref) => ref.getDownloadUrl());
const urls = await Promise.all(items);
// Do whatever you want with the urls Array
};
Note that listAll() has a default pagination size is 1000: It is actually a "helper method for calling list() repeatedly until there are no more results".

My article state is not returning the proper data?

I'm still getting the hang of react and react hooks. I'm working on this react challenge found here: Link. My article state is not showing the proper data when the code first load. If you look in the console log my article state shows an empty array then the proper data but I need the proper data when my code first render otherwise my code won't execute right. It says I can't map over undefined. What's wrong with my code?
import React, {useState, useEffect} from 'react';
const Articles = () => {
const [article, setArticle] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
fetch(`https://jsonmock.hackerrank.com/api/articles?page=${page}`)
.then(res => res.json())
.then(data => setArticle(data))
}, [page])
const pgNums = Array(article.total_pages).fill().map((_, i) => i + 1)
console.log(article)
return (
<React.Fragment>
<div className="pagination">
{pgNums.map(n => <button data-testid="page-button" key={n} onClick={() => setPage(n)}>{n}</button>)}
</div>
<ul className="results">
{/* {article.data.map(article => <li key="title-1" data-testid="result-row">{article.title}</li>)} */}
</ul>
</React.Fragment>
);
}
export default Articles;
The issue is this when you try to access article it is not available. Because of asynchronous api call. What you can do either define another state loading which shows loader when data is fetching or render nothing if data is not available.
In your case you can do like this:
if(!article) return null; <- It means data is not available do not show anything
return (
<React.Fragment>
<div className="pagination">
{pgNums.map(n => <button data-testid="page-button" key={n} onClick={() => setPage(n)}>{n}</button>)}
</div>
<ul className="results">
{/* {article.data.map(article => <li key="title-1" data-testid="result-row">{article.title}</li>)} */}
</ul>
</React.Fragment>
each time page changes you are calling API, you should go with this conditional rendering. Instead of null you should return loading component/waitIndicator etc
const Articles = () => {
const [article, setArticle] = useState([])
const [page, setPage] = useState(1)
const [pgNums, setPageNums] = useState([])
useEffect(() => {
fetch(`https://jsonmock.hackerrank.com/api/articles?page=${page}`)
.then(res => res.json())
.then(data => {
console.log(data.data);
setArticle(data.data)
setPageNums(Array(data.total_pages).fill().map((_, i) => i + 1))
}
)
}, [page])
return (
<React.Fragment>
<div className="pagination">
{pgNums.length > 0 ? pgNums.map(n => <button data-testid="page-button" key={n} onClick={() => setPage(n)}>{n}</button>) : null}
</div>
<ul className="results">
{article.length > 0 ? article.map((article,i) => <li key={i} data-tes.tid="result-row">{article.title}</li>) : null}
</ul>
</React.Fragment>
);
}

Pokemon API Uncaught TypeError: Cannot read property '0' of undefined

I'm trying to access the abilities of my pokemon, but I keep getting the same error. I'm using React hooks to build my project, and the data that I fetched from Pokemon API was set to setWildPokemon. If I put wildPokemon.name, I'll get the name of the pokemon, which is fine. This also works when I output wildPokemon.abilities. However, when I start getting deeper into my nested objects, that's when things go
function App() {
const [pokedex, setPokedex] = useState([]);
const [wildPokemon, setWildPokemon] = useState({});
const [storeCard, setStoreCard] = useState({});
const { id, sprites } = wildPokemon;
// console.log(id, sprites.back_shiny);
console.log(wildPokemon);
console.log(wildPokemon.name);
// console.log(wildPokemon.types[0].name);
useEffect(() => {
encounterWildPokemon();
}, []);
const pokeId = () => {
const min = Math.ceil(1);
const max = Math.floor(151);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
const encounterWildPokemon = () => {
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokeId()}`)
.then(response => {
setWildPokemon(response.data);
});
};
const catchPokemon = pokemon => {
setPokedex(state => {
const monExists = state.filter(p => pokemon.id === p.id).length > 0; // mostly false. Only true if you catch the same pokemon
if (!monExists) {
state = [...state, pokemon];
state.sort(function(a, b) {
return a.id - b.id;
});
}
return state;
});
encounterWildPokemon(); // MISTAKE: we have to call this function whenever we're done
};
const releasePokemon = id => {
setPokedex(state => state.filter(p => p.id != id));
};
// PokeModal
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = pokemon => {
setShow(true);
setStoreCard(pokemon);
};
// JSX
return (
<div className="app-wrapper container">
<header>
<h1 className="title">React Hooks</h1>
{/* <img src="{sprites[0].back_default}" /> */}
<h3 className="subtitle">With Pokémon</h3>
</header>
<section className="wild-pokemon">
<h2>Wild Encounter</h2>
<img
src={
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/" +
wildPokemon.id +
".png"
}
className="sprite"
alt=""
/>
<h3>{wildPokemon.name}</h3>
<button className="catch-btn" onClick={() => catchPokemon(wildPokemon)}>
CATCH
</button>
</section>
UPDATED:
Ok, I just solved the problem =). I was trying to access the data as soon as the webpage renders. Since fetching is asynchronous, I was basically trying to get the data that hasn't existed yet, which is an empty object.
The state is an empty object on the initial load, and that's what is used for the first render. When you try to access sprites[0], sprites is undefined, since the data hasn't been loaded yet. one way to solve this issue is to delay the render until the data is fetched:
return (
sprites && sprites.length && (
<div className="app-wrapper container">
<header>
<h1 className="title">React Hooks</h1>
<img src={sprites[0].back_default} />
<h3 className="subtitle">With Pokémon</h3>
</header>
...
)
Alternatively you can use a loading state and set it to true until the data is fetched. Helpful when you want to show a loader meanwhile.
const [loading, setLoading] = useState(true);
const encounterWildPokemon = () => {
setLoading(true);
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokeId()}`)
.then(response => {
setWildPokemon(response.data);
setLoading(false);
});
};
// JSX
return (
loading ? <p>Loading...</p> : (
<div className="app-wrapper container">
<header>
<h1 className="title">React Hooks</h1>
<img src={sprites[0].back_default} />
<h3 className="subtitle">With Pokémon</h3>
</header>
...
)

Categories

Resources