Can't convert null or undefined with React Hooks - reactjs

I have a state that I set with Hooks in react, like this :
useEffect(() => {
if (uid !== null) {
axios.get(`${process.env.REACT_APP_API_URL}api/balance/${uid}`)
.then((res) => {
setUserWalletIncomes(res.data[0].incomes)
})
}
}, [uid])
This state, to be clear, give me this :
Everything works fine I can delete my incomes etc.
Problem is, when I delete the last income (which makes the state empty), I have this error and a page blank :
TypeError: Cannot convert undefined or null to object
Here is my request to delete :
const handleDeleteIncome = (key) => {
let data = { [key]: "" }
axios({
method: "delete",
url: `${process.env.REACT_APP_API_URL}api/balanceOneIncome/${uid}`,
data: data
}).then((res) => {
if {(userWalletIncomes === null) setUserWalletIncomes({})}
setUserWalletIncomes(res.data[0].incomes)
setformIncomes(false)
})
}
I tried to add a condition : if {(userWalletIncomes === null) setUserWalletIncomes({})}, but I still have the same problem.
Can someone help me with this ?
[EDIT]
My state is initialized like this :
const [userWalletIncomes, setUserWalletIncomes] = useState({})
PS : If I reload the page, everything is fine.

Is this the problem? (I am assuming you don't really have the weird {} brace positioning as in your post?)
You have this:
if (userWalletIncomes === null) { setUserWalletIncomes({}) }
setUserWalletIncomes(res.data[0].incomes)
But after you call setUserWalletIncomes({}) you fall through and immediately call it again with the null value in the next line. Maybe you just need to explicitly return after setting it to the empty object? Or, to simplify, you could remove that conditional and do this instead:
setUserWalletIncomes(res.data[0].incomes || {})

Answer, if it helps someone, is this :
const handleDeleteIncome = (key) => {
let data = { [key]: "" }
axios({
method: "delete",
url: `${process.env.REACT_APP_API_URL}api/balanceOneIncome/${uid}`,
data: data
}).then((res) => {
if (res.data[0].incomes) {
setUserWalletIncomes(res.data[0].incomes)
} else {
setUserWalletIncomes({})
}
})
}
Just a condition that tells if response is null, then set the state to an empty object.

Related

How do i stop a dependency from re-rendering infinite times in a useEffect?

I have a react-select multiselect component which is required to have preselected values based on the user role. The react-select component is used to filter data in a react-table.
I have 2 user roles - Dev and tester.
If it the dev, I need to have open and Reopened issues to be filtered on load
If it is a tester, I need to have resolved issues on load
This is a part of the code that i am using to achieve preselected
async function loadInfo() {
const body = {
project_id: projDetails.id,
};
const response = await axios({
method: "post",
url: apilist.dropdownData,
data: body,
})
.catch((err) => console.log(err));
if (response) {
const getData = response.data.data;
// console.log("IsGeneralInfo:", getData)
setGeneralInfo(getData);
let filteredenv = getData.status.filter((item) => item.id == 8 || item.id == 6)
let envfiltered = filteredenv.map((k) => {
return ({ label: k.values, value: k.values });
})
// console.log("envfilter", envfiltered);
// handleMultiStatusFilter(envfiltered);
}
}
// const {current:myArray}=useRef([{ label: 'Closed', value: 'Closed' }])
useEffect(() => {
if(envfilter){
let myArray=[{ label: 'Closed', value: 'Closed' },{ label: 'Reopen', value: 'Reopen' }];
handleMultiStatusFilter(myArray);
}
}, [selectedOptions])
const handleStatusFilter = (e) => {
setFilterValue(e);
if (e.length > 0) {
dispatch(filterByValue({ type: e, viewIssue: viewIssue, }))
}
else {
dispatch(showAllStatus({ type: 'All', viewIssue: viewIssue, }))
}
}
const handleMultiStatusFilter = (e) => {
setFiltered([])
let arr = []
e.map((data) => {
setFiltered(prevState => [...prevState, data.value]);
arr.push(data.value);
})
setSelectedOptions(e)
handleStatusFilter(arr)
}
This is a part of the redux code used for filtering
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchIssueList.fulfilled, (state, action) => {
// Add user to the state array
state.issuesList = {
status: 'idle',
data: action.payload.data.data !== undefined ? action.payload.data.data : [],
dataContainer: action.payload.data.data !== undefined ? action.payload.data.data : [],
no_of_records: action.payload.data.data !== undefined ? action.payload.data.data.length : 0,
error: {}
}
})
The code works fine with the filtering once i login, but the rerendering keeps going to infinite loop
Is there any way i could stop the infinite rerendering of code and have the filtering happen on load of the screen?
while working with dependencies in useEffect, try to use the most primitive part you can find. no complex objects, as they change way too fast.
for example: use the length of an array not the array itself.
even though for arrays it's mostly safe to use itself.
sorry. correction: for arrays it's not safe either. complex objects are compared by reference not by value. for that you need primitive types like number, string or boolean.

