useEffect: not rendering after update of state - javascript

I have a list of items (I get the items with a GET-call - I didn't add it here, because I think it's irrelevant). When I delete an item, the list should be updated/ re-rendered.
To do this I use the useEffect-hook with a second parameter (productData).
Problem:
I have to refresh the page manually in order to see the new list of items. I don't understand why: I use useEffect with the second parameter. Could someone point me in the right direction what is wrong? Thanks a lot!
Here is my code:
export default function MemberSavedProducts() {
const [productData, setProductData] = useState([]);
const [successMessage, setSuccessMessage] = useState();
const [errorMessage, setErrorMessage] = useState();
useEffect(() => {}, [productData]);
const deleteProduct = async(prod) => {
try {
if (window.confirm("Are you sure you want to delete this product?")) {
const {
data
} = await fetchContext.authAxios.delete(
`savedProducts/${prod}`
);
setProductData(
productData.filter((prod) => prod !== data.deletedItem.Id)
);
setSuccessMessage(data.message);
}
} catch (err) {
const {
data
} = err.response;
setErrorMessage(data.message);
}
};
return (
<CardLarge>
<div className={styles.productWrapper}>
{successMessage && <SuccessMessage text={successMessage} />}
{errorMessage && <ErrorMessage text={errorMessage} />}
{productData.map((prod) => {
return (
<div
key={prod.id}
>
<ProductItem
prod={prod}
onClick={() => {
getInfo(prod.information);
}}
/>
<button
onClick={() => {deleteProduct(prod.Id)}}
>
Delete
</button>
</div>
);
})}
</div>
</CardLarge>
);
}

Discussed this in the comments, posting this answer for completeness.
Are you sure the filter function works? It seems the refresh works because the GET response returns the right array. I think it should be productData.filter((prod) => prod.Id !== data.deletedItem.Id));. Because in your code you are comparing an object to a string.
Or you can use the passed parameter prod instead of the response maybe like this productData.filter((p) => p.Id !== prod));
Also a small clarification: useEffect does not cause a rerender, changing state does trigger a rerender. useEffect is just a listener/callback that triggers on change of the declared dependencies.

Related

Is there any diference between using ref callback as inline function or as a stable function with useCallback?

I have a component which take a children as props and render it inside a div which has a callback ref.
const ShowMoreText = (props: ShowLessOrMoreProps) => {
const { children } = props;
const [showMore, setShowMore] = useState<boolean>(false);
const [showButton, setShowButton] = useState<boolean>(false);
const [childrenHeight, setChildrenHeight] = useState<number>(9.5);
const measureRef = useCallback((node: HTMLDivElement) => {
if (node) {
setChildrenHeight(node.getBoundingClientRect().height);
}
}, []);
useEffect(() => {
console.log(childrenHeight)// when use measureRef function it gives me 0 when using inline version this gives me correct height and I don't know why
if (childrenHeight > 45) {
setShowButton(true);
}
}, [childrenHeight]);
return (
<Stack
sx={{
alignItems: "flex-start",
}}
>
<Collapse in={showMore} collapsedSize={45}>
<div
// ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
ref={() => measureRef} // if I comment this and use the above commmented version everything works fine
>
{children}
</div>
</Collapse>
{showButton && ( //when using measureRef function this button won't display
<Button onClick={() => setShowMore((prev) => !prev)}>
{showMore ? "Show less" : "Show more"}
</Button>
)}
</Stack>
);
};
the problem is that when I use stable measureRef function the console log inside useeffect prints 0 but in inline ref version everythings works fine. can anyone explain me why ?
ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
You return a function but ref shoulb be an Object (of certain type)
Hence You probably return void function.
if You want to pass (node: HTMLDivElement) => { // setChildrenHeight(node?.getBoundingClientRect().height); // } use unreserved prop then.
props.ref is reserved for useRef() hook .
Finally :
If for some reason Yuu want to save such contruction, do thi that way:
(node: HTMLDivElement) => {
return setChildrenHeight(node?.getBoundingClientRect().height);
}}
Where setChildrenHeight() returns correct object type proper po ref.
I found the solution and share it here in case anyone is curious or have a same problem.
The problem was that the children that I passed to this component was something like this:
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
and because of the delay of request the div height was actually 0 at the beginning. so the actual code for callback ref worked correct and the problem raised because of the api call delay. the solution for me and the mistake that i had been made was not to render the above code conditionally like this:
{dataIsLoading &&
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
}
hopefully this can help someone else.

map() shows new messages from state only if I type something into input

