Error retry strategy and function argument binding - javascript

I'd like to make my swr state mutations resilient against network errors and support some kind of retry functionality.
Basically what I do is inside my useSWR powered react hook:
const setFavourite = useCallback((objectId) => {
const url = `/favs/${objectId}`
const newData = [objectId, ...data]
return mutate(newData, false)
.then(() => request("POST", url))
.then(() => mutate())
.catch((error) => {
mutate(data, false).catch(() => {})
const userError = new Error("Could not add favourite")
userError.retry = () => setFavourite(objectId)
})
}, [data, mutate, request])
I generate a new Error object with a retry function that can be used by the UI code to show a toast to retry the operation at later time (upon user interaction).
I wonder if the dependencies to setFavourite are still valid at the time retry() would be called. I guess not, as they depend on data and if this changes, then the setFavourite is also regenerated.
Anybody implemented such kind of retry before?

Related

Resetting State in React with saved initial State

I'm trying to save State twice, so I can reset it later on, but no matter what method I try, the 'setFullTrials' won't update with the saved data. The "console.log(savedData)" shows that all the data is there, so that's definitely not the problem. Not sure where I'm going wrong.
function AllTrials({Trialsprop}) {
let [savedData, setSavedData] = useState([]);
let [fullTrials, setFullTrials] = useState([]);
useEffect(() => {
//Call the Database (GET)
fetch("/trials")
.then(res => res.json())
.then(json => {
// upon success, update trials
console.log(json);
setFullTrials(json);
setSavedData(json);
})
.catch(error => {
// upon failure, show error message
});
}, []);
const resetState = () => {
setFullTrials(savedData);
//setFullTrials((state) => ({
...state,
savedData
}), console.log(fullTrials));
// setFullTrials(savedData.map(e => e));
console.log("savedData", savedData)
}
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(fullTrials)
// Whatever else we want to do after the state has been updated.
}, [fullTrials])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "fullTrials" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info.
P.S: the correct syntax for useState is with const, not let.
i.e. - const [state, setState] = useState()

Cancel async fetch request in React Native using Expo

