Can't update array state using information from another state - javascript

I have two state objects. One is personnel, an array of 1-object arrays like this: [[{}],[{}],[{}],[{}],...]. Another is rowItems which I am trying to fill by pulling out all the objects from the inner arrays of the big personnelarray.
My end goal is to use the rowItems to create a material-ui data grid. Right now, the data grid is empty and not rendering any data, but shows the correct number of personnel items I expect (253) in the pagination display, which is weird.
Here's my code:
const [personnel, setPersonnel] = useState([]);
const [rowItems, setRowItems] = useState([]);
const handleCallback = (data) => {
setPersonnel((prevData) => [...prevData, data]);
};
useEffect (() => {
console.log("personnel:", personnel) // I see all 253 arrays printed
setRowItems((rowItems => [...rowItems, {id: '59686', first_name: 'vbn',}])) // This was for testing only, somehow hardcoding this works
personnel?.map((row) => {
console.log("row", row[0]); // I see the item being printed
setRowItems(rowItems => [...rowItems, row[0]]);
console.log("row items", rowItems) // this is empty. WHYYYY
})
}, [personnel])
return (
<div> // This is where I get personnel items and pass to callback
{props.personnel.edges.map(({ node }) => {
return (
<Personnel
key={node.__id}
personnel={node}
parentCallback={handleCallback}
/>
);
})}
</div>
<DataGrid
columns={cols}
rows={rowItems}
pageSize={12}
/>
)

I took jsN00b's suggestion and tried to move the setRowItems() outside of the map function like so:
useEffect(() => setRowItems(prev => ([ ...prev, ...personnel?.map(row => ({...row[0]}))])), [personnel]);
and it worked! Thanks a million!

Related

How do I map data from state to component prop?

To start with I'm a beginner. Any help would be appreciated.
So I'm getting my data from mongoDB atlas using node+express API. I'm successfull at getting the array to show up in console log using following code.
const [product, setProduct] = useState();
const url = "http://localhost:5000/api/items";
useEffect(() => {
axios.get(url).then((res) => {
setProduct(res.data);
// setProduct(
// JSON.stringify({
// title: setProduct.title,
// price: setProduct.price,
// image: setProduct.image,
// details: setProduct.details,
// })
// );
})
}, [url])
console.log(product)
The console log displays the array properly as collection named 'items' with content of arrays. As you can see I tried to stringify the response as the response returns JSON but again I didn't know how to map Following is the code where I tried to map the contents like id, name etc as props to component.
<div>
{product.map((product) => {
<Product name={product.title} />
})}
</div>
When I do this I get error that the map is not a function. I don't know what I'm doing wrong here. I know I'm supposed to use redux or reducer/context here but I want to get this to work before updating it with those.
[![Response from res.data][1]][1]
[1]: https://i.stack.imgur.com/auxvl.png
you didnt get yours products.
As we can see from screenshot
res.data equal to object with one property items:
res.data= {items: []}
and we need to take/access to these items
use this: setProducts(res?.data?.items || [])
const [products, setProducts] = useState();
useEffect(() => {
axios.get(url).then((res) => {
setProducts(res?.data?.items || []);
})
}, [url])
<div>
{products?.map((product) => {
<Product name={product.title} />
})}
</div>
On the first render the value for types will be undefined ( on sync code execution ), try using it as
<div>
{product?.map((product) => {
<Product name={product.name} />
})}
</div>
? will make sure to run map once value for types is there ( also make sure it is mappable ( is an array ))

With useState How do I set initial state to an empty Array, and then add empty Objects to that Array when I button is Clicked?

