React useEffect hook - infinite loop with redux action that uses ID - javascript

I'm using useEffect in combination with reduct actions. I'm aware that I have to extract the action function from the props and provide it as the second argument which generally works for bulk fetches. But if I use an ID from the props as well, it ends up in an infinity loop:
export function EmployeeEdit(props) {
const { fetchOneEmployee } = props;
const id = props.match.params.id;
const [submitErrors, setSubmitErrors] = useState([]);
useEffect(() => fetchOneEmployee(id), [fetchOneEmployee, id]);
const onSubmit = employee => {
employee = prepareValuesForSubmission(employee);
props.updateEmployee(employee._id, employee)
.then( () => props.history.goBack() )
.catch( err => setSubmitErrors(extractErrors(err.response)) );
};
return (
<div>
<h3>Edit Employee</h3>
<NewEmployee employee={props.employee} employees={props.employees} onSubmit={onSubmit} submitErrors={submitErrors} />
</div>
)
}
EmployeeEdit.propTypes = {
fetchOneEmployee: PropTypes.func.isRequired,
employees: PropTypes.array.isRequired,
employee: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
employees: state.employees.items,
employee: state.employees.item
})
export default connect(mapStateToProps, { fetchOneEmployee, updateEmployee })(EmployeeEdit);
And the redux action:
export const fetchOneEmployee = (id) => dispatch => {
axios.get(`http://localhost:8888/api/users/${id}`)
.then(res => {
const employee = res.data;
dispatch({
type: FETCH_ONE_EMPLOYEE,
payload: employee
})
})
});
};
Anybody an idea what I'm doing wrong?

one of the values in your dependency array ([fetchOneEmployee, id]) is changing. It is hard to say which value it is with the limited code you have supplied.
At first glance though, you probably want fetchOne instead of fetchOneEmployee in your array.

Your inifite loop is probably caused because of fetchOneEmployee passed as argument to useEffect. Did you pass fetchOneEmployee to EmployeeEdit as arrow function? If you did then fetchOneEmployee always will be change.
This is a part of great article about react hooks fetch data.
https://www.robinwieruch.de/react-hooks-fetch-data
I especially recommended header CUSTOM DATA FETCHING HOOK

Related

Props defined by async function by Parent in UseEffect passed to a child component don't persist during its UseEffect's clean-up

Please consider the following code:
Parent:
const Messages = (props) => {
const [targetUserId, setTargetUserId] = useState(null);
const [currentChat, setCurrentChat] = useState(null);
useEffect(() => {
const { userId } = props;
const initiateChat = async (targetUser) => {
const chatroom = `${
userId < targetUser
? `${userId}_${targetUser}`
: `${targetUser}_${userId}`
}`;
const chatsRef = doc(database, 'chats', chatroom);
const docSnap = await getDoc(chatsRef);
if (docSnap.exists()) {
setCurrentChat(chatroom);
} else {
await setDoc(chatsRef, { empty: true });
}
};
if (props.location.targetUser) {
initiateChat(props.location.targetUser.userId);
setTargetUserId(props.location.targetUser.userId);
}
}, [props]);
return (
...
<Chat currentChat={currentChat} />
...
);
};
Child:
const Chat = (props) => {
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, []);
...
The issue I'm dealing with is that Child's UseEffect clean up function, which depends on the chatroom prop passed from its parent, throws a TypeError error because apparently chatroom is null. Namely, it becomes null when the parent component unmounts, the component works just fine while it's mounted and props are recognized properly.
I've tried different approaches to fix this. The only way I could make this work if when I moved child component's useEffect into the parent component and defined currentChat using useRef() which honestly isn't ideal.
Why is this happening? Shouldn't useEffect clean-up function depend on previous state? Is there a proper way to fix this?
currentChat is a dependency of that effect. If it's null, the the unsubscribe should just early return.
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
if(!currentChat) return;
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, [currentChat]);
But that doesn't smell like the best solution. I think you should handle all the subscribing/unsubscribing in the same component. You shouldn't subscribe in the parent and then unsubscribe in the child.
EDIT:
Ah, there's a bunch of stuff going on here that's not good. You've got your userId coming in from props - props.location.targetUser.userId and then you're setting it as state. It's NOT state, it's only a prop. State is something a component owns, some data that a component has created, some data that emanates from that component, that component is it's source of truth (you get the idea). If your component didn't create it (like userId which is coming in on props via the location.targetUser object) then it's not state. Trying to keep the prop in sync with state and worry about all the edge cases is a fruitless exercise. It's just not state.
Also, it's a codesmell to have [props] as a dependency of an effect. You should split out the pieces of props that that effect actually needs to detect changes in and put them in the dependency array individually.

