Updating state being one step behind WITHOUT using useEffect? - reactjs

Note: I've seen people suggesting useEffect to this issue but I am not updating the state through useEffect here..
The problem I am having is that when a user selects id 7 for example, it triggers a function in App.tsx and filters the todo list data and update the state with the filtered list. But in the browser, it doesn't reflect the updated state immediately. It renders one step behind.
Here is a Demo
How do I fix this issue (without combining App.tsx and TodoSelect.tsx) ?
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [filteredTodoList, setFilteredTodoList] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
const filterTodos = () => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === selectedTodoUser
);
setFilteredTodoList(filteredTodos);
};
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
setFilteredTodoList(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos();
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{" "}
{filteredTodoList.map((todo) => (
<div>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))}
</div>
</div>
);
}
In TodoSelect.tsx
export default function TodoSelect({ onSelect }: TodoUsers) {
const users = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
return (
<div>
<span>User: </span>
<select
onChange={(e) => {
onSelect(e.target.value);
}}
>
{users.map((item) => (
<option value={item} key={item}>
{item}
</option>
))}
</select>
</div>
);
}

There's actually no need at all for the filteredTodoList since it is easily derived from the todoData state and the selectedTodoUser state. Derived state doesn't belong in state.
See Identify the Minimal but Complete Representation of UI State
Let’s go through each one and figure out which one is state. Ask three
questions about each piece of data:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
Filter the todoData inline when rendering state out to the UI. Don't forget to add a React key to the mapped todos. I'm assuming each todo object has an id property, but use any unique property in your data set.
Example:
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{filteredTodoList
.filter((todo) => todo.userId.toString() === selectedTodoUser)
.map((todo) => (
<div key={todo.id}>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))
}
</div>
</div>
);
}

State update doesn't happen synchronously! So, selectedTodoUser inside filterTodos function is not what you're expecting it to be because the state hasn't updated yet.
Make the following changes:
Pass todoUser to filterTodos:
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos(todoUser);
};
And then inside filterTodos compare using the passed argument and not with the state.
const filterTodos = (todoUser) => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === todoUser
);
setFilteredTodoList(filteredTodos);
};
You probably won't need the selectedTodoUser state anymore!

Related

Reactjs Sort By Price

I want to sort my product after I click the text "Sort by Log To High",
I put "product.sort((a,b) => a.productPrice > b.productPrice ? 1 : -1)" in a onClick function but it does not work. Now it works only if I put in the const displayProduct.
Any tutorial or video may I refer to? Thanks for helping.
export const Product = () =>{
const [product, setProduct] = useState([]);
const [pageNumber, setPageNumber] = useState(0)
const productPerPage = 12
const pagesVisited = pageNumber * productPerPage
const displayProduct = product
.slice(pagesVisited,pagesVisited + productPerPage)
.map(product => {
return(
<div className='imageContainer ' key={product.id}>
<img src={PopularOne} className="image"/>
<div className='productName'>
<Link style={{ textDecoration:'none' }} to="/productsDetails" state={{ product:product }}>{product.productName}</Link>
</div>
<div className='productPrice'>
<h3 >RM{product.productPrice}</h3>
</div>
</div>
)
})
//product.sort((a,b) => a.productPrice > b.productPrice ? 1 : -1)
const pageCount = Math.ceil(product.length/ productPerPage)
const changePage = ({selected}) =>{
setPageNumber(selected)
}
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(`/routes/getProduct`);
console.log(res)
setProduct(res.data);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
return(
<div className='product'>
<div>
<button><h3>Sort By Low to High</h3></button>
<h3>Sort By High to Low</h3>
</div>
<div className='productContainer'>
{displayProduct}
</div>
<ReactPaginate
previousLabel={""}
nextLabel={""}
breakLabel="..."
pageRangeDisplayed={5}
pageCount={pageCount}
onPageChange={changePage}
containerClassName={"pagination"}
breakClassName={"break"}
pageClassName={"page-item"} //li
pageLinkClassName={"page-link"} //a
activeLinkClassName={"page-link-active"}
/>
<Footer/>
</div>
)
}
When you use the useState function provided by React it returns 2 things, first is the state variable, and second is the updater function. In your case the state is product and the updater is setProduct.
It doesn't work because you are trying to modify the state variable, just use the updater function, and it will work.
For example:
setProduct(prevState => {
let newState = [...prevState];
newState.sort((a, b) => a.productPrice > b.productPrice ? 1 : -1);
return newState;
});
Updater function provides the previous state, in this case it's named prevState.
Shallow clone the array and store it in the newState
variable.
Mutate the newState array via the sort method.
Return the newState. By returning here we tell React to update the state to the value of newState.