I want to mount a component which will mount children components based on how many times a user hits a + ingredient icon as shown here (this screenshot shows one hardcoded to render)
I want to achieve this by setting an ingredients state to initially be an empty array
const [ingredients, setIngredients] = useState([]);
And then have the onClick action of the plus icon add an empty object to this array, while preserving any objects that have been added/modified
const addElementToArray = () => {
setIngredients((prevIngredients) => [
...prevIngredients, {}
]);
}
However, the current behavior is a bit mysterious to me.
When console.log(ingredients) it is initially an empty array, great
However, when I click the + icon once it transforms to:
[{…}, [object Object]: {…}]
and then I believe back to an empty object
And finally, when I click it again the component crashes with the error:
Uncaught TypeError: prevIngredients is not iterable
I'm unsure how to successfully get the functionality I want, any help is appreciated.
Full file below:
const RecipeCreate = props => {
const navigate = useNavigate();
const [ ingredients, setIngredients ] = useState([]);
const [ recipeTitle, setRecipeTitle ] = useState('');
useEffect(() => {
console.log(ingredients)
renderIngredientComponents()
}, [ingredients])
//will render IngredientDetailsInput for each obj in ingredients
//and will be updated using callbacks from that child component
const renderIngredientComponents = () => {
if (ingredients.length > 0) {
return ingredients.map((index, ingredient) => {
return <IngredientDetailsInput
key={index}
position={index}
updateIngredientArray={updateIngredientArray}
removeIngredient={removeIngredient}
/>
})
}
}
//failing function
const addElementToArray = () => {
setIngredients((prevIngredients) => [
...prevIngredients, {}
]);
}
return (
<div>
<div>
<form onSubmit={e => handleSubmit(e)}>
<div>
<label>Recipe Title</label>
<input
type="text"
name="recipeTitle"
value={recipeTitle}
onChange={e => setRecipeTitle(e.target.value)}/>
</div>
<div>
<p>Ingredients</p>
{renderIngredientComponents()}
<div>
<p onClick={()=> addElementToArray()}>+ ingredient</p>
</div>
</div>
<button type="submit">Submit</button>
</form>
</div>
</div>
)
}
export default RecipeCreate;
There are a number of problems here:
setIngredients(ingredients.splice(ingredients[position], 1))
splice mutates the array it's called on - but ingredients is stateful, and state should never be mutated. Mutating state often results in confusing and unpredictable behavior, and should not be done in React.
splice returns the removed element, not the mutated array
You need
setIngredients(ingredients.filter((_, i) => i !== position));
Also
setIngredients(ingredients[position] = details)
I know the comment says it's not implemented yet, but it's also causing a bug - you're passing details to setIngredients, so that it's no longer an array.
setIngredients(ingredients[position] = details)
is equivalent to
ingredients[position] = details;
setIngredients(details)
I'm not sure what logic you want there, but it's causing problems - remove that part entirely until you implement it properly, else it'll mess up your state.

Why does calling one `setState` function update an entirely separate state value?

I have an issue where one setState function is updating two separate state values.
I am trying to make a small React sortable array. The program should behave like so:
fetches data on mount
stores that data in both unsortedData & displayedData state hooks
when a user clicks "toggle sorting" button the array in displayedData is sorted (using .sort())
on a second click of "toggle sorting" it should set displayedData to be the same value as unsortedData - restoring the original order
However, on first click of toggle sorting, it sorts both unsortedData & displayedData, which means I lose the original data order. I understand I could store their order, but I'm wanting to know why these state values appear to be coupled.
Stackblitz working code here.
GIF showing issue here
I cannot find where this is happening. I can't see any object/array referencing going on (I'm spreading for new objects/arrays).
Code here:
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [displayedData, setDisplayedData] = useState([])
const [isSorted, setisSorted] = useState(false)
const handleSorting = () => setisSorted(!isSorted)
useEffect(() => {
if (isSorted === true) setDisplayedData([...unsortedData.sort()]) // sort data
if (isSorted === false) setDisplayedData([...unsortedData]) // restore original data order
}, [isSorted, unsortedData])
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData]) // store original data order in "unsortedData"
setDisplayedData([...mockData])
}, [])
return (
<div>
{displayedData.map(item => item)}
<br />
<button onClick={() => handleSorting()}>Toggle sorting</button>
</div>
)
}
The .sort function mutates the array, so when you do this:
setDisplayedData([...unsortedData.sort()])
You are mutating unsortedData, then making a copy of it afterwards. Since you mutated the original array, that change can display on the screen when the component rerenders.
So a minimum fix would be to copy first, and sort afterwards:
setDisplayedData([...unsortedData].sort())
Also, why do I need the useEffect? Why can't I just move the contents of the useEffect (that has the if statements in) into the handleSorting function?
Moving it into handleSorting should be possible, but actually i'd like to propose another option: have only two states, the unsorted data, and the boolean of whether to sort it. The displayed data is then a calculated value based on those two.
const [unsortedData, setUnsortedData] = useState([3, 9, 6]) // initialize with mock data
const [isSorted, setisSorted] = useState(false)
const displayedData = useMemo(() => {
if (isSorted) {
return [...unsortedData].sort();
} else {
return unsortedData
}
}, [unsortedData, isSorted]);
const handleSorting = () => setisSorted(!isSorted)
// No use useEffect to sort the data
// Also no useEffect for the mock data, since i did that when initializing the state
The benefits of this approach are that
You don't need to do a double render. The original version sets isSorted, renders, then sets displayed data, and renders again
It's impossible to have mismatched states. For example, in between that first and second render, isSorted is true, and yet the displayed data isn't actually sorted yet. But even without the double render case, having multiple states requires you to be vigilant that every time you update unsortedData or isSorted, you also remember to update displayedData. That just happens automatically if it's a calculated value instead of an independent state.
If you want to display sorted array to user only when the user presses the button, you could use this code statement (orignaly coppied from vue3 todo list)
const filters = {
none: (data) => [...data],
sorted: (data) => [...data].sort((a, b) => a - b),
}
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [currentFilter, setCurrentFilter] = useState('none')
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData])
}, [])
return (
<div>
<ul>
{/* All magic is going here! */}
{filters[currentFilter](unsortedData).map(item => <li key={item}>{item}</li>)}
</ul>
<br />
<button
onClick={() =>
setCurrentFilter(currentFilter === 'none' ? 'sorted' : 'none')}>
Toggle sorting
</button>
</div>
)
}

