I'm starting to use hooks in React and I got stuck, when I realized I would need an array of hooks to solve my problem. But according to the Rules of Hooks
Only Call Hooks at the Top Level
I'm not allow to call hooks inside a loop (and I guess also not in map).
My custom hook subscribes to an API and adds data to the state when there is an update:
export const useTrace = (id) => {
[trace, setTrace] = useState([])
useEffect(() => {
Api.getCurrentTrace(id)
.then(currentTrace => {
setTrace(currentTrace)
})
}, [id])
useEffect(() => {
Api.subscribeTraceUpdate(onUpdateTrip)
return () => {
Api.unsubscribeTraceUpdate(onUpdateTrip)
}
}, [])
const onUpdateTrip = msg => {
if (msg.id === id) {
setTrace([msg.data].concat(trace))
}
}
}
In my component I have a state with an array of IDs. For each ID I would like to use the useTrace(id) hook somehow like this:
import DeckGL from '#deck.gl/react'
function TraceMap({ ids }) {
const data = ids.map((id) => ({
id,
path: useTrace(id)
}))
const pathLayer = new PathLayer({
id: 'path-layer',
data,
getPath: d => d.path
})
return <DeckGL
layers={[ pathLayer ]}
/>
}
For the sake of simplicity I got ids as a property instead of having a state.
Why not have a useTraces custom hook rather than useTrace. This new hook can take an array of ids instead of a single id.
export const useTraces = (ids) => {
[traces, setTraces] = useState([]);
useEffect(() => {
(async () => {
const traces = await Promise.all(
ids.map((id) => Api.getCurrentTrace(id))
);
setTraces(traces);
})();
}, [ids]);
// ...
};
Related
I have been building a Laravel and React app and I had encountered something very embarassing.
The state variable context value is not changing with setState function. The code is following.
const ApiProvider = ({ children }) => {
const [data, setData] = React.useState({})
const [loading, setLoading] = React.useState(true)
const [repNumbers, setRepNumbers] = React.useState({})
useEffect(() => {
const fetchData = async() => {
}
fetchData()
return () => {
setData({})
}
}, [])
return <ApiContext.Provider value = {
{
repData: data,
loading,
repNumbers, //this is the state variable
setRepNumbers //this is the setState function
}
} > {
children
} <
/ApiContext.Provider>
}
In the consumming component
const { repData, repNumbers, setRepNumbers } = React.useContext(ApiContext)
const [pageLoading, setPageLoading] = React.useState(true)
useEffect(() => {
const fetchData = async () => {
setPageLoading(true)
await Axios({
})
.then((res) => {
setRepNumbers({...repNumbers, [id]: res.data })
setPageLoading(false)
return false
})
.catch((err) => {
return {}
})
return false
}
fetchData()
}, [])
If there are 2 consuming components, there should be 2 api calls and the repNumbers state should be mutated 2 times and add 2 id data but it only contains one id and if other call resolves, it replace the former id.
So how can I get both ids in repNumbers state?
This: ...but it only contains one id and if other call resolves, it replace the former id.
Assuming React 18 from your question. If so, React "Batches" updates. So although two updates were made, only the very last one was recorded.
This is dicussed in this blog post.
You can consider flushSync()
You might also consider refactoring your code to avoid the situation in the first place.
React code
import React, { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { auth, db } from "../firebase-config";
import { useNavigate } from "react-router-dom";
function Load() {
const navigate = useNavigate();
const [accountList, setAccountList] = useState([]);
const [hasEmail, setHasEmail] = useState(false);
const accountRef = collection(db, "accounts");
Am i using useEffect correctly?
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
checking whether email exists
const emailCheck = () => {
if (accountList.filter((e) => e.email === auth.currentUser.email)) {
setHasEmail(true);
} else {
setHasEmail(false);
}
};
Redirecting based on current user
const direct = () => {
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
return <div></div>;
}
The code compiles but doesn't redirect properly to any of the pages.
What changes should I make?
First question posted excuse me if format is wrong.
There are two problems here:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
In order:
Since getAccounts is asynchronous, you need to use await when calling it.
But even then, setting state is an asynchronous operation too, so the account list won't be updated immediately after getAccounts completes - even when you use await when calling it.
If you don't use the accountList for rendering UI, you should probably get rid of it as a useState hook altogether, and just use regular JavaScript variables to pass the value around.
But even if you use it in the UI, you'll need to use different logic to check its results. For example, you could run the extra checks inside the getAccounts function and have them use the same results as a regular variable:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
const result = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setAccountList(result);
emailCheck(result);
direct();
};
getAccounts();
}, []);
const emailCheck = (accounts) => {
setHasEmail(accounts.some((e) => e.email === auth.currentUser.email));
};
Alternatively, you can use a second effect that depends on the accountList state variable to perform the check and redirect:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
});
useEffect(() => {
emailCheck();
direct();
}, [accountList]);
Now the second effect will be triggered each time the accountList is updated in the state.
I have a functional component (App.js) where I want to fetch some initial data using useEffect.
useEffect(() => {
const init = async () => {
const posts = await getPosts(0, 3);
const newArticles = await getArticles(posts);
setArticles(() => [...articles, ...newArticles]);
};
init();
}, []);
then I want to pass the result to a child
<ArticleList articles={articles}></ArticleList>
but in the Article component I get an empty array when I try to console.log the props.
useEffect(() => {
console.log(props.articles);
setArticles(() => props.articles);
}, [props.articles]);
How can I solve this issue?
I have a Firebase Realtime Database and I want to display the records by mapping a list.
So far I have:
useEffect(() => {
dbRefObject.on('value', snap => getRecords(snap.val()))
}, [dbRefObject, records])
And elsewhere I have:
export const dbRefObject = firebase.database().ref().child('record');
My getRecords() function is:
const getRecords = (snap) => {
let _recordsMap= []
for (let record in snap) {
_recordsMap.push({[record] : snap[record]})
}
I want some kind of behaviour like an unsubscribe() function returned by the useEffect, but I can't get this to work?
useEffect(() => {
const unsubscribe = () => {dbRefObject.on('value', snap => getRecords(snap.val()))}
return () => {
unsubscribe()
}
}, [dbRefObject, records])
you need to call dbRefObject.off("value", originalCallback);
check https://firebase.google.com/docs/database/admin/retrieve-data#section-detaching-callbacks
I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}