I have a React Native app that has been built using Expo (v35.0.0). I have a simple async function (loadData()) that runs a fetch (via fetchData()) request to an API that the response is then passed into my redux store:
const loadData = async (id, token) => {
setIsLoading(true);
try {
await dispatch(fetchData(id, token));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
};
useEffect(() => {
loadData(user.client.id, user.token);
}, [user.client.id]);
However, when the user logs out we are presented with the warning: "Can't perform a React state update on an unmounted component", which I understand is because the async request has been cancelled.
I have attempted to implement the AbortController approach (as outlined in this article: https://dev.to/iquirino/react-hook-clean-up-useeffect-24e7), however then we are presented with an error stating AbortConroller is unknown.
I thought that support for AbortConroller would be within Expo now as it was added to React Native back in July last year as part of the v0.60 release.
So, is it possible to implement AbortController within Expo to cancel the async request? If not, how should I go about cancelling the request to avoid the warning (& memory leak)?
Because your dispatch is async, it's possible for it to finish after your component is unmounted.
Inside useEffect you can return a function that you call to cleanup. Here you can set a flag to indicate the component is no longer mounted, and if this flag indicates it's no longer mounted, you can tell it not to update state.
eg.
let mounted = true;
const loadData = async (id, token) => {
setIsLoading(true);
try {
await dispatch(fetchData(id, token));
} catch (error) {
if (mounted) setHasError(true);
}
if (mounted) setIsLoading(false);
};
useEffect(() => {
loadData(user.client.id, user.token);
return () => mounted = false;
}, [user.client.id]);

React setState from useState hook causes exception when used in callback passed to RxJS subsciption

I'm writing some helpers to ease usage of RxJS within React.
I wrote a custom hook which allows you to send callbacks to subscribe and get updates from an RxJS stream and update it. This hook also lets you register callbacks for stream error and completion.
These worked properly in tests, but when writing a proper example, React throws an error on stream error, error which should be handled and the message displayed in the UI.
This is a working sandbox with the same code as below, with buttons for updating, sending an error and completing the stream
demo component follows. Note the line where the error happens:
export default ({subject})=>{
const [hasError, setHasError] = useState(false);
const [complete, setComplete] = useState(false);
const onError = (err)=>{
setHasError(true); // <-------------- Commenting this line avoids the error!
console.log('Value handled error',err);
};
const onComplete = ()=>{
setComplete(true);
console.log('Value handled completion');
};
const [val,setVal] = useSubject(subject,onError,onComplete);
const onClick = ()=>setVal(val+1);
return (
<span>
{val}
{hasError?' An Error ocurred ':null}
{complete?' Stream has completed ':null}
<button onClick={onClick}>increment</button>
<button onClick={()=>subject.error(5)}>Generate error</button>
<button onClick={()=>subject.complete()}>Complete</button>
</span>
);
}
Custom hook follows:
export const useSubject = (subject,onError,onComplete) => {
const [value, setValue] = useState(subject.getValue());
useEffect(() => {
const subFn = { next: data => setValue(data) };
if (onError) { subFn.error = err => onError(err); }
if (onComplete) { subFn.complete = () => onComplete(); }
const sub = subject.pipe(skip(1)).subscribe(subFn);
return () => sub.unsubscribe();
});
const newSetState = state => subject.next(state);
return [value, newSetState];
};
The error message from React is not very useful, and if I understand correctly, error boundaries would only help me display a better UI, but I'm interested in fixing the error, not containing it.
With the line that updates the state commented out, the callback works and logs to console with no issue, while if I update the state in the callback (so I can drive an UI update), it crashes.
I tried many workarounds and shifting responsabilities between hook and component, but in the end I do need the state updated in the component, which is causing the crash.
Strangely enough, completing the stream also updates the state, via a very similar callback, but that one works.
What am I doing wrong? Can it be helped or worked around?
In RxJS any error or complete-call will basically "kill" a stream.
After you setHasError(true) in your "Sample" component, it re-renders and runs this line again:
const [val, setVal] = useSubject(subject, onError, onComplete);
that hook begins with:
const [value, setValue] = useState(subject.getValue());
Where getValue() on an errored subject throws an error.

Function inside component not receiving latest version of Redux-state to quit polling

I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:
Component not receiving new props
React componentDidUpdate not receiving latest props
Redux store updates successfully, but component's mapStateToProps receiving old state
I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.
I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.
Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';
const Loader = (props) => {
const pollDatabase = (jobId, pollFunction) => {
return delay(5000)
.then(pollFunction(jobId))
.catch(err => console.log("Failed in pollDatabase function. Error: ", err))
};
const pollUntilComplete = (jobId, pollFunction) => {
return pollDatabase(jobId, pollFunction)
.then(res => {
console.log(props.loadJobCompletionStatus) // <- always null
if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
return pollUntilComplete(jobId, pollFunction);
}
})
.catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
};
const uploadHandler = () => {
...
const transferPromise = apiCall1() // Names changed to reduce code
.then(res=> {
return axios.post(api2url, res.data.id);
})
.then(postResponse=> {
return axios.put(api3url, file)
.then(()=>{
return instance.post(api3url, postResponse.data)
})
})
transferDataPromise.then((res) => {
return pollUntilComplete(res.data.job_id,
props.checkLoadTaskStatus)
})
.then(res => console.log("Task complete: ", res))
.catch(err => console.log("An error occurred: ", err))
}
return ( ...); //
const mapStateToProps = state => {
return {
datasets: state.datasets,
loadJobCompletionStatus: state.loadJobCompletionStatus,
loadJobErrorStatus: state.loadJobErrorStatus,
loadJobIsPolling: state.loadJobPollingFirestore
}
}
const mapDispatchToProps = dispatch => {
return {
checkLoadTaskStatus: (jobId) =>
dispatch(actions.loadTaskStatusInit(jobId))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);
delay.js
export const delay = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
}
actions.js
...
export const loadTaskStatusInit = (jobId) => {
return dispatch => {
dispatch(loadTaskStatusStart()); //
const docRef = firestore.collection('coll').doc(jobId)
return docRef.get()
.then(jobData=>{
const completionStatus = jobData.data().complete;
const errorStatus = jobData.data().error;
dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
},
error => {
dispatch(loadTaskStatusFail(error));
})
};
}
It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.
I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.
Can anybody point out what might be causing this?
Thanks
I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.
As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.

admin-on-rest how to implement a custom saga for auto-refresh

In the example provided in the aor-realtime readme
import realtimeSaga from 'aor-realtime';
const observeRequest = (fetchType, resource, params) => {
// Use your apollo client methods here or sockets or whatever else including the following very naive polling mechanism
return {
subscribe(observer) {
const intervalId = setInterval(() => {
fetchData(fetchType, resource, params)
.then(results => observer.next(results)) // New data received, notify the observer
.catch(error => observer.error(error)); // Ouch, an error occured, notify the observer
}, 5000);
const subscription = {
unsubscribe() {
// Clean up after ourselves
clearInterval(intervalId);
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
const customSaga = realtimeSaga(observeRequest);
fetchData function is mentioned, but it's not accessible from that scope, is it just a symbolic/abstract call ?
If I really wanted to refresh the data based on some realtime trigger how could i dispatch the data fetching command from this scope ?
You're right, it is a symbolic/abstract call. It's up to you to implement observeRequest, by initializing it with your restClient for example and calling the client methods accordingly using the fetchType, resource and params parameters.
We currently only use this saga with the aor-simple-graphql-client client

Categories