How to remove element's ref after filtering it from the list

I'm rendering an array of a 'brick' objects as a simple divs on my component.
i am also saving all the refs of the bricks with useRef, when every brick element is saved וnder it's id in a 'brickRefs' object.
under some conditions i am filtering my array so it will render less 'bricks' on the screen.
my problem is: when filtering the array, instead of removing the element i filtered from the 'brickRefs' object - i see that it's still there with a value of null;
what will be the right way to remove it completely from the ref's object ?
//initial ref and state:
const bricksRef = useRef({});
const [bricks, setBricks] = useState([]);
useEffect(() => {
// initializing bricks data
const bricksData = getBricks(NUMBER_OF_BRICKS);
setBricks(bricksData);
}, [])
// filtering the array under some conditions :
const isHittedBrick = (nextTopPos, nextLeftPos) => {
const brickHitted = Object.entries(bricksRef.current).filter(([key, value]) => {
... returning true under some conditions
}).map(([id]) => id)
setBricks(bricks => bricks.filter(b => !brickHitted.includes(b.id)))
}
//render while setting all divs into the refs object:
return (
<section className='bricks'>
{
bricks.map(b => {
return <div key={b.id} className='brick' ref={ref => bricksRef.current[b.id] = ref}>
{b.id}
</div>
})
}
</section>
)
at the filter, right before return delete the false from ref object
const brickHitted = Object.entries(bricksRef.current).filter(([key, value]) => {
if (false under some conditions) delete bricksRef.current[key] // if I understand correctly and b.id === key
... returning true under some conditions
}).map(([id]) => id)
more about delete https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete

Map object with URL to return object

I loop through this array like this:
{props.choosenMovie.characters.map((characters) => (
<p>{characters}</p> /* This displays the URL of course */
))}
These URL's include a name object which is what i want to display,
what is the best practice to do this?
This is how it is displayed on my application, but the desire is to display the name object from the URL's.
In useEffect, map thru your array of urls and make the api call and store the promises in an array. Use promise.all and update the state which will cause re-render.
In render method map thru the updated state and display the names.
see working demo
Code snippet
export default function App() {
const [char, setChar] = useState([
"https://swapi.dev/api/people/1/",
"https://swapi.dev/api/people/2/"
]);
const [people, setPeople] = useState([]);
useEffect(() => {
const promiseArray = [];
char.forEach(c => {
promiseArray.push(fetch(c).then(res => res.json()));
Promise.all(promiseArray).then(res => {
console.log("res", res);
setPeople(res);
});
});
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{people.map((p, i) => {
return <p key={i}>{p.name}</p>;
})}
</div>
);
}
I was working with that API some time ago, and the way I approached it (to display the names etc) was with Promise.all
so the snipped looked like
axios.get(`https://swapi.dev/api/${this.props.match.path.split('/')[1]}/${this.props.match.params.id}/`).then((res) => {
let characters = []
// get all characters in the movie data
let characterPromises = []
res.data.characters.forEach((character) => {
characterPromises.push(axios.get(character))
})
// Create list with all characters names and link to page
Promise.all(characterPromises).then((res) => {
res.forEach((character, i) => {
characters.push(<li key={i}><Link to={`/${character.data.url.split('api/')[1]}`}>{character.data.name}</Link></li>)
})
this.setState({
characters
})
})
})
}
then I just used the characters lists (from state) in the render method

Categories

Resources