RTK Query response state - reactjs

I'm trying to convert some Axio code to RTK query and having some trouble. The 'data' response from RTK query doesn't seem to act like useState as I thought.
Original axio code:
const [ importantData, setImportantData ] = useState('');
useEffect(() => {
async function axiosCallToFetchData() {
const response = await axiosAPI.post('/endpoint', { payload });
const { importantData } = await response.data;
setImportantData(importantData);
}
axiosCallToFetchData()
.then((res) => res)
.catch((error) => console.log(error));
}, []);
const objectThatNeedsData.data = importantData;
New RTK Query code
const { data, isSuccess } = useGetImportantDataQuery({ payload });
if(isSuccess){
setImportantData(data.importantData);
}
const objectThatNeedsData.data = importantData;
This however is giving me an infinite render loop. Also if I try to treat the 'data' object as a state object and just throw it into my component as:
const objectThatNeedsData.data = data.importantData;
Then I get an undefined error because it's trying to load the importantData before it's completed. I feel like this should be a simple fix but I'm getting stuck. I've gone through the docs but most examples just use the if statement block to check the status. The API calls are being made atleast with RTK and getting proper responses. Any advice?

Your first problem is that you always call setImportantData during render, without checking if it is necessary - and that will always cause a rerender. If you want to do that you need to check if it is even necessary:
if(isSuccess && importantData != data.importantData){
setImportantData(data.importantData);
}
But as you noticed, that is actually not necessary - there is hardly ever any need to copy stuff into local state when you already have access to it in your component.
But if accessing data.importantData, you need to check if data is there in the first place - you forgot to check for isSuccess here.
if (isSuccess) {
objectThatNeedsData.data = data.importantData;
}
All that said, if objectThatNeedsData is not a new local variable that you are declaring during this render, you probably should not just modify that during the render in general.

Related

Unexpected behaviour of useState (do not renew a constant after getting server data)

I have a very simple react code, which I use to track containers location on a territory. After a new container get's into the territory I have props.operationsList changed. So I send get response to server API when props.operationsList changes
useEffect(() => {
async function fetchContainerLocation() {
const response = await CoordinatesService.getContainersPosition()
console.log('response = ', response.data.features)
setContainersList(response.data.features)
console.log('containersList = ', containersList)
}
fetchContainerLocation()
}, [props.operationsList])
I need to update containersList const, that I use to rerender a map API where I should locate the containers. I define it like that:
const [containersList, setContainersList] = useState([])
I need to set containersList in accordance with that response fron server (response.data.features) to make my map rerender. What's strange,
console.log('response = ', response.data.features)
shows accurate and correct data from server, but the next
console.log('containersList = ', containersList)
is not equal with this response
Instad of getting the map rendered with the right data from server response, I have wrong data. So, I do now understand why such an straightforward approch do not work and how to fix it
State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately.
So, try to log your containersList outside useEffect and compare both logs. both should be same.
TIP: While using map method with your containerList use it like containerList?.map() so that page does not turn out to be blank.
const fetchContainerLocation = async () => {
const response = await CoordinatesService.getContainersPosition();
console.log("response = ", response.data.features);
setContainersList(response.data.features);
};
useEffect(() => {
fetchContainerLocation();
}, [props.operationsList]);
console.log(containerList);
return (
<>
{containerList?.map((container) => (
<p>something you want to render</p>
))}
</>
);
No idea why, but it worked when I changed response.data.features
to [...response.data.features]
Working code
useEffect(() => {
async function fetchContainerLocation() {
setContainersList([])
const response = await CoordinatesService.getContainersPosition()
setContainersList([...response.data.features])
}
fetchContainerLocation()
}, [props.operationsList])
If anybody could explain why, it would be useful

React: Is useEffect() firing before the response arrives from the function I called?

