Issues with asynchronous nature of redux in React? - javascript

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();
};

Related

is a good approach defining 2 useEffect Hooks, see my use of case bellow

I tried to find the same case, but everyone is in a different situation.
I've created 2 useEffects in my component, one for initializing the component (1 execution) and another to fetch data when some state (filter) changes. (many executions)
const [filter1, setFilter1] = useState({});
const [filter2, setFilter2] = useState({});
//initialize filters, only once
useEffect(() => {
//here i fetch all the data to fill the filter components.
setFilter1(data1);
setFilter2(data2);
}, []);
//everytime my filters change.
useEffect(() => {
//here i fetch data using the filter as parameters.
}, [filter1,filter2]);
The code above is my last approach and works fine. Every time I change the filters the useEffect fetch the data and load the table with that data.
I show below the old approach that I took, and the one I didn't like.
const loadData = useCallback((filter1,filter2) => {
//here i fetch data using the filter as parameters.
}, [filter1,filter2]);
useEffect(() => {
//here i fetch all the data to fill the filter components.
setFilter1(data1);
setFilter2(data2);
loadData(filter1, filter2);
},[]);
and I used the loadData function in every filter change handler like:
const onFilter1Change = (val) => {
setFilter1(val);
loadData(val, filter2)
};
The old approach above brought me some issues regarding how to use the hooks properly and that's why I find myself asking you React experts, how to improve my coding in the best way.
The new approach works fine for me, tho I wonder if it's right to use 2 useEffect in this case, or if there is a better way to do it.
Cheers
Where data1 & data2 comes from?
If it's a prop, and you are using a useEffect only to initialize them, you don't need a useEffect for that:
const [filter1, setFilter1] = useState(data1);
const [filter2, setFilter2] = useState(data2);
useEffect(() => {
//here i fetch data using the filter as parameters.
}, [filter1, filter2]);

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.

setState not mutating while calling a function multiple times

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))

React - Populating First Select Option with Redux State

