React Hook useEffect issue with setState - javascript

I am using useState which has 2 array imageList and videoList and then in useEffect hook i am using forEach on data then if type is image then push to item to image .
But at last i am not getting imagelist or video list of array.
const [list, setDataType] = useState({imageList:[], videoList:[] });
useEffect (()=>{
//data is list of array
dataList.forEach(item =>{
if(!item.images) {
setDataType({...list, imageList:item})
}
else if (item.images[0].type === "video/mp4")
{
setDataType({...list, videoList :item})
}
else if((item.images[0].type === "images/gpeg")
{
setDataType({...list, imageList:item})
}
})
},);
Here type check is working correctly but at last i get last fetch data only which can be videolist or imageList
In last i should get list of all imageList whose type is image and same video list whose type is video

It is not a proper way to call setState in a loop. Below is an attempted solution using array method filter to functionally construct the list.
const [list, setDataType] = useState({ imageList: [], videoList: [] });
useEffect(() => {
let videos = dataList.filter(item => item.images[0].type === "video/mp4")
let images = dataList.filter(item => item.images[0].type === "images/gpeg")
setDataType({imageList: images, videoList: videos})
}, []);

Ideally you don't want to be calling setState constantly inside your loop. Build the state up and then set it.
Two filters might work for you here..
eg.
const [list, setDataType] = useState({imageList:[], videoList:[] });
useEffect (()=>{
//data is list of array
setDataType({
videoList: dataList.filter(item => item.images[0].type === 'video/mp4'),
imageList: dataList.filter(item => item.images[0].type === 'images/gpeg')
});
},);

Don't use the useEffect & useState hooks for data transformation, which is not part of an asynchronous flow.
This case is better handled with useMemo. When the datalist changes, useMemo will produce the list, and if it doesn't change, the memoized value would be used. In addition, it won't cause an infinite loop, because it won't cause a rerender while listening to its own state.
To create list, use a Map that will hold the mime-types and their respective types. Reduce the array of items, and according to the type you get from the Map, push them in the images or videos sub-arrays.
/** external code (not in the hook) **/
const types = new Map([['video/mp4', 'video'], ['images/gpeg', 'image']])
const getListType = ({ type }) => types.has(type) ?
`${types.get(type)}List` : null
/** hook code **/
const list = useMemo(() => dataList.reduce((r, item) => {
const list = getListType(item.images[0])
is(list) r[list].push(item)
return r;
}, { imageList:[], videoList:[] }), [dataList])

Avoid set state inside a loop. On each setState, React will re-render the component. So recommended to prepare your state first and then update the state.
const [list, setDataType] = useState({imageList:[], videoList:[] });
useEffect (() =>{
let newState = {imageList:[], videoList:[] };
dataList.forEach(item =>{
if(!item.images) {
newState.imageList.push(item);
} else if (item.images[0].type === "video/mp4") {
newState.videoList.push(item);
} else if((item.images[0].type === "images/gpeg") {
newState.imageList.push(item);
}
});
setDataType(prevState => {
return {
imageList: newState.imageList,
videoList: newState.videoList
}
});
},[]);

Related

Is there a way of running an if statement on whether or not props have changed in React

I currently have a component that holds a posts array, this component takes in a prop called sortBy which tells the component to either fetch data sorted by popular or new. The way my component is set up is upon initialization the posts are fetched then stored in redux, I only get new posts if my posts is empty. That way I'm not constantly fetching new data.
const posts = useSelector((state) => state.posts.posts);
useEffect(() => {
const { sortby } = props;
// here I would like to check if props has changed
if (Object.keys(posts).length === 0) {
dispatch(getPostsAsync(sortby)).then((response) => {
setLoading(false);
});
}
}, [dispatch, props, posts]);
Now I would like to check if the sortBy props has changed value. So originally it might be sortby='-numLikes' but when I sortby 'new' it should then be sortby='-createdAt' after it changes I would like to tell Redux to fetch new data.
All you need to do to get the behavior you want is to change the useEffect to:
const posts = useSelector((state) => state.posts.posts);
const { sortby } = props;
useEffect(() => {
dispatch(getPostsAsync(sortby)).then((response) => {
setLoading(false);
});
}, [dispatch, sortby]);
Then the useEffect will run every time sortby is changed.

React useEffect and useState interaction

I am using React and Material UI to create a table (XGrid) with some buttons. When you click the row, it should set the row id using useState. When you click the delete button, it should delete the row. It seems that the delete click handler is not using the value from use state. This is either some kind of closure thing or some kind of React thing.
const MyTableThing: React.FC = (props) =>
{
const { data } = props;
const [filename, setFilename] = React.useState<string>("")
const [columns, setColumns] = React.useState<GridColDef[]>([])
const handleDelete = () =>
{
someFunctionThatDeletes(filename); // filename is always ""
setFilename(""); // Does not do anything.. !
}
React.useEffect(() =>
{
if (data)
{
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) =>
{
if (column.field === "delete")
{
column.renderCell = (cellParams: GridCellParams) =>
{
return <Button onClick={handleDelete}>Delete</Button>
}
}
})
setColumns(columns)
}
}, [data?.files])
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) =>
{
console.log(`set selected row to ${param.data.id}`) // This works every time
setFilename(param.data.id)
}
}
The reason for this behavior is that React does not process setState action synchronously. It is stacked up with other state changes and then executed. React does this to improve performance of the application. Read following link for more details on this.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately
you can disable your deleteRow button till the filename variable is updated. you can use useEffect or setState with callback function.
useEffect(() => {
//Enable your delete row button, fired when filename is updated
}, filename)
OR
this.setFilename(newFilename, () => {
// ... enable delete button
});
Let me know if this helps! Please mark it as answer if it helps.
The main problem I see here is that you are rendering JSX in a useEffect hook, and then saving the output JSX into columns state. I assume you are then returning that state JSX from this functional component. That is a very bizarre way of doing things, and I would not recommend that.
However, this explains the problem. The JSX being saved in state has a stale version of the handleDelete function, so that when handleDelete is called, it does not have the current value of filename.
Instead of using the useEffect hook and columns state, simply do that work in your return statement. Or assign the work to a variable and then render the variable. Or better yet, use a useMemo hook.
Notice that we add handleDelete to the useMemo dependencies. That way, it will re-render every time handleDelete changes. Which currently changes every render. So lets fix that by adding useCallback to handleDelete.
const MyTableThing: React.FC = (props) => {
const { data } = props;
const [filename, setFilename] = React.useState<string>('');
const handleDelete = React.useCallback(() => {
someFunctionThatDeletes(filename); // filename is always ""
setFilename(''); // Does not do anything.. !
}, [filename]);
const columns = React.useMemo(() => {
if (!data) {
return null;
}
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) => {
if (column.field === 'delete') {
column.renderCell = (cellParams: GridCellParams) => {
return <Button onClick={handleDelete}>Delete</Button>;
};
}
});
return columns;
}, [data?.files, handleDelete]);
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) => {
console.log(`set selected row to ${param.data.id}`); // This works every time
setFilename(param.data.id);
};
return columns;
};

