I'm trying to map through an array of objects and return a link property as a <li></li> but for some reason nothing is being returned:
import { useState, useEffect } from "react";
import { Config } from "../../config";
export default function Navbar() {
const [navs, setNavs] = useState();
const [mainNav, setMainNav] = useState();
const [links, setLinks] = useState();
useEffect(async () => {
const res = await fetch(`${Config.apiUrl}/navs`);
const data = await res.json();
setNavs(data);
// !!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
}, []);
useEffect(() => {
!!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
!!mainNav && setLinks(mainNav.map((item) => item.link));
}, [navs]);
!!links && console.log(links);
return (
<nav>
<ul>{!!links && links.map((item) => <li>{item.link}</li>)}</ul>
</nav>
);
}
The result of my console.log(links) shows the following output:
[
{
_id: "5f975d6d7484be510af56903", link: "home", url: "/", __v: 0, id: "5f975d6d7484be510af56903"
},
{
_id: "5f975d6d7484be510af56904", link: "posts", url: "/posts", __v: 0, id: "5f975d6d7484be510af56904"
},
]
I don't see why I can access the link property through item.link.
P.S. This is starting too look far from elegant, does anyone else have any idea how I can clean this up more?
The problem is here,
useEffect(() => {
!!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
!!mainNav && setLinks(mainNav.map((item) => item.link));
}, [navs]);
The problem is you are using mainNav from state right after you are setting the value in state in the above useEffect. It doesn't wait for the state to be updated and then take the result. You have two options here,
Either declare another useEffect like this,
useEffect(() => {
!!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
}, [navs]);
useEffect(() => {
!!mainNav && setLinks(mainNav.map((item) => item.link));
}, [mainNav]);
Or you can store the mainNav in a variable at first. Like this,
useEffect(() => {
let mainNavData = [];
if(!!navs) {
mainNavData = navs.map((x) => (x.name === "main-nav" ? x : null));
setMainNav(mainNavData);
setLinks(mainNavData.map((item) => item.link));
}
}, [navs]);
Related
I have a problem with the localStorage in my application. When I add items to a list of "favorites" they are stored without any problem in the localStorage, they can even be deleted by clicking them again.
But when I refresh the page, my application doesn't read that these items are in the favorites list and therefore doesn't mark them. Also, when I add a new item to the favorites list it causes it to delete everything from localStorage and start over.
Here's a gif of the localStorage view
Here's the code:
import React, { useState, useEffect } from 'react';
import SearchBar from '../../SearchBar/SearchBar.js';
import FiltersBox from '../FiltersBox/FiltersBox.js';
import { getItems } from '../../../Database/Database.js';
import './ItemsContainer.css';
function ItemsContainer() {
const [items, setItems] = useState([]);
const [search, setSearch] = useState('');
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
const [sortPrice, setSortPrice] = useState('');
const [filterCategory, setFilterCategory] = useState('');
const addItemToFavorites = item => {
let existentItem = favoriteItems.find(favItem => favItem.id === item.id);
if (existentItem) {
let filterTheExistentItem = favoriteItems.filter(
favItem => item.title !== favItem.title
);
setFavoriteItems(filterTheExistentItem);
let stringItems = JSON.stringify(filterTheExistentItem);
localStorage.setItem('Favorite Items', stringItems);
} else {
setFavoriteItems([...favoriteItems, item]);
let stringItems = JSON.stringify([...favoriteItems, item]);
localStorage.setItem('Favorite Items', stringItems);
}
};
const filteredItemsList = () => {
let newItemList = [];
newItemList = items.filter(item => {
if (filterCategory !== '' && filterCategory !== 'none') {
return item.category === filterCategory;
} else {
return item;
}
});
if (sortPrice === 'ascending') {
return newItemList.sort((a, b) => (a.price > b.price ? 1 : -1));
} else if (sortPrice === 'descending') {
return newItemList.sort((a, b) => (b.price > a.price ? 1 : -1));
} else {
return newItemList;
}
};
function onSortSelected(sortValue) {
setSortPrice(sortValue);
}
function onCategorySelected(categoryValue) {
setFilterCategory(categoryValue);
}
useEffect(() => {
getItems().then(res => setItems(res));
}, []);
useEffect(() => {
let xd = JSON.parse(localItems);
console.log(xd);
}, [localItems]);
return (
<div>
<SearchBar setSearch={setSearch} />
<FiltersBox
items={items}
setItems={setItems}
onSortSelected={onSortSelected}
onCategorySelected={onCategorySelected}
/>
<div>
{filteredItemsList()
.filter(item =>
search.toLowerCase() === ''
? item
: item.title.toLowerCase().includes(search)
)
.map(item => (
<div key={item.id}>
<div>{item.title}</div>
<button
className={favoriteItems.includes(item) ? 'si' : 'no'}
onClick={() => addItemToFavorites(item)}>
Add to favorites
</button>
</div>
))}
</div>
</div>
);
}
export default ItemsContainer;
And here I leave a GIF with a continuous console.log of the localStorage:
I tried everyrhing, and I don't know what is happening.
You're retrieving your items in localItems and... you do nothing with this variable. You should initialize your state favoritesItems with your local storage
const getItemsFromLocalStorage = () => {
const items = localStorage.getItem('Favorite Items');
return items ? JSON.parse(items) : [];
}
const [favoriteItems, setFavoriteItems] = useState(getItemsFromLocalStorage())
This is where the culprit is:
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
You load localStorage into localItems, but you expect it to be in favoriteItems, where you have never assigned it. You would need to specify the item of localStorage as the initial state, like:
let localItems = localStorage.getItem('Favorite Items');
const [favoriteItems, setFavoriteItems] = useState(localItems ? localItems : []);
I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};
I am trying to filter an array
Data:
0: {question: "xx", answer: "<p>xx</p>", userToDisplayFor: "LiteUsers"}
1: {question: "x", answer: "<p>xx</p>", userToDisplayFor: "LiteUsers"},...
I get filterdFaqs is not defined, any ideas?
export const Faq = ({ navigation }) => {
const {
faq,
userTypeReducer: { userType },
} = useSelector((state) => state);
const { data, loading, error } = faq;
const dispatch = useDispatch();
const goBack = () => {
...
};
useMountEffect(() => {
trackScreen(screens.FAQ);
dispatch(FaqList());
}, []);
if (userType == "Lite") {
const filterdFaqs = data.filter((q) => q.userToDisplayFor == "LiteUsers");
} else {
const filterdFaqs = data.filter((q) => q.userToDisplayFor !== "LiteUsers");
}
console.log(filterdFaqs);
return (
...
<FlatList
...
data={filterdFaqs}
renderItem={...}
/>
..
);
};
When you use const inside of any block, it only exists in that block:
{
const foo = 'bar';
}
console.log(foo);
So what you need to do is declare filterdFaqs outside of the if:
let filterdFaqs;
if (userType == "Lite") {
filterdFaqs = data.filter((q) => q.userToDisplayFor == "LiteUsers");
} else {
filterdFaqs = data.filter((q) => q.userToDisplayFor !== "LiteUsers");
}
or inline the if:
const filterdFaqs = userType === "Lite" ? data.filter((q) => q.userToDisplayFor == "LiteUsers") : data.filter((q) => q.userToDisplayFor !== "LiteUsers");
Consider the following:
I have a parent functional component with a nested child component,
function Parent(){
const [isFlagTrue, setIsFlagIsTrue] = useState(false);
const handleFlagChange = (bool) => setIsFlagIsTrue(bool)
useEffect(() => {
console.log("isFlagTrue ", isFlagTrue);
}, [isFlagTrue])
return (
<Child handleFlagChange={handleFlagChange} />
)
}
In the Child I'm making an async call to populate a data-table;
import { useDispatch } from 'react-redux';
function Child({handleFlagChange}) {
const dispatch = useDispatch();
const componentIsMounted = useRef(true)
const [rows, setRows] = useState([]);
useEffect(() => {
if (currentProductItem && currentProductItem.value != null) {
dispatch(getClientVariables(currentProductItem.value)).then(r => {
rawData.current.Rows = r.payload;
dispatch(getClientVariableTypesAll(currentProductItem.value)).then(r => {
rawData.current.ClientDataVariableTypes = r.payload.map(r => {
return {
value: r.ClientDataVariableTypeID,
label: r.ClientDataVariableTypeName
}
});;
setRows(rawData.current.Rows);
console.log('rawData', rawData);
});
});
}
}, [currentProductItem, justSavedNewVariableTypes, justSavedGrid]);
}
useEffect(() => {
console.log("typeof handleFlagChange ", typeof handleFlagChange);
console.log("rows ", rows);
// if (componentIsMounted.current) {
// var flag = rows.some(item => item.ClientDataVariableTypeName == null)
// handleFlagChange(flag)
// }
if (Array.isArray(rows) && typeof handleFlagChange != 'undefined') {
console.log("foo ");
var flag = rows.some(item => item.ClientDataVariableTypeName == null)
handleFlagChange(flag)
}
}, []);
useEffect(() => {
return () => {
componentIsMounted.current = false
}
},[])
....other code & rendering
}
I am expecting the isFlagTrue console in the useEffect of the parent to fire when the rows have been validated in the child by the return value of the some function on the array of rows.
I have tried two solutions one is insuring the <Child/> is mounted (and having the data call being full-filled) by setting a ref to true using useRef().
In Child:
const componentIsMounted = useRef(true)
useEffect(() => {
console.log("typeof handleFlagChange ", typeof handleFlagChange);
if (componentIsMounted.current) {
var flag = rows.some(item => item.ClientDataVariableTypeName == null)
handleFlagChange(flag)
}
}, []);
useEffect(() => {
return () => {
componentIsMounted.current = false
}
},[])
But I get TypeError: handleFlagChange is not a function
So then I tried:
useEffect(() => {
console.log("typeof handleFlagChange ", typeof handleFlagChange);
if (componentIsMounted.current && typeof handleFlagChange != 'undefined' && typeof handleFlagChange != 'undefined') {
console.log("foo ");
var flag = rows.some(item => item.ClientDataVariableTypeName == null)
handleFlagChange(flag)
}
}, []);
But that yields:
typeof handleFlagChange undefined. <---In the Child
isFlagTrue false <--- In Parent
Any ideas?
Have you consider using a default value for the function and let the normal flow of React do yours?
function Child({handleFlagChange = () => {}})
Let me know if this works.
I think this is a typescript error saying that the type of handleFlagChange is not known in child Component.
You have 2 option here:
First, declare type of each prop at first and then use it in function:
type PropsType = { handleFlagChange: () => void }
function Child({handleFlagChange}: PropsType)
Or try to add specific type in the function itself:
function Child({handleFlagChange: () => void})
where () => void being the type of handleFlagChange function.
I am creating a Memory Game and I can't understand how can these two lines order matters in the code:
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that works
setPicks([])
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));
}
}, 500)
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that doesn't work
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));
setPicks([])
}
}, 500)
In the first case, matched state receive the correct updates so why similar cards match, but in the second case it seems that matched state doesn't get correct values.
I can't understand how the order of these two lines affect matched state
const App = () => {
const icons = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
const shuffle = cards => [...cards].sort(() => Math.random() - 0.5);
const shuffledCards = useMemo(() => {
return shuffle(icons).map((icon, i) => ({
icon: icon,
id: i,
isFlipped: false
}));
});
const [picks, setPicks] = useState([]);
const [cards, setCards] = useState(shuffledCards);
const [matched, setMatched] = useState([]);
const handleClick = id => {
!picks.includes(id) && picks.length !== 2 && setPicks([...picks, id]);
};
useEffect(() => {
setCards(cards =>
cards.map((c, i) =>
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
const matches = cards.reduce((matches, { icon, isFlipped }) => {
!matches[icon] && (matches[icon] = 0);
isFlipped && matches[icon]++;
return matches;
}, {});
Object.entries(matches).forEach(([icon, count]) => {
count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
});
const timer = setTimeout(() => {
if (picks.length === 2) {
//the problem is here, that order doesn't work
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
return () => clearTimeout(timer);
}, [picks]);
return (
<div class="game">
<Deck
cards={cards}
handleClick={handleClick}
picks={picks}
matched={matched}
/>
</div>
);
};
const Deck = ({ numbers, cards, ...props }) => {
return (
<div class="deck">
{cards.map((c, i) => {
return <Card i={i} card={c} {...props} />;
})}
</div>
);
};
const Card = ({ handleClick, picks, card, i, matched }) => {
const { icon, isFlipped, id } = card;
return (
<div
className={`card${isFlipped ? " flipped" : ""} ${
matched.includes(icon) ? "matched" : ""
}`}
onClick={() => handleClick(id)}
>
<div class="front" />
<div class="back">{icon}</div>
</div>
);
};
ISSUE :
Here main issue is with the below line position in code:
const matches = cards.reduce((matches, { icon, isFlipped }) => {
So to understand the issue, first, we need to understand what is wrong with the above line position, please do read comments to understand the issue better
setCards(cards => // <--------- 1.
cards.map((c, i) => // <--------- 2.
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
// 1. current state of cards
// 2. returning updated state of cards
// so, if you are aware of async behavior or setState, then you know
// the below line will always point to snapshot of cards when the useEffect run
// and not to the 1. or 2.
// So here cards is pointing to snapshot when useEffect run
const matches = cards.reduce((matches, { icon, isFlipped }) => {
Now, let's see with both cases
useEffect(() => {
// Here you will always get the snapshot of cards values, which is currently available
setCards(cards => // <--------- 1.
cards.map((c, i) => // <--------- 2.
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
// so if you understand correctly, above code block doesn't make any sense for the below line
const matches = cards.reduce((matches, { icon, isFlipped }) => {
....
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that works
setPicks([]) // <---- this will trigger useEffect , and send the cards value from 2.
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));
}
}, 500)
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that works
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false }))); // <------------- 3.
setPicks([]) // <---- this will trigger useEffect , and send the cards value from 3.
}
}, 500)
},[picks]); // <----- useEffect is dependent on picks
DEBUGGING DEMO :
You can change the order as per your case and check the console and see what are the values you are getting for cards when you switch the order
Solution :
Hope this will make your doubts clear, now If we are clear with issue, what's the solution then, here we can solve it 2 ways but the basic is common in both way and that is Always work with updated cards value, in that case order doesn't matter any longer
FIRST SOLUTION :
You can put the match condition code block inside the setCards(cards => { and work with latest cards values
useEffect(() => {
setCards(cards => {
const updated = cards.map((c, i) =>
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
// take whole code block and put it inside the `setCards`
// and directly work with latest update value
const matches = updated.reduce((matches, { icon, isFlipped }) => { // <---- HERE
!matches[icon] && (matches[icon] = 0);
isFlipped && matches[icon]++;
return matches;
}, {});
Object.entries(matches).forEach(([icon, count]) => {
count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
});
return updated;
});
const timer = setTimeout(() => {
if (picks.length === 2) {
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
return () => clearTimeout(timer);
}, [picks]);
WORKING DEMO :
SECOND SOLUTION : ( and I would suggest this one )
You can create a useEffect for cards value, so that you will always get the updated cards value and based on that you can set the match values
useEffect(() => {
setCards(cards =>
cards.map((c, i) =>
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
const timer = setTimeout(() => {
if (picks.length === 2) {
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
return () => clearTimeout(timer);
}, [picks]);
useEffect(() => {
const matches = cards.reduce((matches, { icon, isFlipped }) => {
!matches[icon] && (matches[icon] = 0);
isFlipped && matches[icon]++;
return matches;
}, {});
Object.entries(matches).forEach(([icon, count]) => {
count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
});
}, [cards]);
WORKING DEMO :
The code block you are asking about is responsible for resetting the card state everytime two cards are picked.
const timer = setTimeout(() => {
if (picks.length === 2) {
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
The state of the picked cards picks must be reset by passing it an empty array through the hook setPicks before the card state is set with the hook setCards since picks can only be set if there is not exactly 2 picks made. Resetting the picks length to zero is required in the current logic you have because of what you have in your click handler.
const handleClick = id => {
!picks.includes(id) && picks.length !== 2 && setPicks([...picks, id]);
};