why wont jsx render firestore data

I'm trying to GET data from firestore and render it, in the most basic way possible. I can console.log the data, but as soon as I try to render it via JSX, it doesn't. Why is this?
import React from 'react'
import { useState, useEffect } from 'react'
import {db} from '../../public/init-firebase'
export default function Todo() {
const [todo, setTodo] = useState()
const myArray = []
//retrive data from firestore and push to empty array
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
}
useEffect(() => {
getData()
}, [])
return (
<>
<div>
<h1>Data from firestore: </h1>
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
</div>
</>
)
}
First, change myArray to State like this:
const [myArray, setMyArray] = useState([]);
Every change in myArray will re-render the component.
Then, to push items in the State do this:
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
setMyArray(oldArray => [...oldArray, doc.id])
})
})
}
You're just pushing the ID in myArray, so when to show try like this:
{myArray.map((id) => {
console.log('hi')
return <h1 key={id}>{id}</h1>
})}
If you look closely,
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
those are curly braces {}, not parenthesis (). This means that although doc exists, nothing is happening since you are just declaring <h1>{doc.id}</h1>. In order for it to render, you have to return something in the map function. Try this instead:
{myArray.map((doc) => {
console.log('hi')
return <h1>{doc.id}</h1>
})}
In order to force a "re-render" you will have to use the hooks that you defined
https://reactjs.org/docs/hooks-intro.html
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
// in case the request doesn't fail, and the array filled up with correct value using correct keys:
// i'd setState here, which will cause a re-render
setTodo(myArray)
}

How to run useQuery inside forEach?

