I'm working on a react native application.
I get the result of an SQL query like this:
const [ food, setFood ] = useState([]);
const load_food = async () => {
db.listProduct().then(row => setFood(row))
};
useFocusEffect( () => { load_food(food) }, [ food ] );
If I make a log like this:
console.log(food[i].PRODUCTNAME)
I get the name of my ingredient:
"Orange Juice"
Later I want to create a list from the variable food
const [listData, setListData] = useState(
Array(10)
.fill('')
.map((_, i) => ({ key: `${i}`, text: `Product name: ${food[i].PRODUCTNAME}`}))
);
But I have the following error:
undefined is not an object (evaluating 'food[i].PRODUCTNAME')
I imagine it's a synchronization issue. But I don't know how to solve it
You're mainly correct, it could be a sync problem, and you can use some techniques to avoid it, useEffect is one of them
const [listData, setListData] = useState([]);
useEffect(()=> {
setListData(
Array(10)
.fill('')
.map((_, i) => ({ key: `${i}`, text: `Product name: ${food[i].PRODUCTNAME}`}))
)
}, [food]);
This will only set listData state when food is updated, but also you will have to check food has at least 10 items or you will get undefined again
Related
I ran into an asynchronous useState problem.
I have a situation where I first need to add an object to the state array in the handler. And then add this state to the localStorage.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
If I delete the entire localStorage first and run the handler. So an empty array is added to my localStorage (the state shows me updated). After the action again, the required object is finally added to my array.
I tried something like this, but still the same problem.
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
localStorage.setItem(storageItemName, JSON.stringify(tempArray));
How do you solve this, please?
/// EDIT
I have something like this
const FavoriteSong = () => {
const song = { id: 1, name: "Lala", length: "3:20" };
const [favoritedSongs, setFavoritedSongs] = useState([]);
const [isFavorited, setIsFavorited] = useState(false);
useEffect(() => {
if (localStorage.getItem("storageItemName")) {
const storageSongs = JSON.parse(
localStorage.getItem("storageItemName") || ""
);
setFavoritedSongs(storageSongs);
const foundSong = storageSongs?.find((song) => song.id === song.id);
foundSong ? setIsFavorited(true) : setIsFavorited(false);
}
}, [song]);
const handleClick = () => {
if (isFavorited) {
const filteredSong = favoritedSongs.filter(
(song) => song.id !== song.id
);
localStorage.setItem("storageItemName", JSON.stringify(filteredSong));
setIsFavorited(false);
} else {
setFavoritedSongs((prev) => [...prev, song]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
setIsFavorited(true);
}
};
return <div onClick={handleClick}>CLICK</div>;
};
export default FavoriteSong;
Just place your localStorage.set logic inside a useEffect to make sure it runs after the state actually changes.
useEffect() => {
localStorage.setItem(...);
}, [favoritedSongs]};
For that you can Use the condition If data in the array then It will set in localStorage otherwise not
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
tempArray.length && localStorage.setItem(storageItemName, JSON.stringify(tempArray));
.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
FavoritedSongs.length(your state name) && localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
So in my project I have several instances where I have to fetch data from firebase that would change rather frequently.
I have decided to use custom hooks to fetch the data and reuse them in different components if need be.
For example, I might need to retrieve the data for a single task and other times I need to fetch data for an array of tasks.
Currently I have implemented the following to retrieve a single task:
const useTask = (taskId: string) => {
const [taskData, setTaskData] = useState<TaskWithId>({} as TaskWithId)
useEffect(() => {
const taskCollectionRef = collection(db, 'tasks')
const qTask = query(taskCollectionRef, where(documentId(), '==', taskId))
const unsubscribe = onSnapshot(qTask, querySnapshot => {
querySnapshot.forEach(doc => {
setTaskData({
id: doc.id,
title: doc.data().title,
createdBy: doc.data().createdBy,
description: doc.data().description,
assignedTo: doc.data().assignedTo,
parent: doc.data().parent,
createdAt: doc.data().createdAt,
modifiedAt: doc.data().modifiedAt,
dueDate: doc.data().dueDate,
completed: doc.data().completed,
})
})
})
return unsubscribe
}, [taskId])
return taskData
}
export default useTask
For multiple tasks, I am using another custom hook as follows:
const useTaskArr = (taskArr: string[]) => {
const [taskArrData, setTaskArrData] = useState<TaskWithId[]>([])
useEffect(() => {
if (taskArr.length == 0) {
setTaskArrData([])
return
}
const taskCollectionRef = collection(db, 'tasks')
const qTask = query(taskCollectionRef, where(documentId(), 'in', taskArr))
const unsubscribe = onSnapshot(qTask, querySnapshot => {
setTaskArrData(
querySnapshot.docs.map<TaskWithId>(doc => ({
id: doc.id,
title: doc.data().title,
createdBy: doc.data().createdBy,
description: doc.data().description,
assignedTo: doc.data().assignedTo,
parent: doc.data().parent,
createdAt: doc.data().createdAt,
modifiedAt: doc.data().modifiedAt,
dueDate: doc.data().dueDate,
completed: doc.data().completed,
}))
)
})
return unsubscribe
}, [taskArr])
return taskArrData
}
export default useTaskArr
As you can see, the query is different if it is a single task or an array and the type of the state is also different.
How could I merge both in a single custom hook and pass whether I would need to retrieve an array of tasks or a single one?
I am new to react and I am building a component thats going to be displayed within the profile page for a specific type of user of the app that I am working on "podcaster". I am trying to display their podcasts in a responsive list but I keep getting this typeError. I have looked at different examples here on SO but I have not been able to find the problem.
I am using 2 hooks to get this info [useState & useEffect] yet I am not able to fully understand why I am getting this error.
import { FC, useState, useEffect, Fragment } from 'react'
import ResponsiveList from '../ResponsiveList'
const UserProfilePodcaster: FC = () => {
const {
user: { id, accountType },
} = useAuth()
const [podcasts, setPodcasts] = useState<any[]>([])
const [showModal, setShowModal] = useState(false)
const [currentPodcast, setCurrentPodcast] = useState<any>({})
const [isLoading, setIsLoading] = useState(true)
const [categories, setCategories] = useState<{ name: string; code: string }[]>([])
useEffect(() => {
;(async function () {
const categories = await getCategories()
const podcasts = await getPodcast(id)
setCategories(categories)
setPodcasts(podcasts)
setIsLoading(false)
})()
}, [id])
<ResponsiveList
data={podcasts.map((podcast: any) =>
podcast.name({
...podcast,
edit: (
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => handleEditPodcast(podcast)}
>
Edit
</Button>
),
})
)}
keys={[
{ key: 'podcasts', label: 'Podcast Name' },
{ key: 'categories', label: 'Podcast Categories' },
{ key: 'description', label: 'Podcast Niche' },
{ key: 'Reach', label: 'Podcast Reach' },
{
key: 'edit',
label: 'Edit',
},
]}
emptyMessage="It seems that you have not created a podcast yet, go out and start one 😢"
/>
map is a function of the Array prototype. If map is not a function, it means your podcasts is not an array. This might happen due to bugs in programming or as a result of podcasts being the result of e.g. an API call which is not there yet (is undefined) on first render as the API call has not resolved yet.
There are many ways to go about the latter case. You could e.g. write
data={podcasts?.map(...)}
and make sure the <ResponsiveList/> handles undefined data well, or you could render a loader for as long as the data is not there yet.
You can use lodash package. lodash will handle this type of errors
import _ from 'lodash';
.
.
.
data={_.map(podcasts, pod => {pod['key_name']})}
I am wanting to use the "Cascader" component of "Ant Design" but I am having trouble filling it with data. This is my code, which I am doing wrong, sorry I am still a newbie and I need your support please.
function CascaderEmpCliUn(props) {
const optionLists = { a: []}
const [state, setState] = useState(optionLists);
useEffect(() => {
async function asyncFunction(){
const empresas = await props.loginReducer.data.empresas;
const options = [
empresas.map(empresa => ({
value: empresa.id,
label: empresa.siglas,
children: [
empresa.cli_perm.map(cliente => ({
value: cliente.id,
label: cliente.siglas,
children: [
cliente.uunn_perm.map(un => ({
value: un.id,
label: un.nombre,
}))
]
}))
]})
)
];
setState({a : options})
}
asyncFunction();
}, [])
return (
<Cascader options={state.a} placeholder="Please select" />
)
}
ERROR
Not found value in options
I was able to reproduce your error with dummy data whenever I had an empty array of children at any level. I'm not sure why this should be a problem, but it is. So you need to modify your mapping function to check the length of the child arrays. It seems to be fine if passing undefined instead of an empty array if there are no children.
General Suggestions
You don't need to store the options in component state when you are getting them from redux. It can just be a derived variable. You can use useMemo to prevent unnecessary recalculation.
You are passing the entire loginReducer state in your props which is not ideal because it could cause useless re-renders if values change that you aren't actually using. So you want to minimize the amount of data that you select from redux. Just select the empresas.
Revised Code
function CascaderEmpCliUn() {
// you could do this with connect instead
const empresas = useSelector(
(state) => state.loginReducer.data?.empresas || []
);
// mapping the data to options
const options = React.useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? undefined
: empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? undefined
: cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);
return <Cascader options={options} placeholder="Please select" />;
}
The final code of "options" object:
const options = useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? console.log("undefined")
:
empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? console.log("undefined")
:
cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);
I need advice on where to perform data filtering to achieve best performance. Let's say I receive a big array of products from one endpoint of a remote API and product categories from another endpoint. I store them in Redux state and also persist to Realm database so that they are available for offline usage.
In my app, I have a Stack.Navigator that contains 2 screens: ProductCategories and ProductsList. When you press on a category it brings you to the screen with products that fall under that category. Currently, I perform the data filtering right inside my component, from my understanding it fires off every time the component is rendered and I suspect this approach slows down the app.
So I was wondering if there is a better way of doing that? Maybe filter the data for each category in advance when the app is loading?
My code looks as follows:
const ProductCategories = (props) => {
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) {
setItems(props.productCategories);
}
}, [isFocused]);
return (
...
);
};
const mapStateToProps = (state) => ({
productCategories: state.catalog.productCategories,
});
const ProductsList = (props) => {
const isFocused = useIsFocused();
const productsFilteredByCategory = props.products.filter((product) => {
return product.category === id;
});
useEffect(() => {
if (isFocused) {
setItems(productsFilteredByCategory);
}
}, [isFocused]);
return (
...
)
const mapStateToProps = (state) => ({
products: state.catalog.products,
});
You have to normalize (you can see main principles here) data in redux, to the next view:
// redux store
{
categories: {
1: { // id of category
id: 1,
title: 'some',
products: [1, 2, 3] // ids of products
},
...
},
products: {
1: { // id of product
id: 1,
title: 'some product',
},
...
}
}
Then you can create few selectors which will be even without memoization work much faster then filter, because time of taking data from object by property is constant
const getCategory = (state, categoryId) => state.categories[categoryId]
const getProduct = (state, productId) => state.products[productId]
const getCategoryProducts = (state, categoryId) => {
const category = getCategory(state, categoryId);
if (!category) return [];
return category.products.map((productId) => getProduct(state, productId))
}