What is the correct way to call updateCachedData on a click event in a component that uses the RTKQ query?

I can only think of storing a reference to updateCachedData somewhere globally and use it in that click event but I am not sure this is the React way of doing this.
I have a notifications feed built with a Socket.IO server.
By clicking on a notification it should get deleted from the list. (The list should show only unread notifications.)
But when deleting from the list I create a new array as state in the notifications pane.
When I receive a new notification, all the deleted notifications return back - this is not what I intended.
How can I change the cache entry, more precisely remove items from it without remaking the request for all the notifications?
There are no error messages.
Code
getNotifications: build.query<
IDbNotification[],
IGetNotificationsQueryParams
>({
query: (params: IGetNotificationsQueryParams) => ({
url: `notifications?authToken=${params.authToken || ""}&limit=${
params.limit
}&userId=${params.userId || ""}${
params.justUnread ? "&justUnread" : ""
}`,
method: "GET"
}),
keepUnusedDataFor: 0,
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const { myIo, connectHandler } = getWebSocketConnection(
"notifications",
clone({
subscribtions: arg.userId
? getFollowedUserIds().concat({
uid: arg.userId,
justUnread: arg.justUnread
})
: getFollowedUserIds()
})
);
const listener = (eventData: IDbNotification) => {
if (
(eventData as any).subscriber === arg.userId &&
(!arg.justUnread || typeof eventData.readDateTime === "undefined")
) {
updateCachedData(draft => {
draft.unshift(eventData);
if (draft.length > arg.limit) {
draft.pop();
}
});
}
};
try {
await cacheDataLoaded;
myIo.on("notifications", listener);
} catch {}
await cacheEntryRemoved;
myIo.off("notifications", listener);
myIo.off("connect", connectHandler);
}
})
You can use updateQueryData - updateCachedData is just a shortcut for the current cache entry for convenience.
dispatch(
api.util.updateQueryData('getNotifications', arg, (draft) => {
// change it here
})
)
See this for more context: https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

Delete data in state React Hooks

Maybe it is a dumb question, because I don't find any response on the net for this, but I'm trying to update my state after a axios.delete.
When I add data, I do this and it works fine :
const handleAddIncome = () => {
let incomeName = document.getElementById('newIncomeName').value
let incomeAmount = document.getElementById('newIncomeAmount').value
let data = {
[incomeName]: parseInt(incomeAmount)
}
axios({
method: "put",
url: `http://localhost:5000/api/balance/${uid}`,
withCredentials: true,
data: { incomes: { ...userWalletIncomes, ...data } }
}).then(() => {
setUserWalletIncomes({ ...userWalletIncomes, ...data })
})
}
I also added a bouton that delete the key/value, and this is where I'm stuck.
Here is what I tried, but no success :
const handleDeleteIncome = (key) => {
let data = { [key]: "" }
axios({
method: "delete",
url: `http://localhost:5000/api/balanceOneIncome/${uid}`,
data: data
}).then(() => {
data[key] = null
setUserWalletIncomes(...userWalletIncomes, data)
})
}
PS : the axios delete works fine, my database is updated normally. Not the state
Thanks for help !
[EDIT]
Here is my UseEffect :
useEffect(() => {
if (uid !== null) {
axios.get(`http://localhost:5000/api/balance/${uid}`)
.then((res) => {
if (res.data[0].incomes && res.data[0].fees) {
setUserWalletIncomes(res.data[0].incomes)
setUserWalletFees(res.data[0].fees)
} else if (res.data[0].incomes && !res.data[0].fees) {
setUserWalletIncomes(res.data[0].incomes)
setUserWalletFees({ 'cliquez-ici': '' })
} else if (!res.data[0].incomes && res.data[0].fees) {
setUserWalletIncomes({ 'cliquez-ici': '' })
setUserWalletFees(res.data[0].fees)
}
})
}
}, [uid])
For those who need, I finally chose to change my controller in my backend and make it send the new object after the delete.
I just take that response, and set the new state.
Thanks
Because you already have setUserWalletIncomes({ ...userWalletIncomes, ...data }), I expect userWalletIncomes is an object like { name1: value1, name2: value2 }, right?
Then you have setUserWalletIncomes(...userWalletIncomes, data), use array spread on an object is a runtime error because it's not iterable:
let a = { foo: 42 };
console.log(...a); // TypeError: Found non-callable ##iterator
I guess you still wanted to write this:
let data = { [key]: "" };
setUserWalletIncomes({ ...userWalletIncomes, ...data });
But this only sets the key field to an empty string. To remove the field, you can combine the answer from How do I remove a property from a JavaScript object?
const temp = {...userWalletIncomes};
delete temp[key];
setUserWalletIncomes(temp);

