setState not mutating while calling a function multiple times - javascript

I am using PubNub SDK for Implementing Chat with React components..
I am stuck in refreshing the state due to async nature of setting state.. below is some code to illustrate how this works.
const handleNewChannelCreation = async (newChannelId: string) => {
const newChannelMetadata = (await pubnub.objects.getChannelMetadata({ channel: newChannelId })).data
setCurrentChannelMetadata(newChannelMetadata ? newChannelMetadata : dummyChannelMetadata)
if (channelMetadata) {
const filteredChannels = channelMetadata.filter(isChannel)
const isNewChannel = filteredChannels.findIndex((channel) => channel.id === newChannelMetadata.id)
// the api can return an existing channel therefore we need to check if the channel already exists
if (isNewChannel === -1) {
console.log("handleNewChannelCreation__", filteredChannels.length, "newChannel", newChannelMetadata.name)
setNewChannels(newChannels.concat(newChannelMetadata)) // this is not ideal I know and this dont work as this function is called multiple times.
}
}
setIsModalOpen(false)
}

If you are enqueueing state updates in a loop or multiples within a single render cycle then you'll typically want to use functional state updates so you update from the previous state and not some state value closed over in callback scope. In your case you are appending a new value to an array from the previous state, so there's a dependency here on the previous state.
Functional State Updates
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
setNewChannels(newChannels => newChannels.concat(newChannelMetadata))

Related

How can I update a state variable from a promise?

I am trying to determine if a customer has an active subscription or not. To do this I am utilizing the following code:
const stripe = require('stripe')('some-api-key');
export default function Example(){
// the user will automatically be considered non-subbed by default
const [isSubscriber, setIsSubscriber] = useState(false)
// grab the customer id from stripe
async function get_customer_id() {
const customers = await stripe.customers.search({
query: `metadata[\'some-meta-data-key\']:\'some-meta-data-value\'`
});
return customers.data[0]['id']
}
// grab the list of active subscriptions from stripe
async function customer_is_subscriber(){
const subs = await stripe.subscriptions.list({
status: 'active',
});
return subs
}
// determine if the customer id is in the list of active subscriptions.
// return true if so, false otherwise
async function test_equality(){
const customer_id = await get_customer_id();
const subbed = await customer_is_subscriber();
const answer = subbed.find(sub => sub.customer === customer_id)
return !!answer;
}
useEffect( () => {
async function load_result() {
const promise_function_return = await test_equality()
setIsSubscriber(promise_function_return)
}
load_result();
}, [isSubscriber]);
return (
// some react code
)
}
I have been able to successfully get all of my other functions where I am doing the comparisons for if a user is a subscriber but where I am having an issue is updating the state value (e.g. true if they are subbed, false otherwise).
I found some good past questions on this specific topic such as:
here The useState set method is not reflecting a change immediately
here: setState inside Promise in React
and here: setState inside a Promise function in a useEffect with hooks?
but I just have not been able to get it working correctly. This is currently the closest I have been able to get to solving this problem.
Currently your code says that, when isSubscriber changes, it should check if the user is a subscriber and update the isSubscriber state... so it's a chicken and egg problem. It won't set isSubscriber until isSubscriber gets set.
I think you want to change }, [isSubscriber]); to }, []); so that that code executes when the component first loads (not when isSubscriber changes).
The useEffect hook will always run on mount regardless of if there is anything in its dependency array. This means that your useEffect will work as is, and will run onMount as well as when isSubscriber changes:
useEffect( () => {
async function load_result() {
const promise_function_return = await test_equality()
setIsSubscriber(promise_function_return)
}
load_result();
}, [isSubscriber]);
To verify this, you can check out this codesandbox example. The useEffect looks just like yours, and you will notice that isSubscriber is initially set to false, but is updated to true after 3 seconds.
There's still an adjustment you may want to make even though that part appears to work ok. With isSubscriber in the dependency array, the function in your useEffect will be called any time isSubscriber changes. This probably not what you want, since this function doesn't actually depend on isSubscriber, but actually sets isSubscriber. In this case, that means test_equality() will be run on initial mount and then one more time after it sets isSubscriber, which is unnecessary.
This blog post explains the useEffect dependency array really well.
You can fix this by removing isSubscriber from the dependency array, like this:
useEffect(() => {
console.log("in useEffect");
async function load_result() {
const promise_function_return = await test_equality();
setIsSubscriber(promise_function_return);
}
load_result();
}, [isSubscriber]);
Since you mentioned the state value is not getting updated, there must be another issue going on in either get_customer_id() or customer_is_subscriber(). It would be good to double check and make sure the stripe api calls are working as expected.