React State and Events managed by Child Components

Child component is managing the state of parent objects using callback function. The code blow works well with just one variable but gives and error while dealing with Objects. The error I get is while entering values to the textarea..
remarks.map is not a function
Please help me out with this problem.
Also please do let me know if Ref here is of any use. Thank you.
return (
<div className="container">
{remarks?.map((items: any) => {
return (
<div key={items?.id}>
<label>
<textarea
name="remarkVal"
id={items?.id}
onChange={(e) => onSliderChangeHandler(e)}
value={items?.remarksVal}
ref={childRef}
placeholder={placeholder}
/>
</label>
</div>
);
})}
</div>
);
};
Getting a new row on edit the code as per answers.
setChildState((prevState: any) => [
...prevState,
{ [e.target.name]: e.target.value }
]);
Your state value is an array:
const [remarks, setRemarks] = useState( [{ id: 1, remarkVal: "hello man" }]);
When you update state here, you change it to an object:
setChildState((prevState: any) => ({
...prevState,
[e.target.name]: e.target.value
}));
As the error states, map() is not a function on objects. Keep your state value as an array. For example, you can append an element to it:
setChildState((prevState: any) => ([
...prevState,
e.target.value
]));
Or perhaps modify (replace) the single item within the array:
setChildState((prevState: any) => ([{
...prevState[0],
[e.target.name]: e.target.value
}]));
(Note: This assumes the array will always have exactly one item. Though you seem to be making that assumption anyway. And it's pretty strange to maintain an array which will only ever have one item.)
Or maybe the array will contain multiple items, and you want to update one specific one? Your weird use of .map() inside of onSliderChangeHandler may be trying to imply that. In that case you might want something like this:
const onSliderChangeHandler = (e: any) => {
setChildState((prevState: any) => ([
...prevState.map(p => {
if (p.id === e.target.id) {
return { ...p, [e.target.name]: e.target.value };
} else {
return p;
}
});
]));
};
Note how, instead of mapping over the array to update state to only one item, state is updated to the resulting array of the map operation, in which a target item is replaced (and all others returned as-is).
Basically, what you need to do is take a step back and examine/understand the data structure you are using. Should it be an object or an array of objects? Why? When an update is made, what should be changed? Why? Don't just make random changes to "get it to work", deliberately maintain the data you want to maintain.
By Konrad Linkowski
Link : https://codesandbox.io/s/relaxed-fog-8nfnhb?file=/src/App.js
export default function App() {
const [Form, setForm] = useState(Details);
const [remarks, setRemarks] = useState([{ id: 1, remarksVal: "hello man" }]);
const [parentState, setParentState] = useState(123);
// make wrapper function to give child
const wrapperSetParentState = useCallback(
(val) => {
setParentState(val);
},
[setParentState]
);
const setterRemarks = useCallback(
(val) => {
setRemarks(val);
},
[setRemarks]
);
useEffect(() => {
console.log("parent", remarks);
}, [remarks]);
return (
<div className="App">
<RemarksPage
remarks={remarks}
setterRemarks={setterRemarks}
placeholder="I am remarks section..."
/>
<div style={{ margin: 30 }}>
<Child
parentState={parentState}
parentStateSetter={wrapperSetParentState}
/>
<br />
{parentState}
</div>
</div>
);
}
import { Key, useEffect, useRef, useState } from "react";
export default ({ remarks, placeholder, setterRemarks, ...rest }) => {
const childRef = useRef();
const [childState, setChildState] = useState(remarks);
useEffect(() => {
setterRemarks(childState);
}, [setterRemarks, childState]);
const onSliderChangeHandler = (id: Key | null | undefined, value: string) => {
setChildState((remarks: any[]) =>
remarks.map((remark) =>
remark.id === id ? { ...remark, remarksVal: value } : remark
)
);
};
return (
<div className="container">
{remarks?.map(
(items: {
id: Key | null | undefined;
remarksVal: string | number | readonly string[] | undefined;
}) => {
return (
<div key={items?.id}>
<label>
<textarea
name="remarkVal"
id={items?.id}
onChange={(e) =>
onSliderChangeHandler(items.id, e.target.value)
}
value={items?.remarksVal}
ref={childRef}
placeholder={placeholder}
/>
</label>
</div>
);
}
)}
</div>
);
};

How can I filter through what is displayed with useEffect?

I am a bit lost on what to do for the next step. I have managed to display the content but I can't seem to get it to filter with a click. It was easy enough to do with a different api , i followed webdevsimplified but this i can't work out and I am at my wits end!
All I want is to filter through the mapped api. for example if I check 3, it should show me only 3 starRating. Can anybody offer me some advice please.
App.js
import { useEffect, useState, useRef } from 'react'
import Header from './components/Header';
import SearchBar from './components/SearchBar';
export default function App() {
const [hotelRooms, setHotelRooms] = useState([]);
const fetchHotels = async () => {
const res = await fetch('https://obmng.dbm.guestline.net/api/hotels?collection-id=OBMNG')
const hotels = await res.json()
const hotelRooms = []
for(const hotel of hotels) {
const res = await fetch(`https://obmng.dbm.guestline.net/api/roomRates/OBMNG/${hotel.id}`)
const info = await res.json()
hotelRooms.push({ hotel, rooms: info.rooms })
}
setHotelRooms(hotelRooms)
}
useEffect(() => {
fetchHotels()
}, [])
return (
<div className="App">
<Header/>
{
hotelRooms.map(h => (
<div>
<input value={"1"} type="checkbox" onChange={}/>
<input value={"Adults"}type="checkbox" onChange={}/>
<h2> Name: {h.hotel.name}</h2>
<p> Description: {h.hotel.description}</p>
<p> Rating: {h.hotel.starRating}</p>
<p> Postcode: {h.hotel.postcode}</p>
<p> City: {h.hotel.town}</p>
<img src={h.hotel.images}/>
<p style={{ fontWeight: 'bold' }}>Rooms:</p>
{
h.rooms.map(room => (
<div>
<h5>Occupancy</h5>
<div> Adults: {room.occupancy.maxAdults}</div>
<div> Children: {room.occupancy.maxChildren}</div>
<div> Maximum guests: {room.occupancy.maxOverall}</div>
<div> Room type: {room.name}</div>
<img src={room.images}/>
</div>
))
}
</div>
))
}
</div>
);
}
You should have a state that saves the filtered properties.
const [filter, setFilter] = useState({ ratings: ["1", "2", "3", "4", "5"] });
When you show the checkboxes add a name to them and the respective values.
Remember when you use .map in render, add an unique key to the out most tag.
<div>
{["1", "2", "3", "4", "5"].map((star) => (
<div key={"input-" + star}>
<input
id={"rated" + star}
value={star}
name="ratings"
type="checkbox"
checked={filter.ratings.includes(star)}
onChange={handleRatingFilter}
/>
<label htmlFor={"rated" + star}>Rated {star} star</label>
</div>
))}
</div>
Now in the onChange handler, update the state according to the checkboxes:
const handleRatingFilter = (e) => {
if (e.target.checked) {
// adding value
const temp = [...filter.ratings];
temp.push(e.target.value);
setFilter({ ...filter, ratings: temp });
} else {
// removing value
setFilter({
...filter,
ratings: [...filter.ratings.filter((v) => v !== e.target.value)]
});
}
};
Finally, when you use .map on hotelRooms you can filter the list before mapping it.
{hotelRooms
.filter((h) => filter.ratings.includes(h.hotel.starRating))
.map((h) => (
<div key={h.hotel.name}>
stuff
</div>
))
}
Working CodeSandbox
If I am understanding your question correctly, you want it to re-render after you update hotelRooms? If this is correct, when you first render it, the value is [], a blank array. And in here :
useEffect(() => {
fetchHotels()
}, [])
That last bit [], runs once after rendering. Therefore in your case (if my assumption is correct), you will want to change it to, as you want it to re-render each time hotelRooms value change
useEffect(() => {
fetchHotels()
}, [hotelRooms])

Useeffect not update after i select new item on Select option

I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value

React re-renders entire list of components even with unique keys

I am using the React useState hook to update a list of items. I would like for only the added/updated components to be rendered but everytime the state of of the list changes all the items in list are re-rendered.
I have followed Preventing list re-renders. Hooks version. to solve the re-render issue but it doesn't work
Can someone help me understand, what's wrong with the below code or if this is actually not the right way to do it
function App() {
const [arr, setArr] = useState([])
useEffect(() => {
//getList here returns a list of elements of the form {id: number, name: string}
setArr(getList());
}, [])
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, [arr])
return (
<div className="App">
{
arr.map((item) => {
return (
<NewComp key={`${item.id}`} item={item} clickHandle={clickHandle} />
);
})
}
</div>
);
}
const NewComp = ({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
}
The reason all your NewComp re-render is because your clickHandle function is being recreated whenever there is any change in the state arr.
This happens because you have added arr as a dependency to useCallback. This however is not required.
Once you fix it, you can wrap your NewComp with React.memo to optimize their re-renders. Also you must note that call the render function of a component is different from actually re-rendering it in the DOM.
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, []);
const NewComp = React.memo({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
});

Resources