use SWR with depending request data

I'm trying to use SWR to fetch list of users connected to the logged in user id provided by a custom hook.
I can't put useSWR inside either useCallback or useEffect or if (loggedInAdvisor) { ... }... Can't figure out how to do it.
export const fetchDetailedAdvisorPrognoses = (
body: DetailedAdvisorPrognosesRequest
): Promise<DetailedAdvisorPrognoses[]> | null => {
const accessToken = getFromPersistance(ACCESS_TOKEN)
if (!accessToken) {
return null
}
return fetch('https://x/api/v2/advisors/prognoses', {
method: 'POST',
headers: {
...generateDefaultHeaders(),
'Content-Type': 'application/json',
Authorization: getAuthorizationHeader(accessToken),
},
body: JSON.stringify(body), // body data type must match "Content-Type" header
}).then(res => res.json())
}
function Workload(): ReactElement | null {
const { loggedInAdvisor } = useAuthentication()
// yesterday
const fromDate = moment()
.subtract(1, 'day')
.format('YYYY-MM-DD')
// 14 days ahead
const toDate = moment()
.add(13, 'days')
.format('YYYY-MM-DD')
const { data, error } = useSWR<DetailedAdvisorPrognoses[] | null>('fetchWorkloadData', () =>
'detailed',
fetchDetailedAdvisorPrognoses({
advisorIds: [loggedInAdvisor.id], // <-- I want to pause the query until loggedInAdvisor is defined
fromDate,
toDate,
})
)
// todo: display errors better
if (error) {
return <span>Error: {error.message}</span>
}
if (!data) {
return <LoadingV2 isLoading={!data} />
}
if (data && data.length > 0) {
// advisors prognoses is first element in data array
const [first] = data
const days: WorkloadDay[] = Object.keys(first.daysMap).map(date => ({
date,
value: first.daysMap[date],
}))
return <WorkloadLayout before={first.before} days={days} />
}
return null
}
SWR supports Conditional Fetching, instead of using an if branching, you need to pass null as a key (that's the mental modal of React Hooks too):
const { data, error } = useSWR(
loggedInAdvisor ? 'fetchWorkloadData' : null,
() => {...}
)
Updated 2021/12/10:
You can also fetch some data, that based on the result of another request using SWR, too:
const { data: user } = useSWR('/api/user', fetcher)
const { data: avatar } = useSWR(user ? '/api/avatar?id=' + user.id : null, fetcher)
In this case, if user isn't ready the second request will not start since the key will be null. When the first request ends, the second one will start naturally. This is because a re-render will always happen when user changes from undefined to some data.
You can use this method to fetch as many dependent resources as you want, with the best possible parallelism.

keep old content loaded previously in react pagination

I'm new using react.
I have an array from php.
React is getting it using axios to print some posts.
I'd like to show 10 posts and when user scroll down I get more 10 posts.
It is working ok. The problem is react is overwriting my old posts from the screen, I'd to keep all posts, not only the new ones loaded... Any ideas why react is only keeping the new ones?
my return:
return (
<>
{posts.map((post, index) => {
if (posts.length === index + 1) {
number = post.id;
return <div className={classes.post} ref={lastPostElementRef} key={post.id}>{post.titulo}</div>
}
else {
return <div className={classes.post} key={post.id}>{post.titulo}</div>
}
})}
<div>{loading && 'Loading...'}</div>
<div>{error && 'Error'}</div>
</>
)
my axios:
axios({
method: 'GET',
url: 'http://localhost/posts.php',
params: { q: query, page: pageNumber },
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(res => {
setPosts(prevPosts => {
return res.data
})
if(res.data.length < 10) {
setHasMore(false); // total pagination items 10
}
else {
setHasMore(true);
}
thanks.
When you set the state in functional component, React will replace the previous state.
When you update a state variable, unlike this.setState in a component class, the function returned by useState does not automatically merge update objects, it replaces them.
Solution : You have to add the old state and the new state in a new array and then set this as the new state. You could use spread syntax to copy the old and the new state.
Example:
axios({
method: 'GET',
url: 'http://localhost/posts.php',
params: { q: query, page: pageNumber },
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(res => {
setPosts(prevPosts => {
return [...prevPosts, ...res.data, ]
})
if (res.data.length < 10) {
setHasMore(false); // total pagination items 10
}
else {
setHasMore(true);
}
you need to add newly fetched results to current state;
Use this;
return [...prevPosts ,...res.data]
instead of
return res.data

Resources