React updating display of a page's H1 state with useState and useEffect hooks - reactjs

After reading the docs for useState and useEffect I cant figure out what i'm doing wrong here... Im trying to dynamically update my h1 title with an updated title when a tab is clicked, however the state will not update so my title wont rerender.
This is my subheader component which takes in an array of objects as props. These objects are iterated over and used to populate the subnav bar. (which works as intended).
const Subheader = (props) => {
const {
submenuItems = []
} = props;
// State
const [pageTitle, setPageTitle] = useState(submenuItems[0].name); //Sets starting value as the first item of my submenu which is also the default route so works as intended.
const handleMenuItemClick = (name) => {
setPageTitle(name)
console.log(name) //This prints out the updated expected value
console.log(pageTitle) //This prints out the original not updated value
}
const submenuItemsJSX = submenuItems.map((item, index) => {
return (
<li
key={index}
to={item.to}
onClick={() => handleMenuItemClick(item.name)}
>
<a>
{item.name}
</a>
</li>
)
});
useEffect(() => {
console.log(pageTitle) //This prints out the original not updated value
}, [pageTitle])
return (
<div>
<div>
<h1>
{pageTitle} //This is what i want to update
</h1>
</div>
<div>
<ul>
{submenuItemsJSX}
</ul>
</div>
</div>
)
}
export default Subheader
a sample of whats coming in through the subMenuItems:
{name: 'Categories', to: '/settings/categories', buttons: [] }

setSelectedMenuItem and setPageTitle are the asynchronous method, and you can't get the updated value of selected and pageTitle immediately after setSelectedMenuItem() and setPageTitle().
You should use useEffect to check the updated value with adding dependency.
useEffect(() => {
console.log(selected)
}, [selected])
useEffect(() => {
console.log(pageTitle)
}, [pageTitle])

Your code appears to be correct. The issue must be somewhere else.
I've made a codesandbox demo and everything works.

Have you tried to pass empty string '' to useState, when you declare the pageTitle? And for initialisation of the value you can use useEffect hook.
const [pageTitle, setPageTitle] = useState('');
useEffect(() => {
setPageTitle(submenuItems[0].name)
})

Related

How to avoid this message warning "Maximum update depth exceeded..." on NextJs

on NextJs i not understand, how useEffect work. What i need to do, to stop of receiving this warning message
"Maximum update depth exceeded":
The Code bellow is the page, that call a component ListContainer, this page add a item to container.
The page JSX:
import { useState } from "react";
import AppLayout from "../components/AppLayout";
import ListContainer from "../components/ListContainer";
export default function componentCreator(){
const [item,setItem] = useState([])
/* add item to container */
function addItem(){
let newItem = item
newItem.push({
produto: 'Skol 350ml',
preco: '1200,00',
quantidade: 'cx c/ 15 unidades'
})
setItem(newItem)
}
return (
<AppLayout>
<ListContainer items={item} setItems={setItem}/>
<div className="productcardbuttonshow" onClick={() => addItem()}>ADICIONAR</div>
</AppLayout>
)
}
Bellow the component that handle the items, remove or add. But it works, but on console trigger warning messages about update.
Component ListContainer.jsx:
import { useState,useEffect } from "react";
export default function ListContainer(props){
const [html,setHTML] = useState(null)
const [item,setItem] = useState(props.items)
/* refresh html container */
useEffect(() => {
const itemHTML = item.map((itemmap,id) => {
return (
<div id={id} onClick={() => delItem(id)} className="itemProposta">
{itemmap.produto} - {itemmap.quantidade} - R$ {itemmap.preco}
</div>
)
})
setHTML(itemHTML)
})
/* remove item from container */
function delItem(id){
let itemlist = props.items
itemlist.splice(id,1)
props.setItems(itemlist)
}
return (
<>
{html}
</>
)
}
You are getting into an infinite loops of renders. This code is responsible:
useEffect(() => {
const itemHTML = item.map((itemmap,id) => {
return (
<div id={id} onClick={() => delItem(id)} className="itemProposta">
{itemmap.produto} - {itemmap.quantidade} - R$ {itemmap.preco}
</div>
)
})
setHTML(itemHTML)
})
This callback inside useEffect will run after every render, because there is no dependency array. That means after every render, setHTML(itemHTML) is called. And even if the constituent objects of the array itemHTML are same, a new reference of the array is created. A new reference is created because .map() returns a new reference of the array. And although render and update works correctly, infinite rendering is happening.
Consider adding a dependency array to useEffect. For example:
useEffect(() => {
/* function body */
},[props.items]);
Now useEffect callback only runs if props.items reference changes.
Side note (unrelated to your question):
In the below code,
function addItem(){
let newItem = item
newItem.push({
produto: 'Skol 350ml',
preco: '1200,00',
quantidade: 'cx c/ 15 unidades'
})
setItem(newItem)
}
You should do let newItem = [...item], otherwise you are not creating a new reference of item array and setItem(newItem) is basically useless in that case.

I want only one component state to be true between multiple components

