Question RE Using React Class Fields with Async/Await on Arrow Functions - reactjs

When using React's Class Fields syntax, how can I use async/await and try/catch on arrow functions for my "events" (markComplete, addTodo)?
Would it better to just use a constructor and bind the custom methods in there than using class fields and arrow functions?
class App extends Component {
state = {
todos: [],
};
// async/await here, I understand
async componentDidMount() {
try {
const res = await axios.get(
'http://jsonplaceholder.typicode.com/todos?_limit=10'
);
const data = await res.data;
this.setState({
todos: data,
});
} catch (err) {
console.error(err);
}
}
// Q: how can I use async/await and try/catch here?
markComplete = id => {
this.setState({
todos: this.state.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
}),
});
};
// Q: how can I use async await and try/catch here?
addTodo = title => {
axios
.post('http://jsonplaceholder.typicode.com/todos', {
title,
completed: false,
})
.then(res =>
this.setState({
todos: [...this.state.todos, res.data],
})
);
};
render() {
return (
<Router>...</Router>
);
}
}
export default App;

You can make the arrow function async as well.
addTodo = async title => {
try {
await axios.post('http://jsonplaceholder.typicode.com/todos', { title, completed: false, })
.then(res => this.setState({ todos: [...this.state.todos, res.data] }));
} catch(err) {
// Handle err
}
};
What you have with the returned promise from axios is fine, you can add a catch block to the promise chain.
addTodo = async title => {
await axios.post(
'http://jsonplaceholder.typicode.com/todos',
{ title, completed: false, }
)
.then(res => this.setState({ todos: [...this.state.todos, res.data] }))
.catch(err => {
// Handle err
});
};

Related

context in componentDidMount appears as null

I currently have a context provider.
componentDidMount() {
if (this.state.memberID === null) {
try {
this.checkAuthUser();
} catch (e) {
console.error(e);
}
}
}
checkAuthUser = () => {
new Promise((resolve, reject) => {
this.props.firebase.auth.onAuthStateChanged(authUser => {
if(authUser) {
resolve(authUser);
} else {
reject(new Error("Not authorized"));
}
})
})
.then( authDetails => {
this.props.firebase.getOrgID(authDetails.uid).on('value', snapshot => {
const setSnapshot = snapshot.val();
const getOrganizationID = Object.keys(setSnapshot)[0];
this.setState({ memberID: authDetails.uid, orgID: getOrganizationID })
})
})
.catch(err => console.log(err))
}
When I try to use this in another component:
static contextType = AuthDetailsContext;
componentDidMount() {
console.log('here is context: ' + this.context.orgID);
if(this.context.orgID) {
this.setState({currentOrganization: this.context.orgID, loading: true}, () => {
this.getMembersInDB('1');
})
}
}
My console.log is null. Means the context isn't registering yet. Any idea what I'm doing wrong?
Your design here seems flawed i.e. when your provider is mounted you send the API request and then when your descendant component is mounted you try to use it - these operations will happen in quick succession, far quicker than it would take for an API call to return from a server.
In your provider, if you must have a user before the component mounts then you need to delay rendering the child components until your API response completes i.e.
const AuthDetailsContext = React.createContext(null);
class AuthDetailsProvider extends PureComponent {
...
componentDidMount() {
const { firebase } = this.props;
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser) {
// Maybe set some other state state to inform the user?
this.setState({ authError: new Error('Not Authorised') });
return;
}
firebase.getOrgID(authUser.uid)
.on('value', snapshot => {
const setSnapshot = snapshot.val();
const getOrganizationID = Object.keys(setSnapshot)[0];
this.setState({
authError: null,
memberID: authUsermemberID.uid,
orgID: getOrganizationID
});
});
})
}
render() {
if (this.state.authError) return <b style={{ color: red }}>{this.state.error.message}</b>;
if (!this.state.memberID) return <b>Authenticating...</b>
return (
<AuthDetailsContext.Provider value={this.state}>
{this.props.children}
</AuthDetailsContext.Provider>
);
}
}

