Testing State changes in BLoC architecture - reactjs

I'm relatively new to testing and I can happily test UI components that use renders, but I'm struggling to figure out the correct way to test logic components in my current preferred architecture method.
I'm using the BLoC architecture method with RxJS as I much prefer using this to other state management libraries.
I've tried to simply import the functions or access them like I would do in the components and mock them but I can't seem to actually properly simulate the functions.
Here is the an example of the BLoC:
import { IHomeScreenService } from 'actions/interface'
import delivery from 'delivery'
import { ProtoBloc } from '../ProtoBloc'
import { IHomeScreenState } from './stateModel'
const defaultHomeScreenState: IHomeScreenState = {
categoriesData: [],
mealsData: null,
isLoaded: false,
mealsLoading: false,
currentCategory: 'Beef'
}
class HomeScreenBloc extends ProtoBloc<IHomeScreenState> implements IHomeScreenService {
constructor() {
super(defaultHomeScreenState)
}
initialLoad = async () => {
await this.getCategories()
// Set a default category for initial load
await this.getRecipesByCategory(this.state.currentCategory)
this.setLoader()
}
getCategories = async () => {
const { value, error } = await delivery.MealAPI.getCategories()
const newState = { ...this.state }
if (error) {
console.warn('Request Failed')
return
}
if (value) {
newState.categoriesData = value.categories
this.pushState(newState)
}
}
getRecipesByCategory = async (category: string) => {
const { value, error } = await delivery.MealAPI.getRecipesByCategory(category)
const newState = { ...this.state }
if (error) {
console.warn('Request Failed')
return
}
if (value) {
newState.mealsData = value.meals
newState.currentCategory = category
this.pushState(newState)
}
}
private setLoader = () => {
this.pushState({
...this.state,
isLoaded: true,
})
}
}
export default new HomeScreenBloc()
And here is my test file for this BLoC:
import React from 'react'
import HomeScreenBloc from './index'
let defaultState
HomeScreenBloc.getCategories = jest.fn()
beforeEach(() => {
defaultState = {
categoriesData: [],
mealsData: null,
isLoaded: false,
mealsLoading: false,
currentCategory: 'Beef'
}
})
test('should get default state', () => {
expect(HomeScreenBloc.defaultState).toEqual(defaultState)
})
test('it should set the categories state', () => {
HomeScreenBloc.getCategories()
const newState = HomeScreenBloc.getSubject()
expect(HomeScreenBloc.getCategories).toBeCalled()
console.log(newState)
})
Currently all tests are passing, but what I wan't to be able to check is too see if the state is changing as expected. I also don't wan't to test the API calls in this function as I'll be testing those in the delivery layer.
Any help is very much appreciated, I've been banging my head against this for a while and can't find any resources online that relate to this architecture method.

Related

React,js onclick custom hook

