on first button press state is not updating but function called - javascript

on first button press onChange method function is called but state is not updating as it should and on second button press it is updating see this
import React,{useState} from 'react';
function MainHeader(props) {
const [FirstName, setFirstName] = useState('')
const [User, setUser] = useState({
FirstName: '',
LastName: ''
})
const nameOnChange = (event) => {
setFirstName(event.target.value)
console.log(FirstName)
}
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
console.log(User)
props.addUserToFirebase(User)
}
return (
<div>
<h1>Checking</h1>
<input type="text" onChange={(e) => nameOnChange(e)} value={FirstName} />
<button onClick={() => addName()}>Enter</button>
</div>
);
}
on nameOnChange it is console.log(FirstName) when first time a pressed something then console logs empty state (initial state) and on second button it updates the previous button pressed. I have tried creating class component as well but i am seeing the same issue , same thing happens in the addName function it updates state on second click .
see console

I don't see a problem here. It is working perfectly as it should be. But the only problem I see is your wrong understanding of how React works or how Functional Programming works in general.
There's no mutation in Functional Programming
const nameOnChange = (event) => {
// event => new value
// FirstName => old value
// they remain that way throughout this function call
setFirstName(event.target.value)
// even if you set the state, the values won't change
// they will be updated only in next function call
console.log(FirstName) // still old value
}
The same goes for addName()
For each re-render React call the function MainHeader with values that will not be mutated throughout their call or life. When value are updated, React will call MainHeader with the updated the values.
Correct way of using your Component
Works, but not better way
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
//
console.log({
...User,
FirstName: FirstName
}) // new value, since User is not mutated, User will still have the old value
props.addUserToFirebase(({
...User,
FirstName: FirstName
})
}
Better way
Always use useEffects for side effects.
// Just set the state
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
}
// handle side effects here
useEffect(() => {
// check is needed here, since it will be
// called on component's first mount
if(User.FirstName !== ''){
console.log(User)
props.addUserToFirebase(User)
}
}, [User])
// This will be called whenever React detects a change in `User`

https://reactjs.org/docs/hooks-effect.html
You must use useEffect().
const nameOnChange = (event) => {
setFirstName(event.target.value)
}
useEffect(()=>{
if(FirstName !== ''){
console.log(FirstName)
}
}, [FirstName])
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
}
useEffect(()=>{
if(User.FirstName !== '' && User.LastName !== ''){
console.log(User)
props.addUserToFirebase(User)
}
}, [User])

Related

React state saving first value empty