debounce async/await and update component state

I have question about debounce async function. Why my response is undefined? validatePrice is ajax call and I receive response from server and return it (it is defined for sure).
I would like to make ajax call after user stops writing and update state after I get reponse. Am I doing it right way?
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
this.setState(state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}), () => this.doPriceValidation());
}
doPriceValidation = debounce(async () => {
const response = await this.props.validatePrice(this.state.selected);
console.log(response);
//this.setState({ selected: res.TOE });
}, 400);
actions.js
export function validatePrice(product) {
const actionUrl = new Localization().getURL(baseUrl, 'ValidateTargetPrice');
return function (dispatch) {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST });
dispatch(showLoader());
return axios.post(actionUrl, { argModel: product }, { headers })
.then((res) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_FULFILLED, payload: res.data });
console.log(res.data); // here response is OK (defined)
return res;
})
.catch((err) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_REJECTED, payload: err.message });
})
.then((res) => {
dispatch(hideLoader());
return res.data;
});
};
}
Please find below the working code with lodash debounce function.
Also here is the codesandbox link to play with.
Some changes:-
1) I have defined validatePrice in same component instead of taking from prop.
2) Defined the debounce function in componentDidMount.
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class App extends React.Component {
state = {
selected: { Price: 10 }
};
componentDidMount() {
this.search = _.debounce(async () => {
const response = await this.validatePrice(this.state.selected);
console.log(response);
}, 2000);
}
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
console.log(value);
this.setState(
state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}),
() => this.doPriceValidation()
);
};
doPriceValidation = () => {
this.search();
};
validatePrice = selected => {
return new Promise(resolve => resolve(`response sent ${selected}`));
};
render() {
return (
<div className="App">
<input type="text" onChange={this.handleTargetPriceDayChange} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope that helps!!!
You can use the throttle-debounce library to achieve your goal.
Import code in top
import { debounce } from 'throttle-debounce';
Define below code in constructor
// Here I have consider 'doPriceValidationFunc' is the async function
this.doPriceValidation = debounce(400, this.doPriceValidationFunc);
That's it.

Redux - how to call an action and wait until it is resolved

I'm using react native + redux + redux-thunk
I do not have much experience with redux and react native
I'm calling an action inside my component.
this.props.checkClient(cliente);
if(this.props.clienteIsValid){
...
}
and within that action there is a call to an api that takes a few seconds.
export const checkClient = (cliente) => {
return dispatch => {
axios.get(`${API_HOST}/api/checkclient`, header).then(response => {
dispatch({type: CHECK_CLIENT, payload: response.data }); //valid or invalid
}).catch((error) => { });
}
}
My question is how can I delay the return of the action until the api response is completed? I need the api response to know if the client is valid or invalid. That is, I need the action to be resolved and then verify that the client is valid or invalid.
You can return a promise from the action, so that the call becomes thenable:
// Action
export const checkClient = (cliente) => {
return dispatch => {
// Return the promise
return axios.get(...).then(res => {
...
// Return something
return true;
}).catch((error) => { });
}
}
class MyComponent extends React.Component {
// Example
componentDidMount() {
this.props.checkClient(cliente)
.then(result => {
// The checkClient call is now done!
console.log(`success: ${result}`);
// Do something
})
}
}
// Connect and bind the action creators
export default connect(null, { checkClient })(MyComponent);
This might be out of scope of the question, but if you like you can use async await instead of then to handle your promise:
async componentDidMount() {
try {
const result = await this.props.checkClient(cliente);
// The checkClient call is now done!
console.log(`success: ${result}`)
// Do something
} catch (err) {
...
}
}
This does the same thing.
I don't understand the problem, but maybe this could help
export const checkClient = (cliente) => {
return dispatch => {
dispatch({type: CHECK_CLIENT_PENDING });
axios.get(`${API_HOST}/api/checkclient`, header).then(response => {
dispatch({type: CHECK_CLIENT, payload: response.data }); //valid or invalid
}).catch((error) => { });
}
}
...
this.props.checkClient(cliente);
if(this.props.clienteIsPending){
...
}
if(this.props.clienteIsValid){
...
}
I have written a full code if there is still confusion. The promise should work for a sequence of asynchronous redux action calls
Actions
export const buyBread = (args) => {
return dispatch => {
return new Promise((resolve, reject) => {
dispatch({type: BUY_BREAD_LOADING });
// or any other dispatch event
// your long running function
dispatch({type: BUY_BREAD_SUCCESS, data: 'I bought the bread'});
// or any other dispatch event
// finish the promise event
resolve();
// or reject it
reject();
});
}
export const eatBread = (args) => {
return dispatch => {
return new Promise((resolve, reject) => {
dispatch({type: EAT_BREAD_LOADING });
// or any other dispatch event
// your long running function
dispatch({type: EAT_BREAD_SUCCESS, data: 'I ate the bread'});
// or any other dispatch event
// finish the promise event
resolve();
// or reject it
reject();
});
}
Reducer
const initialState = {}
export const actionReducer = (state = initialState, payload) => {
switch (payload.type) {
case BUY_BREAD_LOADING:
return { loading: true };
case BUY_BREAD_SUCCESS:
return { loading: false, data: payload.data };
case EAT_BREAD_LOADING:
return { loading: true };
case EAT_BREAD_SUCCESS:
return { loading: false, data: payload.data };
}
Component class
import React, {Component} from 'react';
class MyComponent extends Component {
render() {
return (
<div>
<button onClick={()=>{
this.props.buyBread().then(result =>
this.props.eatBread();
// to get some value in result pass argument in resolve() function
);
}}>I am hungry. Feed me</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
actionReducer: state.actionReducer,
});
const actionCreators = {
buyBread: buyBread,
eatBread: eatBread
};
export default connect(mapStateToProps, actionCreators)(MyComponent));

How to increase axios speed?

Because I'm new to using axios so I usually have a trouble in using it. Specifically, I'm making a react-infinite-scroll feature now, but when I compare its speed with other site, my post(react-infinite-scroll feature) is gonna be shown slowly a little. Then I'm thinking this problem is caused by 2 reasons
1. I'm not using axios properly
2. There is a thing makes axios speed urgrade, but I'm not using it
Here's my code, please give me some advice to increase my http request speed.
Thank you for reading my question!
class MainPage extends Component {
constructor(props) {
super(props)
axios.get("http://127.0.0.1:8000/api/question")
.then(res => {
this.setState({
AnswerPostMultiList: res.data
})
}
)
.catch(err => {
console.log(err)
})
}
state = {
AnswerPostMultiList : []
}
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener("scroll", this.handleScroll);
}
handleScroll = () => {
console.log("scroll is executing")
const { innerHeight } = window;
const { scrollHeight } = document.body;
const scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
if (scrollHeight - innerHeight - scrollTop < 1000 && !this.props.isLoading["isLoading"]) {
this.props.onIsLoading() #To prevent this code from calling back continuously, change the value of this.props.isLoading["isLoading"] to false
axios.get("http://127.0.0.1:8000/api/question")
.then(res => {
this.setState({
AnswerPostMultiList: this.state.AnswerPostMultiList.concat(res.data)
})
this.props.onIsLoading() #change the value of this.props.isLoading["isLoading"] to true
}
)
.catch(err => {
console.log(err)
})
}
};
render() {
return(
<>
<PageHeader />
<div className="find_members">
{ this.state.AnswerPostMultiList.map((answerpost,index) => {
return <AnswerPostMulti question={answerpost.question_text} q_owner={answerpost.question_owner} answer={answerpost.answer_image} a_owner={answerpost.answer_owner} key={index} />
})
}
</div>
</>
)
}
}
const mapDispatchToProps = (dispatch) => ({
onIsLoading: () => {
dispatch(isLoadingActions.isLoading())
}
})
const mapStateToProps = state => ({
isLoading: state.isLoading
})
export default connect(mapStateToProps, mapDispatchToProps)(MainPage)
The best place to call a axios API calls is at componentDidMount(){}.
The Application loading process will be in this order skeleton->Initial rendering->later componentDidMount method is called. So here your app skeleton will be loaded and after that you can fetch data and use it to your app skeleton.
componentDidMount() {
axios.get("http://127.0.0.1:8000/api/question")
.then(res => {
this.setState({
AnswerPostMultiList: res.data
})
}
)
.catch(err => {
console.log(err)
});
}

How to cancel a fetch on componentWillUnmount

I think the title says it all. The yellow warning is displayed every time I unmount a component that is still fetching.
Console
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but ... To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
constructor(props){
super(props);
this.state = {
isLoading: true,
dataSource: [{
name: 'loading...',
id: 'loading',
}]
}
}
componentDidMount(){
return fetch('LINK HERE')
.then((response) => response.json())
.then((responseJson) => {
this.setState({
isLoading: false,
dataSource: responseJson,
}, function(){
});
})
.catch((error) =>{
console.error(error);
});
}
When you fire a Promise it might take a few seconds before it resolves and by that time user might have navigated to another place in your app. So when Promise resolves setState is executed on unmounted component and you get an error - just like in your case. This may also cause memory leaks.
That's why it is best to move some of your asynchronous logic out of components.
Otherwise, you will need to somehow cancel your Promise. Alternatively - as a last resort technique (it's an antipattern) - you can keep a variable to check whether the component is still mounted:
componentDidMount(){
this.mounted = true;
this.props.fetchData().then((response) => {
if(this.mounted) {
this.setState({ data: response })
}
})
}
componentWillUnmount(){
this.mounted = false;
}
I will stress that again - this is an antipattern but may be sufficient in your case (just like they did with Formik implementation).
A similar discussion on GitHub
EDIT:
This is probably how would I solve the same problem (having nothing but React) with Hooks:
OPTION A:
import React, { useState, useEffect } from "react";
export default function Page() {
const value = usePromise("https://something.com/api/");
return (
<p>{value ? value : "fetching data..."}</p>
);
}
function usePromise(url) {
const [value, setState] = useState(null);
useEffect(() => {
let isMounted = true; // track whether component is mounted
request.get(url)
.then(result => {
if (isMounted) {
setState(result);
}
});
return () => {
// clean up
isMounted = false;
};
}, []); // only on "didMount"
return value;
}
OPTION B: Alternatively with useRef which behaves like a static property of a class which means it doesn't make component rerender when it's value changes:
function usePromise2(url) {
const isMounted = React.useRef(true)
const [value, setState] = useState(null);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
request.get(url)
.then(result => {
if (isMounted.current) {
setState(result);
}
});
}, []);
return value;
}
// or extract it to custom hook:
function useIsMounted() {
const isMounted = React.useRef(true)
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}
Example: https://codesandbox.io/s/86n1wq2z8
The friendly people at React recommend wrapping your fetch calls/promises in a cancelable promise. While there is no recommendation in that documentation to keep the code separate from the class or function with the fetch, this seems advisable because other classes and functions are likely to need this functionality, code duplication is an anti-pattern, and regardless the lingering code should be disposed of or canceled in componentWillUnmount(). As per React, you can call cancel() on the wrapped promise in componentWillUnmount to avoid setting state on an unmounted component.
The provided code would look something like these code snippets if we use React as a guide:
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
const cancelablePromise = makeCancelable(fetch('LINK HERE'));
constructor(props){
super(props);
this.state = {
isLoading: true,
dataSource: [{
name: 'loading...',
id: 'loading',
}]
}
}
componentDidMount(){
cancelablePromise.
.then((response) => response.json())
.then((responseJson) => {
this.setState({
isLoading: false,
dataSource: responseJson,
}, () => {
});
})
.catch((error) =>{
console.error(error);
});
}
componentWillUnmount() {
cancelablePromise.cancel();
}
---- EDIT ----
I have found the given answer may not be quite correct by following the issue on GitHub. Here is one version that I use which works for my purposes:
export const makeCancelableFunction = (fn) => {
let hasCanceled = false;
return {
promise: (val) => new Promise((resolve, reject) => {
if (hasCanceled) {
fn = null;
} else {
fn(val);
resolve(val);
}
}),
cancel() {
hasCanceled = true;
}
};
};
The idea was to help the garbage collector free up memory by making the function or whatever you use null.
You can use AbortController to cancel a fetch request.
See also: https://www.npmjs.com/package/abortcontroller-polyfill
class FetchComponent extends React.Component{
state = { todos: [] };
controller = new AbortController();
componentDidMount(){
fetch('https://jsonplaceholder.typicode.com/todos',{
signal: this.controller.signal
})
.then(res => res.json())
.then(todos => this.setState({ todos }))
.catch(e => alert(e.message));
}
componentWillUnmount(){
this.controller.abort();
}
render(){
return null;
}
}
class App extends React.Component{
state = { fetch: true };
componentDidMount(){
this.setState({ fetch: false });
}
render(){
return this.state.fetch && <FetchComponent/>
}
}
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Since the post had been opened, an "abortable-fetch" has been added.
https://developers.google.com/web/updates/2017/09/abortable-fetch
(from the docs:)
The controller + signal manoeuvre
Meet the AbortController and AbortSignal:
const controller = new AbortController();
const signal = controller.signal;
The controller only has one method:
controller.abort();
When you do this, it notifies the signal:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
This API is provided by the DOM standard, and that's the entire API. It's deliberately generic so it can be used by other web standards and JavaScript libraries.
for example, here's how you'd make a fetch timeout after 5 seconds:
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000);
fetch(url, { signal }).then(response => {
return response.text();
}).then(text => {
console.log(text);
});
When I need to "cancel all subscriptions and asynchronous" I usually dispatch something to redux in componentWillUnmount to inform all other subscribers and send one more request about cancellation to server if necessary
The crux of this warning is that your component has a reference to it that is held by some outstanding callback/promise.
To avoid the antipattern of keeping your isMounted state around (which keeps your component alive) as was done in the second pattern, the react website suggests using an optional promise; however that code also appears to keep your object alive.
Instead, I've done it by using a closure with a nested bound function to setState.
Here's my constructor(typescript)…
constructor(props: any, context?: any) {
super(props, context);
let cancellable = {
// it's important that this is one level down, so we can drop the
// reference to the entire object by setting it to undefined.
setState: this.setState.bind(this)
};
this.componentDidMount = async () => {
let result = await fetch(…);
// ideally we'd like optional chaining
// cancellable.setState?.({ url: result || '' });
cancellable.setState && cancellable.setState({ url: result || '' });
}
this.componentWillUnmount = () => {
cancellable.setState = undefined; // drop all references.
}
}
I think if it is not necessary to inform server about cancellation - best approach is just to use async/await syntax (if it is available).
constructor(props){
super(props);
this.state = {
isLoading: true,
dataSource: [{
name: 'loading...',
id: 'loading',
}]
}
}
async componentDidMount() {
try {
const responseJson = await fetch('LINK HERE')
.then((response) => response.json());
this.setState({
isLoading: false,
dataSource: responseJson,
}
} catch {
console.error(error);
}
}
In addition to the cancellable promise hooks examples in the accepted solution, it can be handy to have a useAsyncCallback hook wrapping a request callback and returning a cancellable promise. The idea is the same, but with a hook working just like a regular useCallback. Here is an example of implementation:
function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
const isMounted = useRef(true)
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const cb = useCallback(callback, dependencies)
const cancellableCallback = useCallback(
(...args: any[]) =>
new Promise<T>((resolve, reject) => {
cb(...args).then(
value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
)
}),
[cb]
)
return cancellableCallback
}
one more alternative way is to wrap your async function in a wrapper that will handle the use case when the component unmounts
as we know function are also object in js so we can use them to update the closure values
const promesifiedFunction1 = (func) => {
return function promesify(...agrs){
let cancel = false;
promesify.abort = ()=>{
cancel = true;
}
return new Promise((resolve, reject)=>{
function callback(error, value){
if(cancel){
reject({cancel:true})
}
error ? reject(error) : resolve(value);
}
agrs.push(callback);
func.apply(this,agrs)
})
}
}
//here param func pass as callback should return a promise object
//example fetch browser API
//const fetchWithAbort = promesifiedFunction2(fetch)
//use it as fetchWithAbort('http://example.com/movies.json',{...options})
//later in componentWillUnmount fetchWithAbort.abort()
const promesifiedFunction2 = (func)=>{
return async function promesify(...agrs){
let cancel = false;
promesify.abort = ()=>{
cancel = true;
}
try {
const fulfilledValue = await func.apply(this,agrs);
if(cancel){
throw 'component un mounted'
}else{
return fulfilledValue;
}
}
catch (rejectedValue) {
return rejectedValue
}
}
}
then inside componentWillUnmount() simply call promesifiedFunction.abort()
this will update the cancel flag and run the reject function
Using CPromise package, you can cancel your promise chains, including nested ones. It supports AbortController and generators as a replacement for ECMA async functions. Using CPromise decorators, you can easily manage your async tasks, making them cancellable.
Decorators usage Live Demo :
import React from "react";
import { ReactComponent, timeout } from "c-promise2";
import cpFetch from "cp-fetch";
#ReactComponent
class TestComponent extends React.Component {
state = {
text: "fetching..."
};
#timeout(5000)
*componentDidMount() {
console.log("mounted");
const response = yield cpFetch(this.props.url);
this.setState({ text: `json: ${yield response.text()}` });
}
render() {
return <div>{this.state.text}</div>;
}
componentWillUnmount() {
console.log("unmounted");
}
}
All stages there are completely cancelable/abortable.
Here is an example of using it with React Live Demo
import React, { Component } from "react";
import {
CPromise,
CanceledError,
ReactComponent,
E_REASON_UNMOUNTED,
listen,
cancel
} from "c-promise2";
import cpAxios from "cp-axios";
#ReactComponent
class TestComponent extends Component {
state = {
text: ""
};
*componentDidMount(scope) {
console.log("mount");
scope.onCancel((err) => console.log(`Cancel: ${err}`));
yield CPromise.delay(3000);
}
#listen
*fetch() {
this.setState({ text: "fetching..." });
try {
const response = yield cpAxios(this.props.url).timeout(
this.props.timeout
);
this.setState({ text: JSON.stringify(response.data, null, 2) });
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
this.setState({ text: err.toString() });
}
}
*componentWillUnmount() {
console.log("unmount");
}
render() {
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{this.state.text}</div>
<button
className="btn btn-success"
type="submit"
onClick={() => this.fetch(Math.round(Math.random() * 200))}
>
Fetch random character info
</button>
<button
className="btn btn-warning"
onClick={() => cancel.call(this, "oops!")}
>
Cancel request
</button>
</div>
);
}
}
Using Hooks and cancel method
import React, { useState } from "react";
import {
useAsyncEffect,
E_REASON_UNMOUNTED,
CanceledError
} from "use-async-effect2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const [id, setId] = useState(1);
const cancel = useAsyncEffect(
function* () {
setText("fetching...");
try {
const response = yield cpAxios(
`https://rickandmortyapi.com/api/character/${id}`
).timeout(props.timeout);
setText(JSON.stringify(response.data, null, 2));
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
}
},
[id]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button
className="btn btn-success"
type="submit"
onClick={() => setId(Math.round(Math.random() * 200))}
>
Fetch random character info
</button>
<button className="btn btn-warning" onClick={cancel}>
Cancel request
</button>
</div>
);
}
Just four steps:
1.create instance of AbortController::const controller = new AbortController()
2.get signal:: const signal = controller.signal
3.pass signal to fetch parameter
4.controller abort anytime:: controller.abort();
const controller = new AbortController()
const signal = controller.signal
function beginFetching() {
var urlToFetch = "https://xyxabc.com/api/tt";
fetch(urlToFetch, {
method: 'get',
signal: signal,
})
.then(function(response) {
console.log('Fetch complete');
}).catch(function(err) {
console.error(` Err: ${err}`);
});
}
function abortFetching() {
controller.abort()
}
If you have a timeout clear them when component unmount.
useEffect(() => {
getReusableFlows(dispatch, selectedProject);
dispatch(fetchActionEvents());
const timer = setInterval(() => {
setRemaining(getRemainingTime());
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
There are many great answers here and i decided to throw some in too. Creating your own version of useEffect to remove repetition is fairly simple:
import { useEffect } from 'react';
function useSafeEffect(fn, deps = null) {
useEffect(() => {
const state = { safe: true };
const cleanup = fn(state);
return () => {
state.safe = false;
cleanup?.();
};
}, deps);
}
Use it as a normal useEffect with state.safe being available for you in the callback that you pass:
useSafeEffect(({ safe }) => {
// some code
apiCall(args).then(result => {
if (!safe) return;
// updating the state
})
}, [dep1, dep2]);
This is a more general solution for async/await and promises.
I did this because my React callbacks were in between important async calls, so I couldn't cancel all the promises.
// TemporalFns.js
let storedFns = {};
const nothing = () => {};
export const temporalThen = (id, fn) => {
if(!storedFns[id])
storedFns[id] = {total:0}
let pos = storedFns[id].total++;
storedFns[id][pos] = fn;
return data => { const res = storedFns[id][pos](data); delete storedFns[id][pos]; return res; }
}
export const cleanTemporals = (id) => {
for(let i = 0; i<storedFns[id].total; i++) storedFns[id][i] = nothing;
}
Usage: (Obviously each instance should have different id)
const Test = ({id}) => {
const [data,setData] = useState('');
useEffect(() => {
someAsyncFunction().then(temporalThen(id, data => setData(data))
.then(otherImportantAsyncFunction).catch(...);
return () => { cleanTemporals(id); }
}, [])
return (<p id={id}>{data}</p>);
}
we can create a custom hook to wrap the fetch function like this:
//my-custom-fetch-hook.js
import {useEffect, useRef} from 'react'
function useFetch(){
const isMounted = useRef(true)
useEffect(() => {
isMounted.current = true //must set this in useEffect or your will get a error when the debugger refresh the page
return () => {isMounted.current = false}
}, [])
return (url, config) => {
return fetch(url, config).then((res) => {
if(!isMounted.current)
throw('component unmounted')
return res
})
}
}
export default useFetch
Then in our functional component:
import useFetch from './my-custom-fetch-hook.js'
function MyComponent(){
const fetch = useFetch()
...
fetch(<url>, <config>)
.then(res => res.json())
.then(json => { ...set your local state here})
.catch(err => {...do something})
}
I think I figured a way around it. The problem is not as much the fetching itself but the setState after the component is dismissed. So the solution was to set this.state.isMounted as false and then on componentWillMount change it to true, and in componentWillUnmount set to false again. Then just if(this.state.isMounted) the setState inside the fetch. Like so:
constructor(props){
super(props);
this.state = {
isMounted: false,
isLoading: true,
dataSource: [{
name: 'loading...',
id: 'loading',
}]
}
}
componentDidMount(){
this.setState({
isMounted: true,
})
return fetch('LINK HERE')
.then((response) => response.json())
.then((responseJson) => {
if(this.state.isMounted){
this.setState({
isLoading: false,
dataSource: responseJson,
}, function(){
});
}
})
.catch((error) =>{
console.error(error);
});
}
componentWillUnmount() {
this.setState({
isMounted: false,
})
}

Resources