React Hooks: Adding new fields to an Object state does not get reflected immediately

I am using React Hooks to manage states within a component.
const addNode = () => {
let pform = pForm
let handles = [vForm, yForm, hForm]
let access_info = [virtualForm, management1Form, management2Form, consoleForm]
let newObj = {
...currentForm,
p: pform,
handles: handles,
access_info: access_info,
}
console.log('newObj', newObj)
setCurrentForm(
newRouterObj
)
console.log(currentForm)
let currArr = [...addedNodes]
currArr.push(currentForm)
setAddedNodes(currArr)
intializeForms()
}
The function above is an onClick that I use when I press an Add button. The forms (pForm, vForm, yForm, etc.) are all separate states. I gather them together and put them into a single object newObj and use setCurrentForm to update the currentForm state to newObj.
When I console.log the newObj, everything goes in fine. However, when I check the currentForm after the setCurrentForm, the fields (p, handles, and access_info) are empty.
I know that states in React can have a delay in updates so I might have to use useEffect. However, in my use case, which is to gather different states and put them in as a new field in the currentForm state seems useEffect is not the best way to solve it. Can anyone help please?
You are misunderstanding exactly how useState works. When you call the useState setter function, the state value isn't actually updated immediately, instead it will trigger the component to re-render with the updated value. Even though you call the setter half way through the function, the state value will remain the original value for the entire lifetime of that function call.
You could slightly tweak what you have to be
const addNode = () => {
...
let currArr = [...addedNodes]
// you know that currentForm is supposed to be newObj, so just push that
// see my explanation above to understand why it currentForm isn't what you expect
currArr.push(newObj)
...
}
It's an async action so values will not be assigned/updated instantly. You need to watch for the changes using useEffect hook to log new values and to do anything in case
useEffect(() => {
// Whenever `currentForm` will be updated, this callback will be invoked
console.log('updated currentForm values', currentForm);
},[currentForm]);

Real world usage of setState with an updater callback instead of passing an object in React JS