I have the following setup in my react app:
const [modalReasonInput, setModalReasonInput] = useState("")
const validateReason = () => {
console.log(modalReasonInput)
if (modalReasonInput === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
}
const handleChange = (value) => {
setModalReasonInput(value)
validateReason()
}
<TextField
type="textarea"
maxLength={1000}
showCounting
rows={6}
value={modalReasonInput}
onBlur={() => validateReason()}
onChange={(e) => handleChange(e.target.value)}
/>
With the above code when I enter first character in the input field a blank/white space is set as value to state modalReasonInput.
The value is set only if I type another character, but value is set to first character I had typed.
Due to this my validation is failing while typing first character.
How can I update the state value as I type.
It is getting updated as you type but when you try to console.log the state right after you call setModalReasonInput you can't see it right away because state updates in React is asynchronous. You can check the state with useEffect as the state changes:
useEffect(() => {
console.log(modalReasonInput)
}, [modalReasonInput])
The problem is your handleChange function. If you set the state and perform a validation shortly after it is not guaranteed that the state is available in the next line.
const handleChange = (value) => {
setModalReasonInput(value)
validateReason() // value not guarenteed to be in modalReasonInput state
}
You can either separate state update and validation or directly pass in your value to your validation function validateReason():
const validateReason = (value) => {
console.log(value)
if (value === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
};
const handleChange = (value) => {
setModalReasonInput(value)
validateReason(value)
}
const [modalReasonInput, setModalReasonInput] = useState("")
const reasonRef = useRef("")
const validateReason = () => {
console.log(easonRef.current)
if (reasonRef.current === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
}
const handleChange = (value) => {
setModalReasonInput(value)
easonRef.current = value
validateReason()
}
Thanks everyone I ended up using useRef hook for this issue. I didn't use #sm3sher approach because validateReason is being called by other methods also which do not get event value.
I didn't use #Enes method useEffect because judgement_result is initially set to null.

How to immediately rerender child component after updating the sessionStorage using custom hook

My goal is to build a simple product review system using React, Next.JS and the browser's sessionStorage.
The user should be able to click on a button to "Add a review". This action will trigger the display of a text area and a submit button. Once the user click the submit button, the review content should be persisted in the sessionStorage and immediately showed up in a list of reviews.
My problem is that although I can update the sessionStorage after submitting the review, the app is not displaying the list of existing reviews right away.
If I leave the page and get back, the reviews will be shown up, meaning my custom hook seems to be working fine.
Here's the ReviewForm.tsx code:
export const ReviewForm: React.FC<Props> = ({ productId }): JSX.Element => {
const [showForm, setShowForm] = useState<boolean>(false);
const [storedValues, setStoredValues] = useSessionStorage<SessionStorage[]>(
"products-reviews",
[]
);
const registerReview = (event: any) => {
event.preventDefault();
const reviewText = event.target.review.value;
const productIndex = storedValues?.findIndex(
(review) => review.productId === productId
);
if (productIndex === -1 || productIndex === undefined) {
setStoredValues([...storedValues!, { productId, reviews: [reviewText] }]);
} else {
const reviews = [...storedValues![productIndex].reviews, reviewText];
const updatedReviews = [...storedValues!];
updatedReviews[productIndex].reviews = reviews;
setStoredValues(updatedReviews);
}
setShowForm(false);
};
return (
<div className={styles.reviewsContainer}>
<button
className={styles.addReviewButton}
onClick={() => setShowForm(true)}
>
<span>Add a review</span>
</button>
{showForm && (
<form
className={styles.reviewForm}
onSubmit={(event) => registerReview(event)}
>
<textarea className={styles.reviewInput} name="review" required />
<button className={styles.reviewSubmitButton} type="submit">
Submit
</button>
</form>
)}
<ReviewList productId={productId} />
</div>
);
};
And here's the ReviewList.tsx component, rendered inside ReviewForm.tsx:
export const ReviewList: React.FC<Props> = ({ productId }): JSX.Element => {
const [reviews, _] = useSessionStorage<SessionStorage[]>(
"products-reviews",
[]
);
const productReviews = reviews?.find(
(review) => review.productId === productId
)?.reviews;
return (
<ul>
{productReviews?.map((review) => (
<li key={Math.random() * 10000}>{review}</li>
))}
</ul>
);
};
Lastly, here's my custom hook useSessionStorage:
export const useSessionStorage = <T>(
key: string,
initialValue?: T
): SessionStorage<T> => {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
if (!initialValue) return;
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
if (storedValue) {
try {
sessionStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.log(error);
}
}
}, [storedValue, key]);
return [storedValue, setStoredValue];
};
The title of my question says "how to rerender child component" because I noticed if I completely delete the ReviewList.tsx component, bringing all its render logic inside the ReviewForm.tsx, my application will behave as expected.
So maybe the problem is related with this relation between components?
Any advice is welcome.
The problem
The problem is in your useSessionStorage hook. It is not actually synchronized with the session storage, because the state is actually stored with useState, it is only populated on mount.
How does it work in your case:
You initialize FIRST STATE using useState (inside custom useSessionStorage hook) with current session storage value on component mount at ReviewList.tsx
You initialize SECOND STATE using useState (inside custom useSessionStorage hook) with current session storage value on component mount at ReviewForm.tsx
You mutate SECOND STATE and push the changes to session storage with useEffect
So FIRST STATE is not updated with the new value until you re-mount the component.
Solution 1 (Will work only for sync between different browser tabs)
We need to reverse the flow of data from useState -> sessionStorage to sessionStorage -> useState
export const useSessionStorage = <T>(
key: string,
initialValue?: T
): SessionStorage<T> => {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
if (!initialValue) return;
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value!) : initialValue;
} catch (error) {
return initialValue;
}
});
const setStorageValue = useCallback((newValue: T) => {
try {
sessionStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.log(error);
}
}, []);
/** This `useEffect` will make sure `storedValue` is always in sync with the `sessionStorage` */
useEffect(() => {
const listenToStorageEvent = (event: StorageEvent) => {
if (event.storageArea === sessionStorage && event.key === key) {
try {
const newValue = JSON.parse(event.newValue!);
if (storedValue !== newValue) {
setStoredValue(newValue);
}
} catch (error) {
console.log(error);
}
}
};
window.addEventListener("storage", listenToStorageEvent);
return () => {
window.removeEventListener("storage", listenToStorageEvent);
};
}, [key]);
// We expose `setStorageValue` which works with `sessionStorage` instead of `setStoredValue` which works with local state
return [storedValue, setStorageValue];
};
Solution 2
Use custom events to be able to sync the same tab too
https://github.com/imbhargav5/rooks/blob/main/src/hooks/useSessionstorageState.ts
Solution 3
Parse the whole session storage on application start and put it as a state into a context. After that, on each "set" update both the context state and the sessionStorage. This solution has a lot of disadvantages like error proneness due to manual state to session storage synchronization, excessive re-rendering of the whole component tree under session storage provider on each storage value update. So I will not even add code examples here.

