My application allows users to do searches and get suggestions as they type in the search box. For each time that the user enters a character, I use 'fetch' to fetch the suggestions from an API. The thing is that if the user does the search fast, he can get the result before the suggestions are fetched. In this case, I want to cancel the fetch request.
I used to have the same application in React and I could easily cancel the request using AbortController, but that isn't working in Next js.
I did some research and I think the problem is happening because Next doesn't have access to AbortController when it tries to generate the pages.
I also had this problem when I tried to use 'window.innerWidth' because it seems Next doesn't have access to 'window' either.
The solution I found was to use 'useEffect'. It worked perfectly when I used it with 'window'.
const [size, setSize] = useState(0)
useEffect(() => {
setSize(window.innerWidth)
}, [])
But it isn't working when I use AbortController. First I did it like this:
let suggestionsController;
useEffect(() => {
suggestionsController = new AbortController();
},[])
But when I tried to use 'suggestionsController', it would always be undefined.
So I tried to do the same thing using 'useRef'.
const suggestionsControllerRef = useRef(null)
useEffect(() => {
suggestionsControllerRef.current = new AbortController();
},[])
This is how I'm fetching the suggestions:
async function fetchSuggestions (input){
try {
const response = await fetch(`url/${input}`, {signal: suggestionsControllerRef.current.signal})
const result = await response.json()
setSuggestionsList(result)
} catch (e) {
console.log(e)
}
}
And this is how I'm aborting the request:
function handleSearch(word) {
suggestionsControllerRef.current.abort()
router.push(`/dictionary/${word}`)
setShowSuggestions(false)
}
Everything works perfectly for the first time. But if the user tries to do another search, 'fetchSuggestions' function stops working and I get this error in the console 'DOMException: Failed to execute 'fetch' on 'Window': The user aborted a request'.
Does anyone know what is the correct way to use AbortController in Next js?
The solution I found to the problem was create a new instance of AbortController each time that the user does the search. While the suggestions were being displayed, 'showSuggestions' was true, but when 'handleSearch' was called, 'showSuggestions' was set to false. So I just added it as a dependency to useEffect.
useEffect(() => {
const obj = new AbortController();
setSuggestionController(obj)
},[showSuggestions])
I also switched from useRef to useState, but I'm not sure if that was necessary because I didn't test this solution with useRef.
I don't know if that is the best way of using AbortController in Next js, but my application is working as expected now.
I suppose you can try an abort controller to cancel your requests if the user stops typing, but this is not the standard way of solving this common problem.
You want to "debounce" the callback that runs when the user types. Debouncing is a strategy that essentially captures the function calls and "waits" a certain amount of time before executing a function. For example, in this case you might want to debounce your search function so that it will only run ONCE, 500 ms after the user has stopped typing, rather than running on every single keypress.
Look into debouncing libraries or write a debounce function yourself, but fair warning it can be pretty tricky at first!
Related
I think I understand why React.StrictMode causes functions to be called twice. However, I have a useEffect that loads data from my api:
useEffect(() => {
async function fetchData() {
const data = await getData();
setData(data);
}
fetchData();
}, []);
In my getData() function I call a maintenance script cullRecords() that cleans up my data by deleting records over a certain age before returning the data:
async function getData(){
let results = await apiCall();
cullRecords(results);
return results;
}
Here's the rub: React.StrictMode fires the getData() function twice, loading up the apiCall() twice and firing the cullRecords() twice. However, by the time the second cullRecords() subscript fires, my API throws an error because those records are already gone.
While it's not the end of the world, I'm curious if I'm doing something wrong, or if this is just a fringe case, and not to worry about it.
You can read through here also:
https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
The same issue can occur if the user leaves/visits a route quickly for example (which is what development mode is simulating here). It might not be the the best approach to call a backend maintenance script when a UI component is being rendered.
As per the race condition happening on APIs, it’s useful to implement debouncing methods as it’s explaned below.
The debounce() function forces a function to wait a certain amount of
time before running again. The function is built to limit the number
of times a function is called.
You can either use debouncing on either server-side or client side as they have pretty similar implementation.
I am using node module pg in my application and I want to make sure it can properly handle connection and query errors.
The first problem I have is I want to make sure it can properly recover when postgres is unavailable.
I found there is an error event so I can detect if there is a connection error.
import pg from 'pg'
let pgClient = null
async function postgresConnect() {
pgClient = new pg.Client(process.env.CONNECTION_STRING)
pgClient.connect()
pgClient.on('error', async (e) => {
console.log('Reconnecting')
await sleep(5000)
await postgresConnect()
})
}
I don't like using a global here, and I want to set the sleep delay to do an small exponential backoff. I noticed "Reconnecting" fires twice immediately, then waits five seconds and I am not sure why it fired the first time without any waiting.
I also have to make sure the queries execute. I have something like this I was trying out.
async function getTimestamp() {
try {
const res = await pgClient.query(
'select current_timestamp from current_timestamp;'
)
return res.rows[0].current_timestamp
} catch (error) {
console.log('Retrying Query')
await sleep(1000)
return getTimestamp()
}
}
This seems to work, but I haven't tested it enough to make sure it will guarantee the query is executed or keep trying. I should look for specific errors and only loop forever on certain errors and fail on others. I need to do more research to find what errors are thrown. I also need to do a backoff on the delay here too.
It all "seems" to work, I don't want to fail victim to the Dunning-Kruger effect. I need to ensure this process can handle all sorts of situations and recover.
TL;DR: Is there some well-known solution out there using React/Redux for being able to offer a snappy and immediately responsive UI, while keeping an API/database up to date with changes that can gracefully handle failed API requests?
I'm looking to implement an application with a "card view" using https://github.com/atlassian/react-beautiful-dnd where a user can drag and drop cards to create groups. As a user creates, modifies, or breaks up groups, I'd like to make sure the API is kept up to date with the user's actions.
HOWEVER, I don't want to have to wait for an API response to set the state before updating the UI.
I've searched far and wide, but keep coming upon things such as https://redux.js.org/tutorials/fundamentals/part-6-async-logic which suggests that the response from the API should update the state.
For example:
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Return a new todos state array with the new todo item at the end
return [...state, action.payload]
}
// omit other cases
default:
return state
}
}
As a general concept, this has always seemed odd to me, since it's the local application telling the API what needs to change; we obviously already have the data before the server even responds. This may not always be the case, such as creating a new object and wanting the server to dictate a new "unique id" of some sort, but it seems like there might be a way to just "fill in the blanks" once the server does response with any missing data. In the case of an UPDATE vs CREATE, there's nothing the server is telling us that we don't already know.
This may work fine for a small and lightweight application, but if I'm looking at API responses in the range of 500-750ms on average, the user experience is going to just be absolute garbage.
It's simple enough to create two actions, one that will handle updating the state and another to trigger the API call, but what happens if the API returns an error or a network request fails and we need to revert?
I tested how Trello implements this sort of thing by cutting my network connection and creating a new card. It eagerly creates the card immediately upon submission, and then removes the card once it realizes that it cannot update the server. This is the sort of behavior I'm looking for.
I looked into https://redux.js.org/recipes/implementing-undo-history, which offers a way to "rewind" state, but being able to implement this for my purposes would need to assume that subsequent API calls all resolve in the same order that they were called - which obviously may not be the case.
As of now, I'm resigning myself to the fact that I may need to just follow the established limited pattern, and lock the UI until the API request completes, but would love a better option if it exists within the world of React/Redux.
The approach you're talking about is called "optimistic" network handling -- assuming that the server will receive and accept what the client is doing. This works in cases where you don't need server-side validation to determine if you can, say, create or update an object. It's also equally easy to implement using React and Redux.
Normally, with React and Redux, the update flow is as follows:
The component dispatches an async action creator
The async action creator runs its side-effect (calling the server), and waits for the response.
The async action creator, with the result of the side-effect, dispatches an action to call the reducer
The reducer updates the state, and the component is re-rendered.
Some example code to illustrate (I'm pretending we're using redux-thunk here):
// ... in my-component.js:
export default () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(MyActions.UpdateData(someDataFromSomewhere));
});
return (<div />);
};
// ... in actions.js
export const UpdateData = async (data) => (dispatch, getStore) => {
const results = await myApi.postData(data);
dispatch(UpdateMyStore(results));
};
However, you can easily flip the order your asynchronous code runs in by simply not waiting for your asynchronous side effect to resolve. In practical terms, this means you don't wait for your API response. For example:
// ... in my-component.js:
export default () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(MyActions.UpdateData(someDataFromSomewhere));
});
return (<div />);
};
// ... in actions.js
export const UpdateData = async (data) => (dispatch, getStore) => {
// we're not waiting for the api response anymore,
// we just dispatch whatever data we want to our reducer
dispatch(UpdateMyStore(data));
myApi.postData(data);
};
One last thing though -- doing things this way, you will want to put some reconciliation mechanic in place, to make sure the client does know if the server calls fail, and that it retries or notifies the user, etc.
The key phrase here is "optimistic updates", which is a general pattern for updating the "local" state on the client immediately with a given change under the assumption that any API request will succeed. This pattern can be implemented regardless of what actual tool you're using to manage state on the client side.
It's up to you to define and implement what appropriate changes would be if the network request fails.
I have a simple React page / component in Gatsby that makes an API call. For this API call I need a token. I use gatsby-theme-auth0 to obtain this token via their AuthService object.
I am starting the API call in my useEffect. It looks like this:
useEffect(() => {
//defining the async function
async function fetchFromAPI() {
try {
const data = await fetchData()
setData(data)
}
}
//executing the async function:
fetchFromAPI()
}, [])
The function fetchData(), which is asynchronously called in useEffect currently looks like so:
async function fetchData() {
const client = new GraphQLClient(SERVER_URL_GRAPHQL)
let aToken = await AuthService.getAccessToken()
client.setHeader('authorization', `Bearer ${aToken}`)
const query = ...
const data = await client.request(query)
return data
}
All of this generally works. When I navigate to this page, from a different page within my SPA it works. However, when I reload the page, it doesn't. the access token (aToken) then comes back as undefined.
But: I can make things work, when I wrap a setTimeout around the whole call. Then the access token comes back fine and isn't undefined. So I guess something first needs to initialise before AuthService can be called? I'm just not sure how to ensure this.
But this is not what I want to do in production. Now I am wondering why this is. Maybe I am using useEffect the wrong way? Unfortunately, I have not been able to find anything online or on github so far. I'm sure the problem is rather basic though.
EDIT: The AuthService.getAccessToken() method can be found here It's part of gatsby-theme-auth0
EDIT: To clarify, the server does receive the request and sends back {"error":"jwt malformed"} - which makes sense, since it's undefined.
I don't know if you have the authentication in a hook already or not, but you need to check if the user is authenticated before you make any api call, especially those that on app init. Do you have a hook/context when you handle the authentication ? If you have, you can change your code a bit
const {isAuthenticated} = useContext(userAuthenticatedContext)
useEffect(() => {
//defining the async function
async function fetchFromAPI() {
try {
const data = await fetchData()
setData(data)
}
}
//executing the async function:
if(isAuthenticated) fetchFromAPI()
}, [isAuthenticated])
This way, isAuthenticated is a dependency in your useEffect and it will run again when the value of isAuthenticated is changed and it will not fail as you are doing a check, before making the call.
getAccessToken relies on that modules' this.accessToken value to be set. It looks like you need to call either handleAuthentication or checkSession prior to making your call so that the value gets initialized properly. Consider putting checkSession somewhere that runs when the page loads.
I send request to server everytime user types something. I use debounce for the 400ms delay:
type = debounce((text) => {
this.props.actions.loadInfo(text)
}, 400);
When I type something, stop and start again and repeat it, several requests are send and I receive irrelevant data. I use promises:
export const loadInfo = (text) => dispatch => {
loadData(text).then(result => {
dispatch(showUserData(result));
});
};
export const loadData = async (text) => {
const tabData = await axios.get(`url&query=${text}`);
return tabData;
}
I need somehow cancel previous request if user sends the new one(when he typed something), what is the best way to do that? I expected debounce will help me but not. I use axios. This is not duplicate of questions here, I checked provided solutions but thet don't help me
The problem is similar to this one. Axios cancellation API can be used to cancel old requests. This should be done in a function that does a request (loadData) and has direct access to Axios, it may be also debounced:
let cancelObj;
export const loadData = debounce((text) => {
if (cancelObj) {
this.cancelObj.cancel();
}
cancelObj = CancelToken.source();
return axios.get(`url&query=${text}`, {
cancelToken: this._fetchDataCancellation.token
}).catch(err => {
// request wasn't cancelled
if (!axios.isCancel(err))
throw err;
});
}, 200);
Since Redux is used, other solutions may involve it, they depend on how Redux is used.
Even I tried to use debounce function in my code but the problem is that if user types very fast stop and then again start typing, in that case, your input values get updated and UI get distorted, to avoid this I used XMLHttpRequest and its abort() to cancel the previous calls, if calls do not succeed then it will be canceled,
you can try this solution, https://stackoverflow.com/a/55509957/9980970