Does useEffect fire before the response for data arrives, hence the reason I get undefined immediately first before getting the proper data shortly after?
My logic is
LoginScreen and set token (jwt)
Define isLoggedIn state then conditionally render HomeScreen
OnHomeScreen call getUserDetails() using the provided JWT
HomeScreen:
const {token, userInfo} = useContext(LoginContext)
const [stateUserInfo, setStateUserInfo] = userInfo
const [stateToken, setStateToken] = token
async function getUserDetails() {
const data = await axios.get(apiValidate+'&JWT='+stateToken)
setStateUserInfo(data.data.data) //does this line run regardless if the response arrives or not?
}
useEffect(() => {
getUserDetails()
},[])
useEffect(() => {
console.log(stateUserInfo) //returns undefined 1st, then the proper object shortly after
},[stateUserInfo])
I 'fix' my code by doing:
useEffect(() => {
if(stateUserInfo) {
console.log(stateUserInfo) }
},[stateUserInfo])
This works but I think it's ugly?
On a deeper question, I feel like I'm trying to do "synchronous" logic with async data! Is there a more elegant way to do this? Maybe my logic is flawed?
useEffect will run on initial render as well. That means both of your effects will run the first time, but the second one will run when the stateUserInfo updates as well.
You can fix this by storing a boolean that validates if you're on the initial render or on subsequent ones. This way you can combine both effects into a single one.
Also, for the commented question in your code: YES, it will run regardless of the return of the server. You should add that request in a try catch in case it throws an error, and also check the response in case it is unexpected.
const {token, userInfo} = useContext(LoginContext)
const [stateUserInfo, setStateUserInfo] = userInfo
const [stateToken, setStateToken] = token
// store a boolean to determine if you're on initial render
const afterUpdate = useRef(false)
async function getUserDetails() {
const data = await axios.get(apiValidate+'&JWT='+stateToken)
setStateUserInfo(data.data.data) // this line WILL run regardless of the answer of the previous line, so you should validate the response first before assigning it.
}
useEffect(() => {
if (afterUpdate.current) {
console.log(stateUserInfo)
} else {
afterUpdate.current = true
getUserDetails()
}
},[stateUserInfo])

Why is useEffect not working here? Do I need to change the condition?

I am trying to run this code, but useEffect is not running a single time.
export default function DetailPage() {
const [post, setPost] = useState({});
const postId = useParams().postId;
console.log(postId);
useEffect(() => {
console.log("useEffect called");
const fetchPost = async () => {
const res = await axios.get(`/posts/${postId}`);
setPost(res.data);
console.log(post);
console.log("useEffect runs");
};
fetchPost();
console.log("useEffect runs 2 ");
}, [postId]);
}
Here, I am getting postId in the console, but not "useEffect run".
I have used similar code (except I am using another variable there instead of postId) in another file, and it's working there.
Please help me with this code.
Be careful, when you use setState the value of your state is changed asynchronously. So to you, it seems that the state doesn't change, but in reality, it will change.
This because when you try to print the value of a state just after the setState, its value still wasn't updated.
When you want to debug how a state changes with console log, create a separate hook that log every change.
Add something like this:
useEffect(()=>{
console.log("Post state changed!")
console.log(post);
},[post])
For what concerns the issue that your last log (console.log("useEffect runs");) is not running the only possible issues are:
postId changes are not triggering useEffect (possible reason: you are using a ref inside your useParams to store values). To check it just put a console.log before running the Axios request:
const fetchPost = async () => {
//Does this ever display your postId? If it does go to point 2.
console.log(`Trying to fetch post ${postId}`);
//Furthermore if postId can be undefined I'll wrap this request with an if
const res = await axios.get(`/posts/${postId}`);
setPost(res.data);
console.log("useEffect runs");
};
There is an uncaught error in your network request.
First tip: always put a try-catch block when awaiting an Axios response.
This will avoid missing uncaught errors.
try {
const res = await axios.get(`/posts/${postId}`);
setPost(res.data);
} catch(e) {
//Here log your error. Remember to avoid showing the log in production
}
When working with Axios I also always suggest checking the network tab of your browser to understand what is going on with your request.
I link you a post where I explain how to do it: https://stackoverflow.com/a/66232992/14106548

Howcome my state is not updating using react hooks and use Effect