How do I update state but not trigger the infinite useEffect loop?

I'm trying to update state in a higher-order component from a child. My solution is to set state to true after setting tags. useEffect runs when state is true, firstly updating state in the parent component and then updating state to false, which halts its invocation. My aforementioned solution is the only way I've managed to prevent useEffect's infinite loop.
const Student = ({
appendTags,
student: {
id: studentId,
firstName,
lastName,
pic,
email,
company,
skill,
grades,
tags: studentTags
}}) => {
const fullName = `${firstName} ${lastName}`;
const [tag, setTag] = useState('');
const [tags, setTags] = useState([]);
const [stateUpdated, setStateUpdated] = useState(false);
const [displayGrades, setDisplayGrades] = useState(false);
const onTagsSubmit = e => {
if (tag.length) {
e.preventDefault();
setTags(prevState => [...prevState, tag]);
setStateUpdated(true);
setTag('');
}
}
useEffect(() => {
if (stateUpdated) {
appendTags(studentId, tags);
setStateUpdated(false);
};
}, [stateUpdated, setStateUpdated, tags, appendTags, studentId]);
Looks like this is what we have, if you remove stateUpdated.
I presume than on appendTags() call the parent component changes its state and gets re-rendered. After that the appendTags function is recreated. The child component Student is recreated, too. Student's useEffect sees that one of the dependencies, appendTags, has changed, so it has to be re-executed. It calls the appendTags() and we have a loop.
To fix it, you need to wrap appendTags into useCallback hook inside the parent component:
const appendTags = useCallback((id, tags) => {
// update local state
}, []);
// ...
return <Student appendTags={appendTags} /* (...) */ />

Websocket event receiving old redux state in React app

I am building a Chat application using Reactjs and Redux. I have 2 components called ChatHeads and ChatBox which get mounted side-by-side at the same time.
In the ChatHeads component, the selection of User (to whom you want to chat with) is possible and this selection is stored in the redux store as chatInfo.
ChatHeads Component:
function ChatHeads(props) {
const {
dispatch,
userInfo,
userId
} = props;
const [chatHeads, setChatHeads] = useState([]);
const handleChatHeadSelect = (chatHead, newChat = false) => {
dispatch(
chatActions.selectChat({
isNewChat: newChat,
chatId: chatHead.chat._id,
chatUser: chatHead.user
})
);
};
const loadChatHeads = async () => {
const response = await services.getRecentChats(userId, userInfo);
setChatHeads(response.chats);
};
useEffect(() => loadChatHeads(), [userInfo]);
return (
// LOOPING THOUGH ChatHeads AND RENDERING EACH ITEM
// ON SELECT OF AN ITEM, handleChatHeadSelect WILL BE CALLED
);
}
export default connect(
(state) => {
return {
userInfo: state.userInfo,
userId: (state.userInfo && state.userInfo.user && state.userInfo.user._id) || null,
selectedChat: (state.chatInfo && state.chatInfo.chat && state.chatInfo.chat._id) || null
};
},
null,
)(ChatHeads);
Chat Actions & Reducers:
const initialState = {
isNewChat: false,
chatId: '',
chatUser: {},
};
const chatReducer = (state = initialState, action) => {
let newState;
switch (action.type) {
case actions.CHAT_SELECT:
newState = { ...action.payload };
break;
default:
newState = state;
break;
}
return newState;
};
export const selectChat = (payload) => ({
type: actions.CHAT_SELECT,
payload,
});
In the ChatBox component, I am establishing a socket connection to the server and based on chatInfo object from the global store & ws events, I perform some operations.
ChatBox Component:
let socket;
function ChatBox(props) {
const { chatInfo } = props;
const onWSMessageEvent = (event) => {
console.log('onWSMessageEvent => chatInfo', chatInfo);
// handling event
};
useEffect(() => {
socket = services.establishSocketConnection(userId);
socket.addEventListener('message', onWSMessageEvent);
return () => {
socket.close();
};
}, []);
return (
// IF selectedChatId
// THEN RENDER CHAT
// ELSE
// BLANK SCREEN
);
}
export default connect((state) => {
return {
chatInfo: state.chatInfo
};
}, null)(ChatBox);
Steps:
After both the components are rendered, I am selecting a user in the ChatHeads components.
Using the Redux DevTools, I was able to observe that the chatInfo object has been populated properly.
chatInfo: {
isNewChat: false,
chatId: '603326f141ee33ee7cac02f4',
chatUser: {
_id: '602a9e589abf272613f36925',
email: 'user2#mail.com',
firstName: 'user',
lastName: '2',
createdOn: '2021-02-15T16:16:24.100Z',
updatedOn: '2021-02-15T16:16:24.100Z'
}
}
Now, whenever the 'message' event gets triggered in the ChatBox component, my expectation is that the chatInfo property should have the latest values. But, I am always getting the initialState instead of the updated ones.
chatInfo: {
isNewChat: false,
chatId: '',
chatUser: {}
}
What am I missing here? Please suggest...
The reason for this behaviour is that when you declare your callback
const { chatInfo } = props;
const onWSMessageEvent = (event) => {
console.log('onWSMessageEvent => chatInfo', chatInfo);
// handling event
};
it remembers what chatInfo is right at this moment of declaration (which is the initial render). It doesn't matter to the callback that the value is updated inside the store and inside the component render scope, what matters is the callback scope and what chatInfo is referring to when you declare the callback.
If you want to create a callback that can always read the latest state/props, you can instead keep the chatInfo inside a mutable reference.
const { chatInfo } = props;
// 1. create the ref, set the initial value
const chatInfoRef = useRef(chatInfo);
// 2. update the current ref value when your prop is updated
useEffect(() => chatInfoRef.current = chatInfo, [chatInfo]);
// 3. define your callback that can now access the current prop value
const onWSMessageEvent = (event) => {
console.log('onWSMessageEvent => chatInfo', chatInfoRef.current);
};
You can check this codesandbox to see the difference between using ref and using the prop directly.
You can consult the docs about stale props and useRef docs
Broadly speaking, the issue is that you're trying to manage a global subscription (socket connection) inside a much more narrow-scope component.
Another solution without useRef would look like
useEffect(() => {
socket = services.establishSocketConnection(userId);
socket.addEventListener('message', (message) => handleMessage(message, chatInfo));
return () => {
socket.close();
};
}, [chatInfo]);
In this case the message event handler is passed the necessary information through arguments, and the useEffect hook re-runs every time we get a new chatInfo.
However, this probably doesn't align with your goals unless you want to open a separate socket for each chat and close the socket every time you switch to a different chat.
Thus, the "proper" solution would entail moving the socket interaction up in your project. One hint is that you are using userId to open the socket, which means that it's supposed to run once you know your userId, not once the user selects a chat.
To move the interaction up, you could store incoming messages in a redux store and pass the messages to the ChatBox component through props. Or you could create connect to the socket in ChatHeads component and pass the messages down to the ChatBox. Something like
function ChatHeads(props) {
const {
dispatch,
userInfo,
userId
} = props;
const [chatHeads, setChatHeads] = useState([]);
const loadChatHeads = async () => {
const response = await services.getRecentChats(userId, userInfo);
setChatHeads(response.chats);
};
useEffect(() => loadChatHeads(), [userInfo]);
const [messages, setMessages] = useState([]);
useEffect(() => {
socket = services.establishSocketConnection(userId);
socket.addEventListener('message', (msg) => setMessages(messages.concat(msg)));
}, [userId]);
return () => socket.close();
}
return (
// render your current chat and pass the messages as props
)
Or you could create a reducer and dispatch a chatActions.newMessage event and then the messages get to the current chat using redux.
The main point is that if you need chatInfo to open the socket, then every time chatInfo changes, you might have to open a new socket, so it makes sense to add the dependency to the useEffect hook. If it only depends on userId, then move it up to where you get the userId and connect to the socket there.

Child Stateless Functional Component doesn't update even after parent State updates

My parent component has a property in its state called formIsValid, initially set to false. My form also has a submit button. I want the submit button to be disabled until after some input fields (a first name and a last name input) have data in them.
This is what my state looks like:
state = {
employees: [],
costEstimates: emptyCosts(),
relationshipOptions: [],
newEmployee: emptyEmployee(),
formIsValid: false
};
This function handles changes to the First and Last name inputs:
// handle input into "First Name" and "Last Name" inputs
handleChangeValue = async e => {
const newEmployee = { ...this.state.newEmployee };
newEmployee[e.currentTarget.name] = e.currentTarget.value;
this.setState({ newEmployee });
this.validateIfCanBeSubmitted();
await this.updateCostsData(newEmployee); // this is an api thing, not relevent
};
This is what sets the formIsValid property in the state. This property is sent as a prop to the Submit button.
validateIfCanBeSubmitted = () => {
const { firstName, lastName } = this.state.newEmployee;
let formIsValid = firstName && lastName ? true : false;
this.setState({ formIsValid });
};
The Submit button for this is correctly getting disabled if the employee property in the state has its first and last names as empty. The problem is that it's "off by 1 update." It's as if the props aren't getting propagated down to the child button component until after the NEXT time the state changes. Here's a gif of the issue:
This is what the child component looks like. It's just a regular HTML button, however it's within a Stateless Functional Component, so the issue is not with the component's state:
<button
type="button"
onClick={onSubmit}
className={'btn btn-primary mr-1 ' + (formIsValid ? '' : 'disabled')}
disabled={!formIsValid}
>
setState() is asynchronous!
this.validateIfCanBeSubmitted(); is executed on the old state; this update this.setState({ newEmployee }); has not been propagated to this.state when your function is executed.
Make validateIfCanBeSubmitted an update-function.
validateIfCanBeSubmitted = ({ newEmployee: { firstName, lastName }}) => {
return {
formIsValid: firstName && lastName ? true : false
};
}
and use it accordingly:
handleChangeValue = async e => {
const {name, value} = e.currentTarget;
const newEmployee = {
...this.state.newEmployee,
[name]: value
};
this.setState({ newEmployee });
this.setState(this.validateIfCanBeSubmitted);
// this is an api thing, not relevant
await this.updateCostsData(newEmployee);
};
Actually, the code in handleChangeValue should also be in such a function, as it uses the previous state to compute the new one.
so how about combining them:
handleChangeValue = e => {
const {name, value} = e.currentTarget;
this.setState((state) => {
const newEmployee = {
...this.state.newEmployee,
[name]: value
};
const { firstName, lastName } = newEmployee;
const formIsValid = firstName && lastName ? true : false;
//and since you never use the returned Promise, why make anything async?
this.updateCostsData(newEmployee);
return { newEmployee, formIsValid };
});
};

Categories

Resources