State is always outdated inside the promise resolve method - javascript

So I'm running into a weird issue with my SPA regarding states.
I have a left side menu with items, let's say organization and when I click on any of them, the right hand panel changes with the appropriate list of users within the selected organization.
For example: I have organization 1 and organization 2 in the list, and if I click on organization 1, I send a request to middleware to retrieve the list of users within that organization and if I select organization 2, I do that same thing.
So I have a higher component Organization.js that has the following code:-
// Organization.js
const [selectedOrganization, setSelectedOrganization] = useState(null);
// This method will be called when we select an organization from the menu
const handleSelectedOrganization = organization => {
if (!selectedOrganization || selectedOrganization.id !== organization.id) {
setSelectedOrganization(organization);
}
};
return (
<UsersView selectedOrganization={selectedOrganization} />
);
UsersView.js
const UsersView = ({ selectedOrganization = {} }) => {
const [selectedOrganizationUsers, setSelectedOrganizationUsers] = useState(
[]);
let globalOrganization = selectedOrganization?.id; // global var
const refreshOrganizationsList = () => {
const localOrganization = selectedOrganization.id; // local var
Promise.all([ // bunch of requests here]).then(data => {
console.log('from global var', globalOrganization); // outdated
console.log('from local var', localOrganization); // outdated
console.log('from prop', selectedOrganization.id); // outdated
setSelectedOrganizationUsers(data.result); // data.result is an array of objects
});
};
// Will be called when the selectedOrganization prop changes, basically when I select
//a new organization from the menu, the higher component will
// change state that will reflect here since the prop will change.
useEffect(() => {
if (selectedOrganization) {
globalOrganization = selectedOrganization.id;
refreshOrganizationsList();
}
}, [selectedOrganization]);
console.log(selectedOrganization?.id); // Always updated *1
return (
{selectedOrganizationUsers?.length ? (
<>
<div className="headers">
....
)
Now the problem is, some API calls take too long to respond, and in a particular scenario when I switch between orgs fast, we would get some pending API calls and when the response comes, the states are messed up.
For example: If I select from the menu Organization 1, we send 3 requests to middleware that would remain pending let's say for 10 seconds.
If after 5 seconds, I choose Organization 2 from the menu where its API requests would be instant, the right hand panel will be updated with the Organization 2 data but then after 5 seconds, when Organization 1 requests get the responses, the list gets updated with Organization 1 data which is what I try to prevent since now we have selected organization 2.
The reason why I have console.logs in the .then() is because I try to block updating the states when the current selectedOrganization !== the organization.id in the response.
But unfortunately, the console.logs in the above scenario would should me the organization id = 1 and not 2, even if I have selected organization 2 already.
For example:
I select Organization 1, then I selected Organization 2
once I select Organization 2, the outside *1 console.log would log 2 immediately in my browser.
But when I get the API responses of 1, the console.logs inside the .then() gives me 1 not 2, I expect them to give me 2 so that I can make an if (request.organization_id !== selectedOrganization.id) -> don't update the states
Long story short, it seems that when the API call returns with a result, the organization.id within the .then() is always the one was had when we fired the request itself and not the most updated part. As if it's no longer tied with the recent value within the props that comes from the state of the higher component

Use the functional updater function to compare to the latest state
So a possible solution is exactly related to what I linked in the comment, though it might not be that obvious at first.
First, you'll need to slightly change the state structure. If this gets too complicated over time, you might want to take a look at useReducer, the context API, or a full-fledged state management library like Redux or similar.
Then, use the functional updater function to compare to the latest state values, which might have changed if the selected organization has since changed.
const UsersView = ({ selectedOrganization }) => {
// Slightly change the state structure.
const [{ users }, setState] = useState({
currentOrgId: selectedOrganization?.id,
users: [],
});
const refreshOrganizationsList = (orgId) => {
// Set the currentOrgId in the state so we remember which org was the last fetch for.
setState((state) => ({ ...state, currentOrgId: orgId }));
Promise.all([
/* bunch of requests here */
]).then((data) => {
setSelectedOrganizationUsers((state) => {
// Is the current org still the one we started the fetch for?
if (state.currentOrgId !== orgId) {
// Precondition failed? Early return without updating the state.
return state;
}
// Happy path, update the state.
return {
...state,
users: data.result,
};
});
});
};
useEffect(() => {
if (selectedOrganization) {
// instead of a component scoped variable, just pass the id as a param.
refreshOrganizationsList(selectedOrganization.id);
}
}, [selectedOrganization]);
return (/* JSX here */);
};
There's no longer any needs for local variables. In fact, local variables get captured by the closure and even if the component is rendered again, the values won't change inside of the old refreshOrganizationsList function reference (which gets recreated each render cycle).

Related

How to avoiding repeating work (or to keep common/shared state) in nested hooks?

In a nested hook: how could one know if it already was invoked in the current component (instance) and access any previously computed/saved values?
Preferably without the Component author/the hook user having to know about this and not having to do anything special for it to work.
Example
An example to illustrate the problem:
const useNestedHook = () => {
// Some heavy work with the same result for each component instance.
// Is it possible to remember the result of this work when
// this hook is used again in the same component instance?
// So I would like to save a state which all uses of useNestedHook
// could access as long as they are in the same component instance.
}
const useHookA = () => {
useNestedHook();
};
const useHookB = () => {
useNestedHook();
};
const Component = () => {
useHookA();
// Would like to avoid useNestedHook repeating its work since it's
// used in the same component and would have this same result (per definition)
// Preferably without the Component author having to know anything about this.
useHookB();
};
Imagined solution
Something like a "named" shared state, which would give access to the same shared state (in the same component instance) no matter which hook it's used in. With each component instance having its own separate state as usual. Maybe something like:
const [state, setState] = useSharedState("stateId", initialValue);
No, that is not possible. Each useState() call will always be separate from other useState() calls.
The component can not use the hooks as in your example, but the component author doesn't necessarily have to care for the implementation details.
A solution would depend on the use case.
Some details:
One state is defined by where the useState() call is written in the code (see explanation), which is not directly related to the instance. I.e. two useState() calls and two instances are 4 state values.
You can use shared state e.g. using context, but then the state would also be shared by all instances, not only the hooks (which you don't want).
So the useNestedHook() will always be "separate", but if you can use shared state, and you only care for "caching", and can accept that the useNestedHook() is called twice (i.e. skip the expensive operation if the result is the same), then you can use useEffect(). I.e. the call would depend on the value, not the instance and not the hook.
Some examples:
1. One hook with options
E.g. if your hooks A and B would optionally calculate two different values, which need the same useNestedHook() value, you could create one hook with options instead, e.g.:
const useHookAB = ({ A, B }) => {
const expensiveValue = useNestedHook();
if( A ){ /* do what useHookA() was doing */ }
if( B ){ /* do what useHookB() was doing */ }
};
const Component = () => {
useHookAB({ A: true, B: true });
};
I can't imagine another reason right now why you would want to call the hooks like that.
2. The "normal" way
The obvious solution would be:
const useHookA = ( value ) => {
// ...
};
const useHookB = ( value ) => {
// ...
};
const Component = () => {
const value = useNestedHook();
useHookA( value );
useHookB( value );
};
But I can imagine reasons why you can't (or don't like to) do it that way.

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.

I want to access my state variable from one component to other

I have a react query which writes the state variable- follower, and I want to access this variable in other component to find its .length can someone tell me how do I do it
const ModalFollower = ({profile}) => {
const [follower,setFollower] = useState([])
const {
data: followerName,
isLoading: followerLoading,
isFetching: followerFetching
} = useQuery(["invitations", profile?.id], () => {
getFollowers(profile?.id).then((response) => {
if (response) {
setFollower(response);
}
});
});
return(
{
!followerLoading && (
follower.map((e) => {
return(<>
<p>{e.requested_profile.Userlink}</p>
</>}
)
}
)
I want to access the length of follower in some other component
There is no need to copy data from react-query to local state, because react-query is a full-blown state manager for server state. As long as you use the same query key, you will get data from its cache. This is best abstracted away in custom hooks.
Please be aware that with the default values, you will get a "background refetch" if a new component mount, so you will see two network requests if you use it twice. That might look confusing at first, but it is intended, as it is not react-query's primary goal to reduce network requests, but to keep your data on the screen as up-to-date as possible. So when a new component mounts that uses a query, you'll get the stale data from the cache immediately, and then a background refetch will be done. This procedure is called stale-while-revalidate.
The best way to customize this behaviour is to set the staleTime property to tell react-query how long your resource is "valid". For that time, you will only get data from the cache if available. I've written about this topic in my blog here: React Query as a State Manager.
React Query also provides selectors, so if your second component is only interested in the length, this is what my code would look like:
const useInvitations = (profile, select) =>
useQuery(
["invitations", profile?.id],
() => getFollowers(profile?.id),
{
enabled: !!profile?.id
select
}
)
Note that I also added the enabled property because apparently, profile can be undefined and you likely wouldn't want to start fetching without that id.
Now we can call this in our main component:
const ModalFollower = ({profile}) => {
const { data } = useInvitations(profile)
}
and data will contain the result once the promise resolves.
In another component where we only want the length, we can do:
const { data } = useInvitations(profile, invitations => invitations.length)
and data will be of type number and you will only be subscribed to length changes. This works similar to redux selectors.

How can I stop extra rerenders in my component when I rely on a usePrevious?

So my googling did not return any useful results, so sorry if I ask a duplicate question.
So I am listening to my redux-store in react with 2 selectors. 1 to get the oldest timestamp we have, and another to get all messages in the store.
// returns an array
const messages = useSelector(
(store) => selectMessages(store, chatId),
shallowEqual
);
const [showGetMore, setShowGetMore] = useState(true);
// returns a firebase timestamp
const oldestMessage= useSelector((store) =>
selectOldestMessage(store, chatId)
);
// uses the react hook usePrevious code
const previousOldestTimestamp = usePrevious(
// if oldestMessage is undefined still, set it as null,
// so the getMore doesn't disappear
oldestMessage || null
);
useEffect(() => {
if (previousOldestTimestamp === oldestMessage) {
// if timestamps are equal, there are no more old messages to get
setShowGetMore(false);
}
}, [
previousOldestTimestamp,
oldestMessage,
setShowGetMore,
]);
So when I click to get more messages, i make a call that calls firebase for more messages, then it sends 2 actions, 1 to update the messages object, and another to update the oldest message field.
However, using a props/state watcher, my selectOldestMessages sometimes rerenders with the same number of objects in the array, even though I am using shallowEquals to prevent that, and the useEffect is always running and declaring that the timestamps are the same, when in the store they are always updating correctly.
So I assume it's because the 2 actions I dispatch are triggering the component to re render 2 times which causes the usePrevious hook to run 2 times which breaks my code because it should only being re rendered 1 time.
But if I am dead wrong, I would love to know.
Does anyone have a better solution for this, or see a bad react coding pattern?

How can I fetch backend data in child component and then retrieve an object from store?

My parent component <Room/> build children components <RoomSensor/>, when parent build these children I also send to the <RoomSensor/> uuid, by this uuid I fetch sensor data from a backend.
Store is array of objects.
// Parent <Room/>
return props.sensors.map((uuid, index) => {
return <RoomSensor key={index} uuid={uuid}/>
})
// Children <RoomSensor/>
const RoomSensor = props => {
useEffect(() => {
console.log("useEffect")
props.fetchSensor(props.uuid)
}, [props.uuid])
console.log(props.sensor)
return (
<div className={"col-auto"}>
<small><b>{props.sensor.value}</b></small>
</div>
)
}
let mapStateToProps = (state, props) => {
return {
sensor: filterSensor(state, props.uuid)
}
}
let mapDispatchToProps = {
fetchSensor,
}
export default connect(mapStateToProps, mapDispatchToProps)(RoomSensor)
// Selectors
export const getSensor = (state, uuid) => {
return _.filter(state.sensors, ["uuid", uuid])[0]
}
export const filterSensor = createSelector(
getSensor,
(sensor) => sensor
)
And I cannot understand two things:
When I do refresh I get.
TypeError: Cannot read property 'uuid' of undefined
I understand that there is no data in the state yet, that's why such an error occurs. Is it possible not to render the component until the data comes from the server?
If I comment <small><b>{props.sensor.value}</b></small> no errors occur, data appears in the store, then I uncomment this line and voila everything works. But in the console I see too many component rerende. What am I doing wrong? Is there something wrong with the selector?
In general, I want each sensor component to render independently of the others.
The following is based on a few assumptions derived from the shared code and output:
Currently, there's a hard-coded list of 4 sensor UUIDs.
createSelector is from the reselect package.
_ references an import of the lodash package.
"Is it possible not to render the component until the data comes from the server?"
The short answer to this is yes. There're several approaches to achieve this, so you'll want to evaluate what fits best with the structure of the app. Here're 2 approaches for consideration:
Retrieve the list of sensors from the server. Initialize the store with an empty list and populate it upon getting data back from the server.
In getSensor, return a default value if the uuid isn't in the list.
Either way, I'd recommend adding default state to the store. This will help reduce the amount of code required to handle edge cases.
Here's a rough example of what the new reducer and selector for (1) might look like:
export const storeReducer = (state, action) => {
let nextState = state;
if (!state) {
// State is uninitialized, so give it a default value
nextState = {
sensors: [],
};
}
switch (action.type) {
case 'RECEIVE_SENSORS':
// We received sensor data, so update the value in the store
nextState = {
...nextState,
sensors: action.sensors,
};
break;
default:
break;
}
return nextState;
};
export const getSensors(state) {
return state.sensors;
}
The action upon receiving the data from the server, could look something like:
dispatch({
sensors,
type: 'RECEIVE_SENSORS',
})
"...in the console I see too many component rerende[rs]"
Without additional context, it's hard to say exactly why the re-renders are happening, but the most likely cause is that each call to props.fetchSensor(props.uuid) changes the data in the store (e.g. if it's overwriting the data).
From the console output you shared, we see that there're 16 re-renders, which would happen because:
Each of the 4 instances of RoomSensor calls fetchSensor
This results in 4 updates to the store's state
Each update to the store's state causes React to evaluate each instance of RoomSensor for re-render
Hence, 4 state updates x 4 components evaluated = 16 re-renders
React is pretty efficient and if your component returns the same value as the previous run, it knows not to update the DOM. So, the performance impact probably isn't actually that significant.
That said, if the above theory is correct and you want to reduce the number of re-renders, one way to do it would be to check whether the data you get back from the server is the same as what's already in the store and, if so, skip updating the store.
For example, fetchSensor might be updated with something like:
const existingData = getSensor(getState(), uuid);
const newData = fetch(...);
// Only update the store if there's new data or there's a change
if (!existingData || !_.isEqual(newData, existingData)) {
dispatch(...);
}
This would require updating getSensor to return a falsey value (e.g. null) if the uuid isn't found in the list of sensors.
One additional tip
In Room, RoomSensor is rendered with its key based on the item's index in the array. Since uuid should be unique, you can use that as the key instead (i.e. <RoomSensor key={uuid} uuid={uuid} />). This would allow React to base updates to RoomSensor on just the uuid instead of also considering the order of the list.

Categories

Resources