Hi im a programming student and im trying to build a login/register page. and ive written a custom hook to get data of the user/ post data to login or register.
and my problem is that react only accepts custom hooks inside react components or other custom hooks and i want to post the login/register on button click.
whats the best why to use custom hooks onClick?
Custom useFetch hook
import React, { useState, useEffect } from "react";
import axios from "axios";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
const useFetch = () => {
const [data, setData] = useState([]);
const history = useHistory();
let initialBizCardArray = [];
useEffect(() => {
(async () => {
try {
let currLocation = history.location.pathname;
switch (currLocation) {
case (currLocation = "/my-cards"):
case (currLocation = "/cards"):
{
let { data } = await axios.get(`/cards${currLocation}`);
initialBizCardArray = data;
setData(initialBizCardArray);
}
break;
case (currLocation = "/login"):
case (currLocation = "/register"):
{
let { data } = await axios.post(`/users${currLocation}`);
initialBizCardArray = data;
setData(initialBizCardArray);
}
break;
}
} catch (error) {
toast.error(error, {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
return { error };
}
})();
}, []);
return { data, setData, initialBizCardArray };
};
export default useFetch;
How i tried using the useFetch
const handleSubmitLogIn = async (ev) => {
ev.preventDefault();
ValidateErr(
{
email: loginInput.email,
password: loginInput.password,
},
loginSchema
);
try {
let { data } = await useFetch(userLocation, {
email: loginInput.email,
password: loginInput.password,
});
console.log("Succuss");
localStorage.setItem("token", data.token);
autoLoginFunction(data.token);
setTimeout(() => {
let userInfo = jwt_decode(data.token);
userInfo && userInfo.biz
? history.push("/my-cards")
: history.push("/");
}, 100);
} catch (err) {
console.error("error", err.response.data);
toast.error(`😭 Email or password are invalid.`, err, {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
progress: undefined,
});
history.push("/login");
}
You could replace your useEffect hook within useFetch by a method, that way you can return it and call it in your event handler :
import React, { useState } from "react";
import axios from "axios";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
const useFetch = () => {
const [data, setData] = useState([]);
const history = useHistory();
const fetchData = async () => {
try {
let currLocation = history.location.pathname;
switch (currLocation) {
case (currLocation = "/my-cards"):
case (currLocation = "/cards"):
{
let { data } = await axios.get(`/cards${currLocation}`);
setData(data);
}
break;
case (currLocation = "/login"):
case (currLocation = "/register"):
{
let { data } = await axios.post(`/users${currLocation}`);
setData(data);
}
break;
}
} catch (error) {
toast.error(error, {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
}
return { data, fetchData };
};
export default useFetch;
Then you would use it like this within your component :
import React, {useEffect} from "react"
const Component = () => {
const { data,fetchData} = useFetch()
useEffect(() => {
fetchData()
},[])
return (
<div>{data.property}</div>
)
}
Some points
Avoid using location string to decide which api call to make as it makes the code fragile, i.e. it has many reasons to break, like route name change and every time a new route is added/changed there is a possiblity that someone can break other parts of the code block
try just create a api function and export them
export const loginRegisterApi = async (email, password) => {
//network call with axios instance or anything you
// error checks
return response.data
}
use libraries like React query which make server related state management a lot more easier
cont LoginComponent = ({ ... }) => {
const { mutate, ... } = useMutation({
mutationFn: loginRegisterApi,
onSuccess: () => {
// Invalidate and refetch
// queryClient.invalidateQueries({ queryKey: ['userProfile'] })
},
})
return (
<button
onClick={() => {
// validations and checks
mutate({
{
email: loginInput.email,
password: loginInput.password,
},
})
}}
>
Login
</button>
...
)
}
If you expect the login and register requirments will change and code becomes complex, its could be better to seperate them as changing one could break the other, and login is used more often then register
Consider these as points for discussion, hope they help in someway

Alternative useState in react-native class component

i'm aware i cant use useState in class components, but there's this tutorial i'm trying to relpicate in my App. They used a functional component unlike me.
Their App.js in the tutorial is like this:
const App = () => {
useEffect(()=>{
fcmService.registerAppWithFCM();
fcmService.register(onRegister, onNotification, onOpenNotification);
localNotificationService.configure(onOpenNotification)
},[])
const onRegister = (token) => {
console.log("[App] Token", token);
}
const onNotification = (notify) => {
// console.log("[App] onNotification", notify);
const options = {
soundName: 'default',
playSound: true,
}
localNotificationService.showNotification(
0,
notify.notification.title,
notify.notification.body,
notify,
options,
)
}
const onOpenNotification = async (notify) => {
console.log('notify', notify);
}
So basically, const onOpenNotification, const onRegister, const onNotification are called in another file imported into App.js. I tried adding them in my App.js like :
class App extends Component {
componentDidMount() {
fcmService.registerAppWithFCM();
fcmService.register(onRegister, onNotification, onOpenNotification);
localNotificationService.configure(onOpenNotification)
const onRegister = (token) => {
console.log("[App] Token", token);
}
const onNotification = (notify) => {
// console.log("[App] onNotification", notify);
const options = {
soundName: 'default',
playSound: true,
}
localNotificationService.showNotification(
0,
notify.notification.title,
notify.notification.body,
notify,
options,
)
}
const onOpenNotification = async (notify) => {
console.log('notify', notify);
}
}
}
My console.log shows these errors:
LOG [FCMService] getInitialNotification getInitialNotification null
LOG [LocalNotificationService] onRegister: {"os": "android", "token": "emA0hq4KCMq0j:APA91bEWbOUXjxdIs_s2ksSbjwxhdMVfr35y9sZBUIYX72Q9obU7daQw4zI-a0qn6KsvxWvGtQoEdPlTq5l98trb-yhmtARDcliqAayi_r0K8f_"}
LOG [FCMService] getToken Rejected [TypeError: onRegister is not a function. (In 'onRegister(fcmToken)', 'onRegister' is undefined)]
I'm guessing that's because the const functions() are not properly structured, Whats the best way to replicate this tutorial, preferably if I don't have to change to a functional component
I don't think I would be able to put all the information you might need to help here, but I would appreciate if you could take some time to see how the 2 imported files I talked about are structured and how they call the const onRegister in App.js
Thank you!
EDIT: Updated Code
Try moving the methods to class level.
class App extends Component {
componentDidMount() {
fcmService.registerAppWithFCM();
fcmService.register(this.onRegister, this.onNotification, this.onOpenNotification);
localNotificationService.configure(this.onOpenNotification)
}
onRegister = (token) => {
console.log("[App] Token", token);
}
onNotification = (notify) => {
// console.log("[App] onNotification", notify);
const options = {
soundName: 'default',
playSound: true,
}
localNotificationService.showNotification(0,notify.notification.title,
notify.notification.body, notify, options,)
}
onOpenNotification = async (notify) => {
console.log('notify', notify);
}
}

Sharing and updating a variable between classes in React / React Native that aren't parent/child

I have a variable that I want to keep track of and update its value between two classes. In one of my classes, I started using props like this with the variable isLoading in my Post class:
class Post extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false
};
}
post = () => {
this.props.uploadPost()
this.props.updatePhoto()
this.props.updateDescription('')
this.props.navigation.navigate('Home')
}
openLibrary = async () => {
const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL)
if (status === 'granted') {
const image = await ImagePicker.launchImageLibraryAsync()
if(!image.cancelled ){
this.setState({ isLoading: true });
const resize = await ImageManipulator.manipulateAsync(image.uri, [], { format: 'jpeg', compress: 0.1 })
const url = await this.props.uploadPhoto(resize.uri)
this.props.updatePhoto(url)
this.setState({ isLoading: false });
}
}
}
...
Now, I also have another class called Camera that I want to update this same variable. However, I'm not implementing a child like function where I call Post or Camera class in each other.
This is my code for Camera.
class CameraUpload extends React.Component {
state = {
type: Camera.Constants.Type.back,
};
snapPhoto = async () => {
const { status } = await Camera.requestPermissionsAsync();
if (status === 'granted') {
const image = await this.camera.takePictureAsync()
global.config.loading = true;
image ? this.props.navigation.navigate('Post') : null
if( !image.cancelled ){
const resize = await ImageManipulator.manipulateAsync(image.uri, [], { format: 'jpeg', compress: 0.1 })
const url = await this.props.uploadPhoto(resize.uri)
this.props.updatePhoto(url)
loading = false;
// url ? this.props.navigation.navigate('Post') : null
}
}
}
I tried using a global config variable but the variable's value was not getting updated between classes. Please let me know what the best way to go about solving this problem is. Thanks!
React Context
You can use the concept of "Context" in react. You may read about it here
https://reactjs.org/docs/context.html
Async Storage
Also you can use async storage if it suits your design
You can make one utility class:
import AsyncStorage from '#react-native-community/async-storage';
export async function storeData(key, value) {
try {
await AsyncStorage.setItem(key, value)
} catch (e) {
console.log("Error Storing in AsyncStorage stroing " + key + " in async Storage")
}
}
export async function getData(key) {
try {
const value = await AsyncStorage.getItem(key)
return (value == null || value == undefined) ? undefined : value
} catch (e) {
console.log("Error Reading in AsyncStorage stroing " + key + " in async Storage")
}
}
You can store and get data through key-value pairs.
// Firstly import it in your js
import * as asyncStorage from './AsyncStorage'
//For storingthe data:
await asyncStorage.storeData("key1", "value1");
// For getting the data:
await asyncStorage.getData("key1")
You can try global variable:
class post {
onpost(){
global.isLoading = true;
}
}
class cameraupload {
componentDidMount(){
global.isLoading = false;
}
}
Context is the way to go for small pieces of shared state, in my opinion.
Combined with hooks, it's very easy to access state and call functions from any child component.
Define your provider with any shared state and functions
import React, { createContext, useState } from 'react'
const Context = createContext()
const MySharedStateProvider = ({ children }) => {
const [myState, setMyState] = useState('hello')
const updateMyState = value => setMyState(value)
return (
<Context.Provider value={{ myState, updateMyState }}>
{children}
</Context.Provider>
)
}
export { MySharedStateProvider, Context as MySharedStateContext }
Create a hook for your Context, that can be used in any child component
import { useContext } from 'react'
import { MySharedStateContext } from './MySharedStateProvider.js'
export const useMySharedState = () => useContext(MySharedStateContext)
Wrap your components with the provider (I know it wouldn't look like this, it's just an example)
<MySharedStateProvider>
<Posts/>
<CameraUpload/>
</MySharedStateProvider>
Now in your Post or CameraUpload component, you use your hook to get the values
import { useMySharedState } from './MySharedStateHook.js'
const Post = () => {
const { myState, setMyState } = useMySharedState()
}

How to test a React component having async functions with jest & enzyme

I have a component:
RandomGif.js
import React, { Component } from "react";
import Gif from "./Gif";
import Loader from "./library/Loader";
import { fetchRandom } from "../resources/api";
class RandomGif extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
state = {
loading: false,
gif: null
};
componentDidMount() {
this.handleClick();
}
async handleClick() {
let gifContent = null;
try {
this.setState({
loading: true
});
const result = await fetchRandom();
if (!!result && result.data) {
gifContent = {
id: result.data.id,
imageUrl: result.data.images.downsized_large.url,
staticImageUrl: result.data.images.downsized_still.url,
title: result.data.title
};
}
} catch (e) {
console.error(e);
} finally {
this.setState({
loading: false,
gif: gifContent
});
}
}
render() {
const { gif, loading } = this.state;
const showResults = gif && !loading;
return (
<div className="random">
{!showResults && <Loader />}
<button className="btn" onClick={this.handleClick}>
RANDOMISE
</button>
{showResults && <Gif data={gif} />}
</div>
);
}
}
export default RandomGif;
If I call methods directly from the instance of this component, I can successfully test that the state is being updated. However, If I simulate a button click, nothing gets updated and the test fails. I've tried setImmediate and setTimeout tricks but those are not working.
So far I've not able to write a test case for:
Simulating button click.
Simulating lifecycle method.
This is what I've come up with so far.
RandomGif.spec.js
import React from "react";
import { shallow, mount } from "enzyme";
import RandomGif from "./RandomGif";
describe("Generate Random Gif", () => {
it("should render correctly.", () => {
const wrapper = shallow(<RandomGif />);
expect(wrapper).toMatchSnapshot();
});
it("should load a random GIF on calling handleSearch fn.", async () => {
const wrapper = mount(<RandomGif />);
const instance = wrapper.instance();
expect(wrapper.state("gif")).toBe(null);
await instance.handleClick();
expect(wrapper.state("gif")).not.toBe(null);
});
it("THIS TEST FAILS!!!", () => {
const wrapper = mount(<RandomGif />);
expect(wrapper.state("gif")).toBe(null);
wrapper.find('button').simulate('click');
wrapper.update()
expect(wrapper.state("gif")).not.toBe(null);
});
});
api.py
export const fetchRandom = async () => {
const url = `some_url`;
try {
const response = await fetch(url);
return await response.json();
} catch (e) {
console.error(e);
}
return null;
};
Please help me figure out the missing pieces of a puzzle called 'frontend testing'.
We need to mock fetchRandom so no real request will be sent during testing.
import { fetchRandom } from "../resources/api";
jest.mock("../resources/api"); // due to automocking fetchRandom is jest.fn()
// somewhere in the it()
fetchRandom.mockReturnValue(Promise.resolve({ data: { images: ..., title: ..., id: ...} }))
Since mocking is a Promise(resolved - but still promise) we need either setTimeout or await <anything> to make component's code realized this Promise has been resolved. It's all about microtasks/macrotasks queue.
wrapper.find('button').simulate('click');
await Promise.resolve();
// component has already been updated here
or
it("test something" , (done) => {
wrapper.find('button').simulate('click');
setTimeout(() => {
// do our checks on updated component
done();
}); // 0 by default, but it still works
})
Btw you've already did that with
await instance.handleClick();
but to me it looks the same magic as say
await 42;
And besides it works(look into link on microtasks/macrotasks) I believe that would make tests worse readable("what does handleClick return that we need to await on it?"). So I suggest use cumbersome but less confusing await Promise.resolve(); or even await undefined;
Referring to state and calling instance methods directly are both anti-patterns. Just a quote(by Kent C. Dodds I completely agree with):
In summary, if your test uses instance() or state(), know that you're testing things that the user couldn't possibly know about or even care about, which will take your tests further from giving you confidence that things will work when your user uses them.
Let's check rendering result instead:
import Loader from "./library/Loader";
...
wrapper.find('button').simulate('click');
expect(wrapper.find(Loader)).toHaveLength(1);
await Promise.resolve();
expect(wrapper.find(Loader)).toHaveLength(1);
expect(wrapper.find(Gif).prop("data")).toEqual(data_we_mocked_in_mock)
Let's get that altogether:
import {shallow} from "enzyme";
import Gif from "./Gif";
import Loader from "./library/Loader";
import { fetchRandom } from "../resources/api";
jest.mock( "../resources/api");
const someMockForFetchRandom = { data: { id: ..., images: ..., title: ... }};
it("shows loader while loading", async () => {
fetchRandom.mockReturnValue(Promise.resolve(someMockForFetchRandom));
const wrapper = shallow(<RandomGif />);
expect(wrapper.find(Loader)).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find(Loader)).toHaveLength(1);
await Promise.resolve();
expect(wrapper.find(Loader)).toHaveLength(0);
});
it("renders images up to response", async () => {
fetchRandom.mockReturnValue(Promise.resolve(someMockForFetchRandom));
const wrapper = shallow(<RandomGif />);
wrapper.find('button').simulate('click');
expect(wrapper.find(Gif)).toHaveLength(0);
await Promise.resolve();
expect(wrapper.find(Gif).props()).toEqual( {
id: someMockForFetchRandom.data.id,
imageUrl: someMockForFetchRandom.data.images.downsized_large.url,
staticImageUrl: someMockForFetchRandom.data.images.downsized_still.url,
title: someMockForFetchRandom.data.title
});
});

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