The React documentation says the following about setState:
If you need to set the state based on the previous state, read about the updater argument below,
Beside the following sentence, which I don't understand:
If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
They say:
The first argument is an updater function with the signature (state, props) => stateChange ... state is a reference to the component state at the time the change is being applied.
And make an example:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
Saying:
Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with state.
What do they mean by guaranteed to be up-to-date and what should we be aware of when deciding if we should use setState with an updater function (state, props) => stateChange or directly with an object as the first parameter?
Let's assume a real world scenario. Suppose we have a fancy chat application where:
The state of the chat is represented by this.state = { messages: [] };
Previous messages are loaded making an AJAX request and are prepended to the messages currently in the state;
If other users (not the current user) send a message to the current user, new messages arrive to current user from a realtime WebSocket connection and are appended to the messages currently in the state;
If it is the current user the one who sends the message, the message is appended to the messages of the state as in point 3 as soon as the AJAX request fired when the message is sent completes;
Let's pretend this is our FancyChat component:
import React from 'react'
export default class FancyChat extends React.Component {
constructor(props) {
super(props)
this.state = {
messages: []
}
this.API_URL = 'http://...'
this.handleLoadPreviousChatMessages = this.handleLoadPreviousChatMessages.bind(this)
this.handleNewMessageFromOtherUser = this.handleNewMessageFromOtherUser.bind(this)
this.handleNewMessageFromCurrentUser = this.handleNewMessageFromCurrentUser.bind(this)
}
componentDidMount() {
// Assume this is a valid WebSocket connection which lets you add hooks:
this.webSocket = new FancyChatWebSocketConnection()
this.webSocket.addHook('newMessageFromOtherUsers', this.handleNewMessageFromOtherUser)
}
handleLoadPreviousChatMessages() {
// Assume `AJAX` lets you do AJAX requests to a server.
AJAX(this.API_URL, {
action: 'loadPreviousChatMessages',
// Load a previous chunk of messages below the oldest message
// which the client currently has or (`null`, initially) load the last chunk of messages.
below_id: (this.state.messages && this.state.messages[0].id) || null
}).then(json => {
// Need to prepend messages to messages here.
const messages = json.messages
// Should we directly use an updater object:
this.setState({
messages: messages.concat(this.state.messages)
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: messages.concat(state.messages)
.sort(this.sortByTimestampComparator)
}
})
// What if while the user is loading the previous messages, it also receives a new message
// from the WebSocket channel?
})
}
handleNewMessageFromOtherUser(data) {
// `message` comes from other user thanks to the WebSocket connection.
const { message } = data
// Need to append message to messages here.
// Should we directly use an updater object:
this.setState({
messages: this.state.messages.concat([message])
// Assume `sentTimestamp` is a centralized Unix timestamp computed on the server.
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: state.messages.concat([message])
.sort(this.sortByTimestampComparator)
}
})
}
handleNewMessageFromCurrentUser(messageToSend) {
AJAX(this.API_URL, {
action: 'newMessageFromCurrentUser',
message: messageToSend
}).then(json => {
// Need to append message to messages here (message has the server timestamp).
const message = json.message
// Should we directly use an updater object:
this.setState({
messages: this.state.messages.concat([message])
.sort(this.sortByTimestampComparator)
})
// Or an updater callback like below cause (though I do not understand it fully)
// "Both state and props received by the updater function are guaranteed to be up-to-date."?
this.setState((state, props) => {
return {
messages: state.messages.concat([message])
.sort(this.sortByTimestampComparator)
}
})
// What if while the current user is sending a message it also receives a new one from other users?
})
}
sortByTimestampComparator(messageA, messageB) {
return messageA.sentTimestamp - messageB.sentTimestamp
}
render() {
const {
messages
} = this.state
// Here, `messages` are somehow rendered together with an input field for the current user,
// as well as the above event handlers are passed further down to the respective components.
return (
<div>
{/* ... */}
</div>
)
}
}
With so many asynchronous operations, how can I be really sure that this.state.messages will always be consistent with the data on the server and how would I use setState for each case? What considerations I should make? Should I always use the updater function of setState (why?) or is safe to directly pass an object as the updater parameter (why?)?
Thank you for the attention!
setState is only concerned with component state consistency, not server/client consistency. So setState makes no guarantees that the component state is consistent with anything else.
The reason an updater function is provided, is because state updates are sometimes delayed, and don't occur immediately when setState is called. Therefore, without the updater function, you have essentially a race condition. For example:
your component begins with state = {counter: 0}
you have a button that updates the counter when clicked in the following way: this.setState({counter: this.state.counter +1})
the user clicks the button really fast, so that the state does not have time to be updated between clicks.
that means that the counter will only increase by one, instead of the expected 2 - assuming that counter was originally 0, both times the button is clicked, the call ends up being this.setState({counter: 0+1}), setting the state to 1 both times.
An updater function fixes this, because the updates are applied in order:
your component begins with state = {counter: 0}
you have a button that updates the counter when clicked in the following way: this.setState((currentState, props) => ({counter: currentState.counter + 1}))
the user clicks the button really fast, so that the state does not have time to be updated between clicks.
unlike the other way, currentState.counter + 1 does not get evaluated immediately
the first updater function is called with the initial state {counter: 0}, and sets the state to {counter: 0+1}
the second updater function is called with the state {counter: 1}, and sets the state to {counter: 1+1}
Generally speaking, the updater function is the less error-prone way to change the state, and there is rarely a reason to not use it (although if you are setting a static value, you don't strictly need it).
What you care about, however, is that updates to the state don't cause improper data (duplicates and the like). In that case, I would take care that the updates are designed so that they are idempotent and work no matter the current state of the data. For instance, instead of using an array to keep the collection of messages, use a map instead, and store each message by key or hash that is unique to that message, no matter where it came from (a millisecond timestamp may be unique enough). Then, when you get the same data from two locations, it will not cause duplicates.
I'm not an expert in React by any means and have only been doing it for two months only, but here's what I learned from my very first project in React, which was as simple as showing a random quote.
If you need to use the updated state right after you use setState, always use the updater function. Let me give you an example.
//
handleClick = () => {
//get a random color
const newColor = this.selectRandomColor();
//Set the state to this new color
this.setState({color:newColor});
//Change the background or some elements color to this new Color
this.changeBackgroundColor();
}
I did this and what happened was that the color that was set to the body was always the previous color and not the current color in the state, because as you know, the setState is batched. It happens when React thinks it's best to execute it. It's not executed immediately. So to solve this problem, all I have to do was pass this.changeColor as a second argument to setState. Because that ensured that the color I applied was kept up to date with the current state.
So to answer your question, in your case, since you're job is to display the message to the user as soon as a new message arrives, i.e use the UPDATED STATE, always use the updater function and not the object.

Issues with asynchronous nature of redux in React?

I'm pulling data into one of my parent components and then using various filter statements which are based on user choices from select boxes. I'm then calling an action which simply stores that filtered data based on the users search into global state so that my child components can access them.
One of my child components is supposed to render the results but what is happening is the results being rendered are lagging one action behind. I've encountered similar issues when using set state and my solution then was to use a callback but I'm not exactly sure how to go about dealing with this issue in this situation with redux.
The wordpress.get is just named import of axios config.
componentDidMount = async () => {
const response = await wordpress.get(
"*********************/api/wp/v2/variants?per_page=100"
);
this.props.fetchData(response);
const data = []
response.data.forEach(ele => {
data.push(ele)
})
this.props.sendFilteredView(data);
};
handleChange = () => {
this.preBuiltFiltering();
};
I've left out pre-built filtering because its long and excessive, all it does is run the filter based on the users choices and then dispatches the this.props.sendFilteredView action with the filtered data set as the argument. The action just returns the payload.
I then am rendering the results of the filter in a child component by accessing the global state (I also tried just passing it directly through props, same issue).
It’s an async function, you’re using a callback after the forEach with data.
So you need to wait forEach been completed.
Try to use await before forEach.
componentDidMount = async () => {
const response = await wordpress.get(
"*********************/api/wp/v2/variants?per_page=100"
);
this.props.fetchData(response);
const data = []
await response.data.forEach(ele => {
data.push(ele)
})
this.props.sendFilteredView(data);
};
handleChange = () => {
this.preBuiltFiltering();
};

React: scrollIntoView only works inside of setTimeout

My application consists of a basic input where the user types a message. The message is then appended to the bottom of all of the other messages, much like a chat. When I add a new chat message to the array of messages I also want to scroll down to that message.
Each html element has a dynamically created ref based on its index in the loop which prints them out. The code that adds a new message attempts to scroll to the latest message after it has been added.
This code only works if it is placed within a setTimeout function. I cannot understand why this should be.
Code which creates the comments from their array
comments = this.state.item.notes.map((comment, i) => (
<div key={i} ref={i}>
<div className="comment-text">{comment.text}</div>
</div>
));
Button which adds a new comment
<input type="text" value={this.state.textInput} onChange={this.commentChange} />
<div className="submit-button" onClick={() => this.addComment()}>Submit</div>
Add Comment function
addComment = () => {
const value = this.state.textInput;
const comment = {
text: value,
ts: new Date(),
user: 'Test User',
};
const newComments = [...this.state.item.notes, comment];
const newState = { ...this.state.item, notes: newComments };
this.setState({ item: newState });
this.setState({ textInput: '' });
setTimeout(() => {
this.scrollToLatest();
}, 100);
}
scrollToLatest = () => {
const commentIndex = this.state.xrayModalData.notes.length - 1;
this.refs[commentIndex].scrollIntoView({ block: 'end', behavior: 'smooth' });
};
If I do not put the call to scrollToLatest() inside of a setTimeout, it does not work. It doesn't generate errors, it simply does nothing. My thought was that it was trying to run before the state was set fully, but I've tried adding a callback to the setState function to run it, and it also does not work.
Adding a new comment and ref will require another render in the component update lifecycle, and you're attempting to access the ref before it has been rendered (which the setTimeout resolved, kind of). You should endeavor to use the React component lifecycle methods. Try calling your scrollToLatest inside the lifecycle method componentDidUpdate, which is called after the render has been executed.
And while you're certainly correct that setting state is an asynchronous process, the updating lifecycle methods (for example, shouldComponentUpdate, render, and componentDidUpdate) are not initiated until after a state update, and your setState callback may be called before the component is actually updated by render. The React docs can provide some additional clarification on the component lifecycles.
Finally, so that your scroll method is not called on every update (just on the updates that matter), you can implement another lifecycle method, getSnapshotBeforeUpdate, which allows you to compare your previous state and current state, and pass a return value to componentDidUpdate.
getSnapshotBeforeUpdate(prevProps, prevState) {
// If relevant portion or prevState and state not equal return a
// value, else return null
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Check value of snapshot. If null, do not call your scroll
// function
}

Categories

Resources