I tried to dispatch the API call using redux in useEffect hooks. After the response came to redux-saga response goes to reducer and the reducer updated the state successfully but my component is not refreshing.
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import SubscriptionComponent from '../../Components/Subscription/Subscription';
import SubscriptionActions from '../../Redux/Subscription/Actions';
import {
getMySubscriptions,
getMySubscriptionByName,
getMySubscriptionByGroup,
} from '../../Redux/Subscription/Selectors';
const Subscription = (props) => {
const { navigation } = { ...props };
const [visible, setVisible] = useState(false);
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const dispatch = useDispatch();
const [data, setData] = useState(subscriptionList);
const payload = {
memberId: '604f2ad047bc495a0a7fad26',
vendorId: '5fd484c39590020dc0dfb82a',
vendorOrgId: '5fd484439590020dc0dfb829',
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
const onHandleSubscriptionByName = () => {
setVisible(false);
const subscription = getMySubscriptionByName(data);
setData(subscription);
};
const onHandleSubscriptionByGroup = () => {
setVisible(false);
const subscription = getMySubscriptionByGroup(data);
setData(subscription);
};
return (
<SubscriptionComponent
list={data}
navigation={navigation}
onPressList={(val) =>
navigation.navigate('SubscriptionDetails', { _id: val._id, name: val.name })
}
visible={visible}
openMenu={() => setVisible(!visible)}
closeMenu={() => setVisible(!visible)}
sortByName={() => onHandleSubscriptionByName()}
sortBySub={() => onHandleSubscriptionByGroup()}
/>
);
};
export default Subscription;
used reselect to get the state from redux.
export const getMySubscriptions = createSelector(mySubscriptionSelector, (x) => {
const mySubscriptions = x.map((item) => {
return {
_id: item._id,
image: 'item.image,
description: item.description,
name: item.name,
subscriptionGroup: item.subscriptionGroup,
subscriptionAmount: item.subscriptionAmount,
status: item.status,
delivery: item.delivery,
product: item.product,
};
});
return mySubscriptions ;
});
Why component is not refreshing.
Thanks!!!
You're storing the selection result in local state.
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const [data, setData] = useState(subscriptionList);
useState(subscriptionList) will only set data initially not on every update.
EDIT:
Your setup is a little odd:
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
Using data in the dependency array of useEffect, will cause refetching the data, whenever data is updated. Why? I looks like your sorting is working locally, so no need to refetch?
I would suggest to store the sort criteria (byName, byGroup) also in Redux and eliminate local component state, like that:
// ToDo: rewrite getMySubscriptions so that it considers sortCriteria from Redux State
const subscriptionList = useSelector(getMySubscriptions);
const dispatch = useDispatch();
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
// Empty dependency array, so we're only fetching data once when component is mounted
}, []);
const onHandleSubscriptionByName = () => {
dispatch(SubscriptionActions.setSortCriteria('byName'));
};
const onHandleSubscriptionByGroup = () => {
dispatch(SubscriptionActions.setSortCriteria('byGroup'));
};
As mentioned in the comments you will need to add a new action setSortCriteria and reducer to handle the sorting and adjust your selector, so that it filters the subscription list when a sortCriteria is active.
You do not update data after fetching new subscription.
const [data, setData] = useState(subscriptionList);
Only initializes data, but does not update it, you need to add useEffect to update data:
useEffect(() => {
setData(subscriptionList);
}, [JSON.stringify(subscriptionList)]);
JSON.stringify only used for deep compare complex objects, since useEffect only runs shallow compare and might miss, changes in objects.
-----EDIT------
Other problem might be that your getMySubscriptions function might need deep compare, since useSelector by itself doesn't do that, example might be:
import { useSelector, shallowEqual } from 'react-redux';
const subscriptionList = useSelector((state) => getMySubscriptions(state), shallowEqual);
Note that both solutions must be used.
Related
I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.
Here is my code example:
import { useDispatch, useSelector } from 'react-redux';
import { updateMyData } from '../features';
const MyComponent = ({ list }) => {
const dispatch = useDispatch();
// select myData from redux store
const someData = useSelector((state) => state.myData);
React.useEffect(() => {
// YOU WILL NOT SEE THIS LINE
console.log('useEffect', someData);
}, [someData]);
// each time I can see newly changed data
console.log('someData', someData);
const handleClick = () => {
const newData = list.find((l) => l.name === 'myName');
// change myData, in my case it is some pagination, and I select newData from existing list
dispatch(updateMyData(newData));
};
return <div onClick={handleClick}>{'Hello world!'}</div>;
};
In my case, I have a list of items, and when I press some button change it (in redux store).
someData changes, MyComponent rerenders but useEffect wasn't called.
useEffect was called only during the first render.
Why useEffect hadn't called when its deps were changed?
I want to use the list to seData and setUserData. I am using data to map through the list of users and display on the table. I am using the userData to get user information in different components. This is causing infinite loop.
import { useState, useEffect, useContext } from 'react';
import { collection, onSnapshot } from 'firebase/firestore';
import { db, auth } from '../../firebase';
import { UserContext } from '../../Contexts/UserContext';
const Datatable = () => {
const {userData, setUserData} = useContext(UserContext);
const [data, setData] = useState([]);
const [isEditing, setIsEditing] = useState(false);
const [currentId, setCurrentId] = useState("");
useEffect(() => {
const getUserData = () => {
onSnapshot(collection(db, "users"), (snapshot) => {
let list = [];
snapshot.docs.forEach((doc) => {
list.push({ id: doc.id, ...doc.data() });
setData(list);
setUserData(list)
});
}, (err) => {
console.log(err);
});
}
getUserData();
}, [])
}
Updating values to the state should not be done in useEffect. You have to create a function outside and update the state value.
If you do so, You will be getting Warning: Maximum update depth exceeded. error.
This happens because once if you update a state the reference of the state will be changed and component rerender happens. Again since the reference is changed. The useEffect will be called again and this happens infinitely which the react stops after a certain extent.
I have theorical question about custom hooks and use effect when redux is involved.
Let`s assume I have this code:
//MyComponent.ts
import * as React from 'react';
import { connect } from 'react-redux';
const MyComponentBase = ({fetchData, data}) => {
React.useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data?.name}</data>
}
const mapStateToProps= state => {
return {
data: dataSelectors.data(state)
}
}
const mapDispatchToProps= {
fetchData: dataActions.fetchData
}
export const MyComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponentBase);
This works as expected, when the component renders it does an async request to the server to fetch the data (using redux-thunk). It initializes the state in the reduces, and rerender the component.
However we are in the middle of a migration to move this code to hooks. Se we refactor this code a little bit:
//MyHook.ts
import { useDispatch, useSelector } from 'react-redux';
import {fetchDataAction} from './actions.ts';
const dataState = (state) => state.data;
export const useDataSelectors = () => {
return useSelector(dataState);
}
export const useDataActions = () => {
const dispatch = useDispatch();
return {
fetchData: () => dispatch(fetchDataAction)
};
};
//MyComponent.ts
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, [fetchData]);
return <div>{data?.name}</data>
}
With this change the component enters in an infite loop. When it renders for the first time, it fetches data. When the data arrives, it updates the store and rerender the component. However in this rerender, the useEffect says that the reference for fetchData has changed, and does the fetch again, causing an infinite loop.
But I don't understand why the reference it's different, that hooks are defined outside the scope of the component, they are not removed from the dom or whateverm so their references should keep the same on each render cycle. Any ideas?
useDataActions is a hook, but it is returning a new object instance all the time
return {
fetchData: () => dispatch(fetchDataAction)
};
Even though fetchData is most likely the same object, you are wrapping it in a new object.
You could useState or useMemo to handle this.
export const useDataActions = () => {
const dispatch = useDispatch();
const [dataActions, setDataActions] = useState({})
useEffect(() => {
setDataActions({
fetchData: () => dispatch(fetchDataAction)
})
}, [dispatch]);
return dataActions;
};
first of all if you want the problem goes away you have a few options:
make your fetchData function memoized using useCallback hook
don't use fetchData in your useEffect dependencies because you don't want it. you only need to call fetchData when the component mounts.
so here is the above changes:
1
export const useDataActions = () => {
const dispatch = useDispatch();
const fetchData = useCallback(() => dispatch(fetchDataAction), []);
return {
fetchData
};
};
the 2nd approach is:
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, []);
return <div>{data?.name}</data>
}
I am stuck at getting context data.
I have a context and a component which uses its data.
I need to get the updated data of context's variable on API call success in my component.
so How can I do that ?
Here what I have tried.
context.js
import React, { useState, createContext,useEffect } from 'react';
import {getData} from './actionMethods';
const NewContext = createContext();
function newContextProvider(props) {
const [dataValue, setData] = useState([])
useEffect(() => {
const fetchMyData = async () => {
const dataValue = await getData(); // this is an API call
setData(dataValue)
};
fetchMyData();
}, []);
return (
<NewContext.Provider
value={{
state: {
dataValue
},
actions: {
}
}}
>
{props.children}
</NewContext.Provider>
);
}
const newContextConsumer = newContext.Consumer;
export { newContextProvider, newContextConsumer, newGridContext };
myComponent.js
import React, { useState, useContext } from 'react'
import context from './context'
import deleteAPI from './actionMethods'
function myComponent(props) {
const id= 10
const {state,actions} = useContext(context)
deleteAPI(id).then(res => {
if (res){
// what should I write here to get the updated Data from the context which will call an API to get the updated data.
}
})
}
Any help would be great.
Thank You.
As a generic example, one option is to fetch the data from the server when the app loads in the front-end. From there you can send requests to modify the server data and at the same time update your local version. Something like:
Fetch data and save it to the local store: [{id: 0, name: 'first'},{id: 1, name: 'second'}]
Modify the data sending a request to the server. For example deleting an item. id: 0
Once the server responds confirming the operation was successful you can modify that data in the local store. [{id: 1, name: 'second'}]
You can handle the data using a Redux store or a React Context. For example, using a Context:
export const ItemsContext = createContext([]);
export const ItemsContextProvider = props => {
const [items, setItems] = useState([]);
const deleteItem = id => {
deleteItemsAxios(id).then(() => {
setItems(items => items.filter(item => item.id !== id));
});
};
useEffect(() => {
const fetchItems = async () => {
const items_fetched = await fetchItemsAxios();
if (items_fetched) {
setItems(items_fetched);
} else {
// Something went wrong
}
};
fetchItems();
}, []);
return (
<ItemsContext.Provider
value={{
items,
deleteItem
}}
>
{props.children}
</ItemsContext.Provider>
);
};
We define a Component that will manage the data fetch. The data items are inside a state. When the Component mounts we fetch the items and save them in the state. If we want to delete an item we first call the corresponding fetch function. Once it finishes, if it was successful, we update the state and remove that item. We use React Context to pass the items data, as well as the deleteItem function, to any component that needs them.
Let me know if you need more explanation.