I am calling components as folloews
{userAddresses.map((useraddress, index) => {
return (
<div key={index}>
<Address useraddress={useraddress} />
</div>
);
})}
Their state:
const [showEditAddress, setShowEditAddress] = useState(false);
and this is how I am handling their states
const switchEditAddress = () => {
if (showEditAddress === false) {
setShowEditAddress(true);
} else {
setShowEditAddress(false);
}
};
Well, it's better if you want to toggle between true and false to use the state inside useEffect hook in react.
useEffect will render the component every time and will get into your condition to set the state true or false.
In your case, you can try the following:
useEffect(() => { if (showEditAddress === false) {
setShowEditAddress(true);
} else {
setShowEditAddress(false);
} }, [showEditAddress])
By using useEffect you will be able to reset the boolean as your condition.
Also find the link below to react more about useEffect.
https://reactjs.org/docs/hooks-effect.html
It would be best in my opinion to keep your point of truth in the parent component and you need to figure out what the point of truth should be. If you only want one component to be editing at a time then I would just identify the address you want to edit in the parent component and go from there. It would be best if you gave each address a unique id but you can use the index as well. You could do something like the following:
UserAddress Component
const UserAddress = ({index, editIndex, setEditIndex, userAddress}) => {
return(
<div>
{userAddress}
<button onClick={() => setEditIndex(index)}>Edit</button>
{editIndex === index && <div style={{color: 'green'}}>Your editing {userAddress}</div>}
</div>
)
}
Parent Component
const UserAddresses = () => {
const addresses = ['120 n 10th st', '650 s 41 st', '4456 Birch ave']
const [editIndex, setEditIndex] = useState(null)
return userAddresses.map((userAddress, index) => <UserAddress key={index} index={index} editIndex={editIndex} setEditIndex={setEditIndex} userAddress={userAddress}/>;
}
Since you didn't post the actual components I can only give you example components but this should give you an idea of how to achieve what you want.

react, map component, unexpected result

I am building Weather App, my idea is to save city name in database/localhost, place cities in useState(right now it's hard coded), iterate using map in first child component and display in second child component.
The problem is that 2nd child component outputs only one element (event though console.log prints both)
BTW when I change code in my editor and save, then another 'li' element appears
main component
const App = () => {
const [cities, setCities] = useState(['London', 'Berlin']);
return (
<div>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
export default App
first child component
const DisplayWeather = ({displayWeather}) => {
const [fetchData, setFetchData] = useState([]);
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData([...fetchData , data]);
})
}, [])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
export default DisplayWeather
second child component
const Weather = ({data}) => {
console.log(data) // it prints correctly both data
return (
<li>
{data.name} //display only one data
</li>
)
}
export default Weather
The Problem
The setFetchData hooks setter method is asynchronous by default, it doesn't give you the updated value of the state immediately after it is set.
When the weather result for the second city is returned and set to state, the current value fetchData at the time is still an empty array, so you're essentially spreading an empty array with the second weather result
Solution
Pass a callback to your setFetchData and get the current previous value of the state and then continue with your spread accordingly.
Like this 👇🏽
setFetchData((previousData) => [...previousData, data]);

My component is mutating its props when it shouldn't be

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.
propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

React Hook useEffect() run continuously although I pass the second params

I have problem with this code
If I pass the whole pagination object to the second parameters of useEffect() function, then fetchData() will call continuously. If I only pass pagination.current_page so It will call only one time, but when I set new pagination as you see in navigatePage() function, the useEffect() does not call to fetchData() although pagination has changed.
How to solve this. Thank you very much!
Besides I do not want the use useEffect() call when first time component mounted because the items is received from props (It is fetch by server, this is nextjs project).
import React, {useEffect, useState} from 'react';
import Filter from "../Filter/Filter";
import AdsListingItem from "../AdsListingItem/AdsListingItem";
import {Pagination} from "antd-mobile";
import styles from './AdsListing.module.css';
import axios from 'axios';
const locale = {
prevText: 'Trang trước',
nextText: 'Trang sau'
};
const AdsListing = ({items, meta}) => {
const [data, setData] = useState(items);
const [pagination, setPagination] = useState(meta);
const {last_page, current_page} = pagination;
const fetchData = async (params = {}) => {
axios.get('/ads', {...params})
.then(({data}) => {
setData(data.data);
setPagination(data.meta);
})
.catch(error => console.log(error))
};
useEffect( () => {
fetchData({page: pagination.current_page});
}, [pagination.current_page]);
const navigatePage = (pager) => {
const newPagination = pagination;
newPagination.current_page = pager;
setPagination(newPagination);
};
return (
<>
<Filter/>
<div className="row no-gutters">
<div className="col-md-8">
<div>
{data.map(item => (
<AdsListingItem key={item.id} item={item}/>
))}
</div>
<div className={styles.pagination__container}>
<Pagination onChange={navigatePage} total={last_page} current={current_page} locale={locale}/>
</div>
</div>
<div className="col-md-4" style={{padding: '15px'}}>
<img style={{width: '100%'}} src="https://tpc.googlesyndication.com/simgad/10559698493288182074"
alt="ads"/>
</div>
</div>
</>
)
};
export default AdsListing;
The issue is you aren't returning a new object reference. You save a reference to the last state object, mutate a property on it, and save it again.
const navigatePage = (pager) => {
const newPagination = pagination; // copy ref pointing to pagination
newPagination.current_page = pager; // mutate property on ref
setPagination(newPagination); // save ref still pointing to pagination
};
In this case the location in memory that is pagination remains static. You should instead copy all the pagination properties into a new object.
const navigatePage = (pager) => {
const newPagination = {...pagination}; // shallow copy into new object
newPagination.current_page = pager;
setPagination(newPagination); // save new object
};
To take it a step further you really should be doing functional updates in order to correctly queue up updates. This is in the case that setPagination is called multiple times during a single render cycle.
const navigatePage = (pager) => {
setPagination(prevPagination => {
const newPagination = {...prevPagination};
newPagination.current_page = pager;
});
};
In the case of pagination queueing updates may not be an issue (last current page set wins the next render battle), but if any state updates actually depend on a previous value then definitely use the functional update pattern,

Resources