I have loop - forEach - which find productId for every element of array. I want to fetch my database by productId using apollo query.
How to do it?
products.forEach(({ productId, quantity }) =>
// fetch by 'productId'
);
From the rules of hooks:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.
Hooks cannot be used inside a loop, so you can't use them inside a forEach callback.
You should create a separate component for each product that only uses the useQuery hook once. You can then map over the products and return the component for each one:
const YourComponent = () => {
...
return products.map(({ productId, quantity }) => (
<Product key={productId} productId={productId} quantity={quantity} />
))
}
const Product = () => {
const { data, error, loading } = useQuery(...)
// render your data accordingly
}
To run useQuery based on logic or manually multiple times by setting refetchOnWindowFocus & enable properties to false and then calling method refetch(). example as below.
const RefetchExample = () => {
const manualFetch = {
refetchOnWindowFocus: false,
enabled: false
}
const GetAssetImage = async () => {
const {data} = await axiosInstance.get("Your API")
return data
}
const {assetImage, refetch} = useQuery('assetImage', GetAssetImage, manualFetch)
const imageFetch = () => {
refetch();
}
return (
<Button onClick={imageFetch}>Refetch</Button>
)
}
export default RefetchExample
If you want to perform multiple calls to useQuery then you can't do that in a forEach, map etc. You need to use useQueries e.g.
function Products({ productIds }) {
const productQueries = useQueries({
queries: productIds.map(productId => {
return {
queryKey: ['productById', productId],
queryFn: () => fetchProductById(productId),
}
})
})
Example taken from: https://tanstack.com/query/v4/docs/guides/parallel-queries

Infinite loop during useEffect and Reducer

I don't know why but I have infinite loop when fetching data in Redux operations.
I have an app with Redux and ReactJS.
This is my React component
const CustomersTable = (props) => {
useEffect( () => {
props.getAllCustomers()
}, []);
return <Table ...props.customers />
}
const mapStateToProps = (state) => ({
customers: state.customers,
})
const mapDispatchToProps = dispatch => ({
getAllCustomers: () => dispatch(getAllCustomers()),
})
export default connect(
mapStateToProps, mapDispatchToProps
)(CustomersTable);
This is getAllInvoices()
const fetchCustomers = async() => {
/**
* I fetch only documents with flag delete==false
*/
const snapshot = await firestore.collection("customers").where('deleted', '==', false).get()
let data = []
snapshot.forEach(doc => {
let d = doc.data();
d.id_db = doc.id
//...other
data.push(d)
})
return data
}
export const getAllCustomers = () =>
async (dispatch) => {
const customers = await fetchCustomers()
// I reset state becouse I wont duplicate inovices in tables
dispatch(actions.reset())
customers.map(customer => dispatch(
actions.fetch(customer)
))
}
And reducers
const customerReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.FETCH_CUSTOMERS:
return {
...state, list: [...state.list, action.item]
}
case types.RESET_CUSTOMERS:
return {
...state, list: []
}
default:
return state
}
}
I expect that reducers RESET_CUSTOMERS and then FETCH_CUSTOMERS done job. But it still working in loop reset->customers.
I thought that is still rendered the component in useEffect but I think that hook is writing good.
I tested other reducers which are copy-pase reducers from Customers and they work well.
EDIT 1
#godsenal, thanks for your reply:
actions.js:
import types from './types'
const fetch = item => ({
type: types.FETCH_CUSTOMERS, item
})
const reset = item => ({
type: types.RESET_CUSTOMERS, item
})
export default {
fetch,
reset
}
As regards <Table /> it is AntDesign component (https://ant.design/components/table/). Without that, it looks the same.
EDIT 2
It is incredible. I copied all files from modules (customers) and paste into contracts directory. Then I changed all variables, functions, etc from customer to contract. Now it working (only contracts), but customers infinite loop. Maybe something disturbs in outside a structure.
EDIT 3
I found in app.js that in mapStateToProps I added customers to props. After remove (because I don't need it in root component) it began works fine. I suspect that fetch method in <CustomerTable /> affect the <App /> component and it render in a loop. I discovered that component isn't still updated in a loop, but its mounts and unmounts in a loop.
But still, I don't understand one thing. In <App />, I still have in mapStateToProps dispatching invoice from a store (the same case as customers) and in this case, everything works fine.

extracting data using mapstatetoprops

I have this component where I need some data from my Redux store.
However, I see it has been passed some other required data in a bit different way. My concern is as how to use mapStateToProps in this case and get the data.
Here is the component where I need to extract data from redux store:
const NavBarScore = withStyles(navBarScoreStyles)(
({ classes, matchDetails }) => {
// some opeartions on matchDetails
return (
<span className={classes.middleScoreContainer}>
<span className={classes.teamName}>{scoreData.homeTeamName}</span>
<span className={classes.teamName}>{scoreData.awayTeamName} </span>
</span>
);
}
);
I see that in one of the component there is something like this, where CricketFantasy is in one of the rooteReducer:
const NavBarScore = connect(({ cricketFantasy: { matchDetails } }) => ({
matchDetails
}))(NavScore);
I tried doing similar thing in another component and accessing it but it does not show any data.
My concern is how to simply get data from redux in this component using mapstatetoprops.
Create a container component for NavScore say NavScoreContainer which will dispatch events to redux and fetches data and maps state to props.
For example:-
const mapStateToProps = (state, ownProps) => {
return ({
scores: state.scores
})
}
const mapDispatchToProps = (dispatch, ownProps) => ({
fetchScores: () => {
dispatch(fetchScores());
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(NavScore)
Here scores will be passed as a prop to the component and can be accessed as this.props.scores.
declare your component like this:
const NavBarScore = ({ classes, matchDetails }) => {
// some opeartions on matchDetails
return (
<span className={classes.middleScoreContainer}>
<span className={classes.teamName}>{scoreData.homeTeamName}</span>
<span className={classes.teamName}>{scoreData.awayTeamName} </span>
</span>
)
}
with the declaration of your NavBarScore component, just export it this way:
export default withStyles(navBarScoreStyles)(connect(({ cricketFantasy: { matchDetails } }) => ({ matchDetails: cricketFantasyMatchDetails }))(NavBarScore))
When you import it, it will get the data you need or assign it to a new variable if you only need it in the same file.
const NavBarScoreWithData = withStyles(navBarScoreStyles)(connect(({ cricketFantasy: { matchDetails } }) => ({ matchDetails: cricketFantasyMatchDetails }))(NavBarScore))
this is assuming that you have cricketFantasy.matchDetails in your global state, else you can review and test it with connect(state => state)

Categories

Resources