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

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.

Related

How to persist the same data even after useQuery key change and component is unmounted & remounted again

I have been stuck with one issue, let's say I have this below custom react query hook.
export const useGetData = () => {
const { filter } = useSelector((state) => state.filterReducer);
const queryInfo = useQuery(["filter", filter], () => getData(), {
staleTime: Infinity,
cacheTime: Infinity,
enabled: false,
keepPreviousData: true
});
return {
...queryInfo,
data: useMemo(() => queryInfo.data, [queryInfo.data])
};
};
so here I'm actually storing the user's filter state in the global redux store and whenever the user makes any changes this will create a new cache with data as undefined. so since this query has been set enabled to false it will not make an API call and the user has to manually click on the apply filter button in the UI in order to get the data, which actually makes refetch call and gets the data update the existing undefined cache with the data returned from the server.
since this query has keepPreviousData set to true it will keep the previous data in the UI. but in the cache, the new filter has been updated with undefined. So now let's say the user doesn't apply the filter (by clicking on the apply filter button), they just change the filter (which actually creates a new cache), and let's say the user unmounts the component and mounts the component again (using a toggle), the previous data which was showing has gone! (because now it's showing undefined)
Is it possible to keep the previous data if enabled: false, keepPreviousData: true, even if the component is unmounted and mounted back again?
here is the reproduced code sandbox - link
I'm stuck with this issue for the past couple of days not sure how to fix it. Any help on this will be highly appreciated thanks in advance.
The problem seems to be the local state management. As long as the user changes filters, they shouldn't be sent to redux because that drives the query. It is also intermediate state that redux is not interested in. If the user hasn't applied the filter, but it is globally available, you cannot distinguish between filters that have been applied and filters that haven't been applied yet, leaving your UI in an inconsistent state.
The state should likely be kept local in the form only. Once the user clicks apply, you save the state to redux, which will re-fetch the query.
As a side-note, I would automatically enable the query once you have filters. Also, queryInfo.data is referentially stable - there is no need to memoize it:
export const useGetData = () => {
const { filter } = useSelector((state) => state.filterReducer);
return useQuery(["filter", filter], () => getData(filter), {
staleTime: Infinity,
cacheTime: Infinity,
enabled: !!filter,
keepPreviousData: true
});
};
Then, the local state management will look something like this (pseudo-code):
<FilterForm onSubmit={(data) => dispatch(actions.setFilter(data))} />

React Query // How to properly use client-state and server-state (when user can drag/sort items on the UI)?

I have an React App using React Query to load a dataset of folders containing items. The app allows the user to drag/sort items (within a folder and drag to another folder). Before implementing drag/sort it was simple, RQ fetched the dataset folderData and then the parent supplied data to child components: parent > folder > item .
In order to implement drag/sort, I am now having to copy the entire dataset folderData into a client-state variable foldersLocal. This is the only way I could figure out how to change the UI when the user moves an item.
However, I feel like this approach essentially removes most of the benefits of using React Query because as soon as the data is fetched, I copy the entire dataset into a "client-side" state variable (foldersLocal) and only work with it. This also makes the QueryClientProvider functionality effectively useless.
Is there a better way to approach this? Or am I missing something?
// Fetch data using React Query
const {isLoading, isError, foldersData, error, status} = useQuery(['folders', id], () => fetchFoldersWithItems(id));
// Place to hold client-state so I can modify the UI when user starts to drag/sort items
const [foldersLocal, setFoldersLocal] = useState(null);
// Store React Query data in client-state everytime it's fetched
useEffect(() => {
if (status === 'success') {
setFoldersLocal(foldersData);
}
}, [foldersData]);
// Callback to change "foldersLocal" when user is dragging an item
const moveItem = useCallback((itemId, dragIndex, hoverIndex, targetFolderId) => {
setFoldersLocal((prevData) => {
// change data, etc. so the UI changes
}
}
// Drastically simplified render method
return (
<QueryClientProvider client={queryClient}>
<FolderContainer folders={foldersLocal} moveItem={moveItem} moveItemCompleted={moveItemCompleted}/>
</QueryClientProvider>
)

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.

State is always outdated inside the promise resolve method

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

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

Categories

Resources