My useEffect function is trying to fetch data from an API endpoint. The results resultAllTestTypes are currently logging fine.
However, I can't find out why the allTestTypes are coming back as undefined as I thought I had already set it in a state variable it should be able to log it to the console. But when I log the allTestTypes data it gives me this.
Code:
const [allTestTypes, setAllTestTypes] = useState([])
useEffect(() => {
async function onLoadCreateUnitTests() {
const results = await get('get_tables_autocomplete/b', user.user)
const resultsAllTestTypes = await get('get_all_test_types', user.user)
autoComplete.setTablesAutoComplete(results)
setAllTestTypes(resultsAllTestTypes)
console.log('resultAllTestTypes data ',resultsAllTestTypes.data);
console.log('allTestTypes data ',allTestTypes.data);
}
onLoadCreateUnitTests()
It's setting the state, you just have a console.log in a spot that's not particularly useful.
allTestTypes is a local const. It will never change, and that's not what setAllTestTypes is trying to do. When you set state, this tells react to render the component again. When that render occurs, you'll make a new call to useState, which will return the new value and assign it to a new local const. That new variable can be interacted with by code in the new render, but code from the previous render (such as your console.log) will never see the new value.
If you'd like to verify that the component is rerendering with a new value, move your console.log into the body of the component:
const [allTestTypes, setAllTestTypes] = useState([])
console.log('Rendering with', allTestTypes);
useEffect(() => {
async function onLoadCreateUnitTests() {
const results = await get('get_tables_autocomplete/b', user.user)
const resultsAllTestTypes = await get('get_all_test_types', user.user)
autoComplete.setTablesAutoComplete(results)
setAllTestTypes(resultsAllTestTypes)
}
onLoadCreateUnitTests()
});
cuz setAllTestTypes is async, so u can't get it immediately.
if u want to use it ,use the local variable resultsAllTestTypes instead

React Hooks: Referencing data that is stored inside context from inside useEffect()

I have a large JSON blob stored inside my Context that I can then make references to using jsonpath (https://www.npmjs.com/package/jsonpath)
How would I go about being able to access the context from inside useEffect() without having to add my context variable as a dependency (the context is updated at other places in the application)?
export default function JsonRpc({ task, dispatch }) {
const { data } = useContext(DataContext);
const [fetchData, setFetchData] = useState(null);
useEffect(() => {
task.keys.forEach(key => {
let val = jp.query(data, key.key)[0];
jp.value(task.payload, key.result_key, val);
});
let newPayload = {
jsonrpc: "2.0",
method: "call",
params: task.payload,
id: "1"
};
const domain = process.env.REACT_APP_WF_SERVER;
let params = {};
if (task.method === "GET") {
params = newPayload;
}
const domain_params =
JSON.parse(localStorage.getItem("domain_params")) || [];
domain_params.forEach(e => {
if (e.domain === domain) {
params[e.param] = e.value;
}
});
setFetchData({ ...task, payload: newPayload, params: params });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [task]);
}
I'm gonna need to post an answer because of code, but I'm not 100% sure about what you need, so I'll build a correct answer with your feedback :)
So, my first idea is: can't you split your effects in two React.useEffect? Something like this:
export default function JsonRpc({ task, dispatch }) {
...
useEffect(() => {
...
setFetchData(...);
}, [task]);
useEffect(() => {
...
}, [data]);
..
}
Now, if my understanding are correct, this is an example of events timeline:
Due to the update on task you will trigger the first useEffect, which can setFetchData();
Due to the update on fetchData, and AXIOS call is made, which updates data (property in the context);
At this, you enter the second useEffect, where you have the updated data, but NO call to setFetchData(), thus no loop;
Then, if you wanted (but couldn't) put data in the dependencies array of your useEffect, I can imagine the two useEffect I wrote have some shared code: you can write a common method called by both useEffects, BUT it's important that the setFetchData() call is outside this common method.
Let me know if you need more elaboration.
thanks for your reply #Jolly! I found a work around:
I moved the data lookup to a state initial calculation:
const [fetchData] = useState(processFetchData(task, data));
then im just making sure i clear the component after the axios call has been made by executing a complete function passed to the component from its parent.
This works for now, but if you have any other suggestions id love to hear them!

Resources