I have component League.js where there are 4 boxes containing League team details.
The team details are coming from this API => https://www.api-football.com/demo/api/v2/teams/league/${id}
When I click on each of the league boxes I am populating the Dropdown in my component Details.js
I won't get the team_id of the first team in the dropdown anytime I click on each of the League boxes in League.js in order to have the right calculation in a component called Stat.js. To do this I make a call request to this endpoint => https://www.api-football.com/demo/api/v2/statistics/${league}/${team}
So I need to pass league_id and team_id as a parameter, I can get both values correctly but I am doing something wrong on how to pass the first team_id of the team for each league.
These are the steps, I put only the relevant code. I am creating firstTeamStats state that I am dispatching
In my actions => index.js
export const RECEIVE_FIRST_TEAM_STATS = "RECEIVE_FIRST_TEAM_STATS";
export const receivedFirstTeamStats = json => ({
type: RECEIVE_FIRST_TEAM_STATS,
json: json
});
.get(`https://www.api-football.com/demo/api/v2/teams/league/${id}`)
.then(res => {
let teams = res.data.api.teams;
dispatch(receivedFirstTeamStats(teams[0].team_id));
})
in my reducer => index.js
case RECEIVE_FIRST_TEAM_STATS:
return {
...state,
firstTeamStats: action.json,
isTeamsDetailLoading: false
};
in my League.js component
import {getTeamsStats} from "../actions";
const mapStateToProps = state => ({
firstTeamStats: state.firstTeamStats
});
const mapDispatchToProps = {
getStats: getTeamsStats,
};
const onClick = (evt, id, firstTeamStats) => {
evt.preventDefault();
getDetail(id); \\ get the team names in the Detail.js component
getStats(id, firstTeamStats); \\ get the state value for the first team in the dropdown in Detail.js component
};
<a href={`#${item.league_id}`} onClick={(e) => onClick(e, item.league_id, firstTeamStats)}
Right now firstTeamStats in the onclick method above returns correctly the first team state value but of the existing league and not in the one where I click which is what I want.
However I can get this correctly in the Details.js Component, I have put {firstTeamStats} there in the demo where I have reproduced my case => https://codesandbox.io/s/romantic-solomon-6e0sb (Use CORS Unblock Chrome extension to see )
So the question is, how can i pass {firstTeamStats} correctly in League.js in the method onclick to the request getStats(id, firstTeamStats) where {firstTeamStats} is the state of my first team_id of the league?
Whenever you are making an API call, it is an asynchronous event. You should always await, before using its response in the next line.
So inside getDetail(), you are making an API call. But you haven't declared this method as async. so you are not waiting for it to complete and in the next line you are calling getStats() method with an old data. Thats why your app will always be a step behind, than your DOM events.
One way would be to use async|await and return the API response
from the function, instead of using dispatch.
You can also invoke the getTeamsStats() inside the getTeamsDetailById() after the api response is fetched, instead of invoking it inside OnClick()
event.
const onClick = (evt, id, firstTeamStats) => {
evt.preventDefault();
getDetail(id); \\ this is an async api call. you should have used await here.
getStats(id, firstTeamStats); \\ It will execute before the new stats are fetched
};
In Stats.js, you should not be strictly validating these fields to show Statistics... these are only numbers.. can also be 0 at times. so please remove these checks.
if (
teamsStatsWinHome &&
teamsStatsWinAway &&
teamsStatsDrawHome &&
teamsStatsDrawAway &&
teamsStatsLoseHome &&
teamsStatsLoseAway
) {
I have a working instance of your application... check below.
https://codesandbox.io/s/charming-dewdney-zke2t
Let me know, if you need anything.

How can I change the state of arrays using hooks?

I don't know exactly what it is, but I have run into countless problems in trying to do the simplest state updates on arrays using hooks.
The only thing that I have found to work is using the useReducer to perform a single update on the array with putting dispatch on onClick handlers. In my current project, I am trying to update array state in a for loop nested in a function that runs on a form submit. I have tried many different solutions, and this is just one of my attempts.
function sessionToState(session) {
let formattedArray = []
for (let i = 0; i < session.length; i++) {
formattedArray.push({ url: session[i] })
setLinksArray([...linksArray, formattedArray[i]])
}
}
// --------------------------------------------------------
return (
<div>
<form
method="post"
onSubmit={async e => {
e.preventDefault()
const session = await getURLs({ populate: true })
sessionToState(session)
await createGroup()
I was wondering if there are any big things that I am missing, or maybe some great tips and tricks on how to work with arrays using hooks. If any more information is needed don't hesitate to ask. Thanks.
I was wondering if there are any big things that I am missing
TLDR: setLinksArray does not update linksArray in the current render, but in the next render.
Assuming the variables are initialized as follows:
const [linksArray, setLinksArray] = useState([])
A hint is in the const keyword, linksArray is a constant within 1 render (and this fact wouldn't change with let, because it's just how useState works).
The idea of setLinksArray() is to make a different constant value in the next render.
So the for loop would be similar to:
setLinksArray([...[], session0])
setLinksArray([...[], session1])
setLinksArray([...[], session2])
and you would get linksArray = [session2] in the next render.
Best way to keep sane would be to call any setState function only once per state per render (you can have multiple states though), smallest change to your code:
function sessionToState(session) {
let formattedArray = []
for (let i = 0; i < session.length; i++) {
formattedArray.push({ url: session[i] })
}
setLinksArray(formattedArray)
}
Furthermore, if you need to perform a side effect (like an API call) after all setState functions do their jobs, i.e. after the NEXT render, you would need useEffect:
useEffect(() => {
...do something with updated linksArray...
}, [linksArray])
For a deep dive, see https://overreacted.io/react-as-a-ui-runtime
When invoking state setter from nested function calls you should use functional update form of setState. In your case it would be:
setLinksArray(linksArray => [...linksArray, formattedArray[i]])
It is not exactly clear what kind of problems you encounter, but the fix above will save you from unexpected state of linksArray.
Also this applies to any state, not only arrays.
Performance wise you shouldn't call setState every iteration. You should set state with final array.
const sessionToState = (session) => {
setLinksArray(
session.map(sessionItem => ({url: sessionItem}))
);
}
... or if you want to keep old items too you should do it with function inside setState ...
const sessionToState = (session) => {
setLinksArray(oldState => [
...oldState,
...session.map(sessionItem => ({url: sessionItem}))
]);
}

Categories

Resources