I have two components which i am working with. In the first component, i made a custom useEffect hook that retrieves data from my server. Please see the code below:
Code snippet One
import {useState, useCallback} from 'react';
import {stageQuizApi} from '../api/quiz';
import {QuestionService} from "../services/IdDbServices/question_service";
const usePostData = ({url, payload, config}) => {
const [res, setRes] = useState({data: null, error: null, isLoading: false});
const callAPI = useCallback(() => {
setRes(prevState => ({...prevState, isLoading: true}));
stageQuizApi.patch(url, payload, config).then( res => {
setRes({data: res.data, isLoading: false, error: null});
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
}).catch((error) => {
console.log(error)
if (error.response) {
const errorJson = error.response.data
setRes({data: null, isLoading: false, error: errorJson.message});
} else if (error.request) {
setRes({data: null, isLoading: false, eror: error.request});
} else {
setRes({data: null, isLoading: false, error: error.message});
}
})
}, [url, config, payload])
return [res, callAPI];
}
export default usePostData;
The above module has two purpose. It first makes an axios request to my endpoint and secondly makes a database insertion to browser IndexDb (similar to localstorage but with sql approach) ( like inserting data into the database using the response that was gotten from the first request. so typically i have a promise in the outer .then block. This part:
Code snippet Two
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
Here is the problem, I am trying to maintain state as i want the result of this module to be shared in another route and i don't want to hit the server again hence i inserted the result into indexDb browser storage. Here is the code that executes the above module:
Code snippet Three
const displaySingleQuestion = ()=>{
OnUserGetQuestion();
history.push('/player/question');
}
The above method is called from my first route /question and it is expected to redirect user to the /player/question when the displaySingleQuestion is called.
On the new route /player/question i then want to fetch the data from IndexDb and update the state of that component using the useEffect code below:
Code snippet Four
useEffect(()=>{
const getAllUserFromIndexDb = async()=>{
try{
const result = await new Promise((resolve, reject) => {
service.getStudents().then(res=>resolve(res)).catch(err=>reject(err))
});
console.log('it did not get to the point i was expecting',result)
if(result[0]){
console.log('it got to the point i was expecting')
const singleQuestion = result[0];
const questionPage = playerQuestionToDisplay;
questionPage.name = singleQuestion.name;
questionPage.anwserRatio = singleQuestion.anwserRatio;
questionPage.answer = singleQuestion.answer;
questionPage.options = singleQuestion.options;
questionPage.playerId = singleQuestion.playerId;
questionPage.question = singleQuestion.question;
questionPage.timePerQuestion = singleQuestion.timePerQuestion;
return setplayerQuestionToDisplay({playerQuestionToDisplay:questionPage})
}
}
catch(error){
console.log(error)
}
}
getAllUserFromIndexDb();
return function cleanup() {
setplayerQuestionToDisplay({playerQuestionToDisplay:{}})
}
},[history.location.pathname]);
The problem is that only one Button click (Code snippet three)(displaySingleQuestion()) triggers the whole functionality and redirect to the /player/question page but in this new route the state is not been set until a page reload as occurred, i tried debugging the problem and i found out that when the button is clicked i found out that Code snippet two is executed last hence when Code snippet Four ran it was in promise and until a page reloads occurs the state of the component is undefined
Thanks for reading, Please i would appreciate any help in resolving this issue.
Related
I am having issue with useTransition() that it is being set to true but actually never changes back to false.
I am trying to delete record from MongoDB and once it is finished I would like to refresh React Server Component as explained here:
https://beta.nextjs.org/docs/data-fetching/mutating
Issue is that in this case server component won't get refreshed and Button is stucked with loading text.
'use client'
const DeleteButton = ({ details }) => {
const [isPending, startTransition] = useTransition();
const router = useRouter();
const handleDelete = async () => {
await fetch('/api/clients', { method: 'DELETE', body: details._id });
startTransition(() => {
console.log('tran started', isPending);
router.refresh();
});
}
useEffect(() => {
// at load isPending = false
// after start tranisition it is set to false
// but it never returns back to false
console.log('is pending ? ', isPending);
}, [isPending]);
return <Button onClick={() => handleDelete()}>{ isPending ? 'Loading' : 'Delete' }</Button>
}
This is BE code at /api/clients
import type { NextApiRequest, NextApiResponse } from 'next';
import ClientsCollection from '../../db/collections/clients';
import Client from '../../helpers/interfaces/client';
type Data = {
name: string;
};
const clientsCollection = new ClientsCollection();
export default function handler(req: NextApiRequest, res: NextApiResponse<any>) {
switch (req.method) {
case 'DELETE': {
const result = clientsCollection.deleteClientById(req.body);
res.status(200).json(result);
}
default:
res.status(403);
}
}
Closing this.
I misunderstood why error did happen, I was basically having useEffect to react on changes from Redux which for some reason blocked setting isPending back to to false.
I did debug it the way that I've commented out current table that was displaying all records and created simple html table without any functionality which helped me identify which code was wrong.
Edit - added minimally reproducible example: https://snack.expo.dev/#hdorra/code
I hope everyone can access the snack. So if you add a task, you can see it show up in the log. Click on the circle, it shows as true (meaning it is clicked). Save and refresh and everything is stored (the task) but the checkbox is not. I stripped the code to make it as bare minimum as possible but it shows the problem.
It has been days of me on this error. I am relatively new to stackoverflow so my apologies if my question isn't clear or I am not asking it in the correct format. I am trying to create a to do app in react native that is using async storage. I created a toggle button that saves the toggle to a state. This button is located in a component:
const [checkBoxState, setCheckBoxState] = React.useState(false);
const toggleComplete = () => {
setCheckBoxState(!checkBoxState)
handleEdit();
console.log(checkBoxState)
}
When the user checks on it - seems to be showing up correctly as marked true and false in the console.
Then, this is passed to an edit handler to update the array, again console shows it is the correct state:
const handleEdit = () => {
props.editHandler(props.todoKey, text, checkBoxState);
console.log(text2, checkBoxState)
};
Then it shows that it saved correctly:
const [todos, setTodos] = React.useState([]);
const handleEdit = (todoKey, text, newStatus) => {
const newTodos = [...todos];
const index = newTodos.findIndex(todos => todos.key === todoKey);
newTodos[index] = Object.assign(newTodos[index], {title: text, status: newStatus});
setTodos(newTodos);
console.log(todos, newStatus)
};
The async function to save to the device and load are as follows:
To save:
const saveTodoToUserDevice = async (todos) => {
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem('todos', stringifyTodos);
} catch (error) {
console.log(error);
}
};
To load from the device:
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem('todos');
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
}
};
So here is the issue - I get the console log that says it is saved correctly and loaded. BUT, when I refresh, the checkbox state is not saved at all, just the title text (so it is saving but the checkbox would always be false (the initial state set). If I clicked on true, it would show as true and then when I refresh, it goes back to false.
I have spent days and days on this and can't figure it out. Any direction would be helpful Thank you!
I have gone through your code and found some errors you are making in different places. In Task.js you can do without that checkBoxState. For that, pass the status to Task as props while rendering it in FlatList, like so:
<Task
key={item.key}
todoKey={item.key}
title={item.title}
status={item.status}
editHandler={handleEdit}
pressHandler={handleDelete}
/>
Then as below, change the button to toggle the status, so you use what's coming from the props and create a function called toggleStatus and pass it to onPress:
<TouchableOpacity onPress={toggleStatus}>
<View
style={[
styles.circle,
!props.status ? styles.completeCircle : styles.incompleteCircle,
]}
></View>
</TouchableOpacity>
The code for toggleStatus:
const toggleStatus = () => {
props.editHandler(props.todoKey, props.title, !props.status);
};
And handleEdit would be simplified to:
const handleEdit = () => {
props.editHandler(props.todoKey, text2, props.status);
setEdit(false);
console.log(props.status);
};
Lastly, in TasksMain.js so you don't replace what's in the storage with that initial array given to useState, make sure saveTodoToUserDevice runs after getTodosFromUserDevice. For that, add the below state in TasksMain.js and slightly change the two functions as follow:
const [loading, setLoading] = React.useState(true);
const saveTodoToUserDevice = async (todos) => {
if (loading) return;
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem("todos", stringifyTodos);
} catch (error) {
console.log(error);
}
};
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem("todos");
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 11 months ago.
I have a simple page editor, When a user clicks edit page it opens an editor. I am passing the ID of the page using redux which will be used to get data from API.
Here is my Editor.
const [pageData, setPageData] = useState("");
const getPage = async (id) => {
try {
const response = await api.get(`/landing_pages/${id}`);
console.log("page", response.data); // displays data at the end
setPageData(response.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getPage(pageID);
console.log('Page Data', pageData) // displays nothing
let LandingPage = pageData;
const editor = grapesjs.init({
container: "#editor",
components: LandingPage.components || LandingPage.html,
})
}, [pageID, getPage])
Why is Page Data display nothing even though the data from API is returned and is displayed in the console at the end? what am I doing wrong here?
Even if you await your getPage call, the updated pageData won't be available until the next render cycle so your assignment to LandingPage will be one cycle behind.
You should instead update in one useEffect and watch for changes to pageData in another.
const [pageData, setPageData] = useState("");
useEffect(() => {
const getPage = async (id) => {
try {
const response = await api.get(`/landing_pages/${id}`);
console.log("page", response.data); // displays data at the end
setPageData(response.data);
} catch (error) {
console.log(error);
}
};
getPage(pageID);
}, [pageID]);
useEffect(() => {
console.log('Page Data', pageData); // displays updated pageData
let LandingPage = pageData;
const editor = grapesjs.init({
container: "#editor",
components: LandingPage.components || LandingPage.html,
});
}, [pageData]);
I'm using React, and just wanted some advice on error handling.
I have my fetch request in an async function, this function is in another folder and is being imported in my App.js file. Im doing this because I want to try out testing with mock service worker, and have read its easier with the requests in a separate file.
From looking at my code below, is this best practice for error handling? Is there a better way thats more concise?
Here is my fetch async function, at the moment i've purposely gave the wrong env variable name so it will give me a 401 unauthorised error.
require('dotenv').config()
export const collect = async () => {
const key = process.env.REACT_APP_API_KE
try{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=london&appid=${key}`)
if(res.status !== 200){
throw new Error(res.status)
}
const data = await res.json()
return data
} catch (error){
let err = {
error: true,
status: error.message,
}
return err
}
}
This is being called in my App.js file (not rendering much at the moment)
import { useState } from 'react'
import { collect } from './utilities/collect'
require('dotenv').config()
function App() {
const [data, setData] = useState("")
const [error, setError] = useState({ error: false, status: "" })
const handleFetch = async () => {
let newData = await collect()
if(newData.error){
setError({ error: newData.error, status: newData.status })
}else {
setData(newData)
}
}
return (
<div className="App">
<h1>weather</h1>
<button onClick={handleFetch}>fetch</button>
</div>
);
}
export default App;
Any help or advise would be great.
When writing an abstraction around Promises or async and await one should make sure it is used appropriately, that is a good Promse must allow it consumer to use it then and catch method or should allow it consumer use try and catch to consume it and provide appropriate information on Errors
From Your code, The abstraction doesnt gives back an appropriate response and doesnt follow the standard behavior of a promise it always resolve and never reject and though the code works its implementation of the collect is different from a standard Promise and wont be nice for a standard code base, for example a good abstraction will provide error information returned from the third party api
Appropriate way to amend code
The third party api returns this response
{
"cod":401,
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."}
This should be your implementation
// Your fetch functon abstraction modified
require('dotenv').config()
const collect = async () => {
const key = process.env.REACT_APP_API_KE;
const res = await fetch(
`http://api.openweathermap.org/data/2.5/weather?q=london&appid=${key}`,
);
if (res.status !== 200) {
const error = await res.json();
throw {message: error.message,status:error.cod};
}
const data = await res.json();
return data;
};
Your app component should now be like this
import { useState } from 'react'
import { collect } from './utilities/collect'
require('dotenv').config()
function App() {
const [data, setData] = useState("")
const [error, setError] = useState({ error: false, status: "" })
const handleFetch = async () => {
try {
let newData = await collect()
setData(newData)
} catch(e){
setError({ error: e.message, status: e.status })
}
}
return (
<div className="App">
<h1>weather</h1>
<button onClick={handleFetch}>fetch</button>
</div>
);
}
export default App;
Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/