Every time I send a message, map() only shows my new message when I type something in the input (re-render).
The listener works fine, it displays new messages in the console immediately after sending a new message, curiously, the messages state also updates when I look in React Developers Tools,
But useEffect[messages] does not trigger on a new message, and it only displays new messages after I type something in the input.
Here is my component with comments:
import { useState, useEffect, useRef } from "react";
const ChatWindowChannel = ({ window, chat }) => {
const [messages, setMessages] = useState([]);
const [message, setMessage] = useState("");
const loaded = useRef(null);
const messagesEndRef = useRef(null);
const channelMessagesHandler = async () => {
const channelMessagesListener = await chat.loadMessagesOfChannel(window);
channelMessagesListener.on((msgs) => {
console.log(msgs); // Here shows new messages after I click `send`
// Works correct.
setMessages(msgs); // In React Developers Tools the state `messages` are update if I click `send`
// Works correct.
console.log(messages); // Shows always empty array[]
// Dont works correct
});
};
async function send() {
await chat.sendMessageToChannel(window, message, {
action: "join",
alias: chat.gun.user().alias,
pubKey: chat.gun.user().is.pub,
name: "grizzly.crypto",
});
setMessage("");
}
useEffect(() => {
// Shows only once. It shold every time on new message
console.log("Messages changed");
}, [messages]);
useEffect(() => {
loaded.current !== window.key && channelMessagesHandler();
loaded.current = window.key;
}, [window]);
return (
<div>
<div className="ChatWindow">
<h2>
Channel {window.name} - {window.isPrivate ? "Private" : "Public"}
</h2>
<div>
<details style={{ float: "right" }}>
<summary>Actions</summary>
<button
onClick={async () => {
await chat.leaveChannel(window);
}}
>
Leave channel
</button>
</details>
</div>
<div className="msgs">
{messages.map((message, key) => (
<div key={`${key}-messages`}>
<small>{message.time}</small>{" "}
<small>{message.peerInfo.alias}</small> <p>{message.msg}</p>
<div ref={messagesEndRef} />
</div>
))}
</div>
<div>
<div>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") send();
}}
/>
</div>
<div>
<button onClick={send}>Send</button>
</div>
</div>
</div>
</div>
);
};
export default ChatWindowChannel;
Note 1: setMessages(msgs); do not change the value of messages immediately, it will be set only on next render so console.log(messages) just after setting will give you incorrect results.
Note 2: In case the reference of the array msgs that you are trying to set to a state variable is not changed even if array is modified - re-render will not be executed and useEffects will not be triggered, reference to an array needs to be new.
setMessages([...msgs]);
Same happens also with {objects}.
Can you try something like this:
setMessages([...msgs]);
instead of setMessages(msgs);
State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions enqueue changes to the component state, but React may delay the changes, updating several components in a single pass. In that case you can't view (console.log) immediately after using state setter function.

How to save a component state after re-rendering? React js

There are some movie cards that clients can click on them and their color changes to gray with a blur effect, meaning that the movie is selected.
At the same time, the movie id is transferred to an array list. In the search bar, you can search for your favorite movie but the thing is after you type something in the input area the movie cards that were gray loses their style (I suppose because they are deleted and rendered again based on my code) but the array part works well and they are still in the array list.
How can I preserve their style?
Search Page:
export default function Index(data) {
const info = data.data.body.result;
const [selectedList, setSelectedList] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<main className={parentstyle.main_container}>
<NavBar />
<div className={style.searchbar_container}>
<CustomSearch
onChange={(e) => {
setSearchTerm(e.target.value);
}}
/>
</div>
<div className={style.card_container}>
{info
.filter((value) => {
if (searchTerm === '') {
return value;
} else if (
value.name
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase())
) {
return value;
}
})
.map((value, key) => {
return (
<MovieCard
movieName={value.name}
key={key}
movieId={value._id}
selected={selectedList}
setSelected={setSelectedList}
isSelected={false}
/>
);
})}
</div>
<div>
<h3 className={style.test}>{selectedList}</h3>
</div>
</main>
Movie Cards Component:
export default function Index({ selected, movieName, movieId, setSelected }) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
useEffect(()=>{
})
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
console.log(e.target);
}
setSelected([...selected]);
toggleClass();
};
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
}
I can't directly test your code so I will assume that this is the issue:
Don't directly transform a state (splice/push) - always create a clone or something.
Make the setActive based on the list and not dependent. (this is the real issue why the style gets removed)
try this:
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
// filter out the id
setSelected(selected.filter(s => s !== e.target.id));
return;
}
// add the id
setSelected([...selected, e.target.id]);
};
// you may use useMemo here. up to you.
const isActive = selected.includes(movieId);
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
This is a very broad topic. The best thing you can do is look up "React state management".
As with everything in the react ecosystem it can be handled by various different libraries.
But as of the latest versions of React, you can first start by checking out the built-in tools:
Check out the state lifecycle: https://reactjs.org/docs/state-and-lifecycle.html
(I see in your example you are using useState hooks, but I am adding these for more structured explanation for whoever needs it)
Then you might want to look at state-related hooks such as useState: https://reactjs.org/docs/hooks-state.html
useEffect (to go with useState):
https://reactjs.org/docs/hooks-effect.html
And useContext:
https://reactjs.org/docs/hooks-reference.html#usecontext
And for things outside of the built-in toolset, there are many popular state management libraries that also work with React with the most popular being: Redux, React-query, Mobx, Recoil, Flux, Hook-state. Please keep in mind that what you should use is dependant on your use case and needs. These can also help you out to persist your state not only between re-renders but also between refreshes of your app. More and more libraries pop up every day.
This is an ok article with a bit more info:
https://dev.to/workshub/state-management-battle-in-react-2021-hooks-redux-and-recoil-2am0#:~:text=State%20management%20is%20simply%20a,you%20can%20read%20and%20write.&text=When%20a%20user%20performs%20an,occur%20in%20the%20component's%20state.