Array is initially empty, but after an entry in a text field which is used for filtering, it is full

I verushc an array from one component to another component.
The initial array is filled by a DB and is not empty.
If I try to map over the array in my second component, it is empty (length = 0);
However, after I wrote a value in a search box to filter the array, all articles appear as intended.
What is that about?
export default function Einkäufe({ alleEinkäufe, ladeAlleEinkäufe, url }) {
const [searchTerm, setSearchTerm] = React.useState("");
const [searchResults, setSearchResults] = React.useState(alleEinkäufe);
const listeFiltern = (event) => {
setSearchTerm(event.target.value);
};
React.useEffect(() => {
setSearchResults(alleEinkäufe);
}, []);
React.useEffect(() => {
const results = alleEinkäufe.filter((eink) =>
eink.artikel.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
}, [searchTerm]);
[...]
{searchResults.map((artikel, index) => {
return ( ... );
})}
}
The problem is with your useEffect hook that sets the list of searchResults, it's not rerun when alleEinkäufe property is updated. You need to add alleEinkäufe as it's dependency.
React.useEffect(() => {
setSearchResults(alleEinkäufe);
}, [alleEinkäufe]);
My bet is that the parent component that renders Einkäufe is initially passing an empty array which is used as searchResults state and then never updated since useEffect with empty dependencies array is only run once on the component's mount.
I would also advise you to use English variable and function names, especially when you ask for assistance because it helps others to help you.
Your search term intially is "". All effects run when your components mount, including the effect which runs a filter. Initially, it's going to try to match any article to "".
You should include a condition to run your filter.
React.useEffect(() => {
if (searchTerm) {
const results = alleEinkäufe.filter((eink) =>
eink.artikel.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
}
}, [searchTerm]);
BTW, "" is falsy.

useEffect break array of useState at first time

I am learning react hooks. I am having mock data js call "MockFireBase.js" as below:
const userIngredientsList = [];
export const Get = () => {
return userIngredientsList;
}
export const Post = (ingredient) => {
ingredient.id = userIngredientsList.length + 1;
userIngredientsList.push(ingredient);
return ingredient;
}
Then my react hooks component "Ingredients.js" will call this mock utilities as following details:
const Ingredients = () => {
const [userIngredients, setUserIngredients] = useState([]);
// only load one time
useEffect(() => { setUserIngredients(Get()); }, []);
const addIngredienHandler = ingredient => {
let responsData = Post(ingredient);
setUserIngredients(preIngredients => {
return [...preIngredients, responsData]
});
}
return (
<div className="App">
<IngredientForm onAddIngredient={addIngredienHandler} />
<section>
<IngredientList ingredients={userIngredients} />
</section>
</div>
);
)
}
When I added first ingredient, it added two (of course I get same key issue in console.log). Then I added second ingredient is fine.
If I remove the useEffect code as below, it will work good.
// only load one time
useEffect(() => { setUserIngredients(loadedIngredients); }, []);
I am wondering what I did anything wrong above, if I use useEffect
The problem is not in useEffect. It's about mutating a global userIngredientsList array.
from useEffect you set initial component state to be userIngredientsList.
Then inside addIngredienHandler you call Post(). This function does two things:
2a. pushes the new ingredient to the global userIngredientsList array`. Since it's the same instance as you saved in your state in step 1, your state now contains this ingredient already.
2a. Returns this ingredient
Then, addIngredienHandler adds this ingredient to the state again - so you end up having it in the state twice.
Fix 1
Remove userIngredientsList.push(ingredient); line from your Post function.
Fix 2
Or, if you need this global list of ingredients for further usage, you should make sure you don't store it in your component state directly, and instead create a shallow copy in your state:
useEffect(() => { setUserIngredients([...Get()]); }, []);

I keep getting map is not a function

import React, { useState, useEffect } from "react";
import { checkRef } from "./firebase";
function Dashboard() {
const [count, setCount] = useState([]);
let hi = [];
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
checkRef.on("value", snapshot => {
let items = snapshot.val();
let newState = [];
for (let tracker in items) {
newState.push({
reason: items[tracker].reason,
teacher: items[tracker].teacher
});
}
setCount({ items: newState });
});
// Update the document title using the browser API
// checkRef.on('value',(snapshot) => {
// console.log(snapshot.val())
// })
}, []);
return (
<div>
{count.items.map(item => {
return <h1>{item.reason}</h1>;
})}
</div>
);
}
export default Dashboard;
I am trying to return each item as an h1 after getting the item but i keep getting the error ×
TypeError: Cannot read property 'map' of undefined. I apoligize i AM new to web dev and trying to learn. I have spent way to much time with no results. thanks
Problem lies with your initalization of count.
For the very first render your count state variable doesn't contain any property named items. hence it fails.
your state variable is getting items prepery only after useEffect which excutes after first render.
based on your useeffct code, you should initialize count state variable with an object like follwing,
const [count, setCount] = useState({items:[]});
It sounds like useEffect is not calling the call back function (the one you defined).
You could try initializing the count.items to be an array
Or make sure that useEffect calls the callback function
Another way to fix your issue is simply do this:
{
count && count.items && count.items.map( item => {
// do something with the item
})
}

Categories

Resources