React usestate array length varies depending on item i click

im trying to build a list you can add and delete components from.
Adding works but when i try to delete an item, every item that comes after that also gets deleted.
I found that the length of the use state array i use varies depending on which item i click delete on.
const Alerting = () => {
const [Alerts, setAlert] = useState([]);
const AddAlertingChoice = () => {
const addedAlerts = Alerts => [...Alerts, <AlertingCoinChoice coins={coins} id={new Date().getUTCMilliseconds()}];
setAlert(addedAlerts);
}
const DeleteAlertingChoice = id =>{
console.log("alerts "+Alerts.length) //This length always displays the item index-1?
const removedArr = [...Alerts].filter(alert => alert.props.id != id)
setAlert(removedArr)
}
return (
<>
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((item, i) => (
item
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
</>
)
The items
const AlertingCoinChoice = ({coins, id, DeleteAlertingChoice}) => {
return (
<>
<AlertingCoin>
<CoinSelect id={'SelectCoin'}>
<OptionCoin value='' disabled selected>Select your option</OptionCoin>
</CoinSelect>
<ThresholdInput id={'LowerThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<ThresholdInput id={'UpperThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<SaveButton id={'AlertSaveAndEdit'} onClick={ClickSaveButton}>Save</SaveButton>
<DeleteAlertButton onClick={() => {DeleteAlertingChoice(id)}}>X</DeleteAlertButton>
</AlertingCoin>
</>
)
why cant it just delete the item i pass with the id parameter?
It sounds like you're only passing down the DeleteAlertingChoice when initially putting the new <AlertingCoinChoice into state, so when it gets called, the length is the length it was when that component was created, and not the length the current state is.
This also causes the problem that the DeleteAlertingChoice that a component closes over will only have the Alerts it closes over from the time when that one component was created - it won't have the data from further alerts.
These problems are all caused by one thing: the fact that you put the components into state. Don't put components into state, instead transform state into components only when rendering.
const Alerting = () => {
const [alerts, setAlerts] = useState([]);
const AddAlertingChoice = () => {
setAlerts([
...alerts,
{ coins, id: Date.now() }
]);
}
const DeleteAlertingChoice = id => {
console.log("alerts " + alerts.length);
setAlerts(alerts.filter(alert => alert.id !== id));
}
return (
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((alertData) => (
<AlertingCoinChoice {...alertData} DeleteAlertingChoice={DeleteAlertingChoice} />
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
);
};
You also don't need fragments <> </> when you're already only rendering a single top-level JSX item.

React props renders are step late

I have the following which is changing the values, but it's always one step behind. For example, when you click on the "paid: false" for a customer, it changes to true but the app doesn't rerender and you have to update another thing on the app in order to see the change. Is there a simple way to fix this in React? I don't know how to research what I'm looking for so a point in the right direction will help a lot.
const [receipt, setReceipt] = useState(receiptData);
// const [currentReceipt, setCurrentReceipt] = useState({});
// For some reason I do not know yet, everything is working but this and onSubmitFromApp are one step behind.
const handlePaid = (index) => {
for (let receiptPaid in receiptData) {
if (receiptPaid === index) {
receiptPaid.paid = !receiptPaid.paid;
console.log(receiptPaid);
}
}
setReceipt(receiptData);
}
Link to full code: https://codesandbox.io/s/korilla-receipts-starter-forked-01xz0?file=/src/App.js:206-675
Your approach is kind of weird and involves mutations. You're better off doing something like this (I removed the form stuff cos that's unrelated):
// App.js
export default function App() {
const [receipts, setReceipts] = useState(receiptData);
// Map over the current state, not the imported array
const handlePaid = (id) => {
setReceipts(
receipts.map((receipt) =>
receipt.id === id ? { ...receipt, paid: !receipt.paid } : receipt
)
);
};
return (
<div className="App">
<Receipts receiptsArr={receipts} handlePaid={handlePaid} />
</div>
);
}
// Receipts.js
const Receipts = (props) => {
const receiptMap = props.receiptsArr.map((receipt, index) => {
return (
<Receipt
...
handlePaid={() => props.handlePaid(receipt.id)}
/>
);
});
return <div className="container">{receiptMap}</div>;
};
// Receipt.js
const Receipt = (props) => {
return (
...
<span onClick={props.handlePaid} >Paid: {props.paid ? 'true' : 'false'}</span>
</div>
)
}
You can check out the sandbox here

Categories

Resources