How can i intercept an on-click asynchronously - javascript

I have a component with a button which handles some stuff, i want to pass an interceptor to this component so i can call an API inside the interceptor and ask for permission, if permission is granted the code inside the component's button's onClick is executed and if not, well it is not
So right now i'm having trouble figuring out what to do, here is a sudo code showing what i want to do:
//Inside componentA which is using componentB
onClickInterceptor = () => {
axios.post(//something)
.then(response => {
// do a few thing and finally you know if you should return true or not
})
.catch(error => {
//you know you don't have permission
})
return //This is my problem, i don't know what to return, i don't want to return the axios post, i want something like a new promise ?
}
//Inside componentB
onButtonClick = (event) => {
if (this.props.onClickInterceptor) {
this.setState({ disableButton: true })
this.props.onClickInterceptor()
.then(permissionState) => {
if (permissionState) {
this.runTheMainCode()
}
else {
//dont do anything
}
this.setState({ disableButton: false })
}
}
else
this.runTheMainCode()
}
this.runTheMainCode() {
//...
}
Right now i don't know what to return inside onClickInterceptor, i know i don't want to to return the axios, but how can i return a promise like that which only returns true or false ?
By the way, i want the button to be disabled until the interceptor is done

You need to return the axios promise and handle it at the componentB
//Inside componentA which is using componentB
onClickInterceptor = () => {
return axios.post(//something)
.then(response => {
// you can return false or true here
return true
})
.catch(error => {
throw error
})
}
//Inside componentB
onButtonClick = (event) => {
this.props.onClickInterceptor.then(returnedBoolean=>{
// here will come what you returned
this.setState({ disableButton: returnedBoolean })
}).catch(err=>{
// the error hadnling ....
this.setState({ error: true })
})
}

Related

How do I set state from within a fetch in ReactJS?

I have the following component class:
import React, { Component } from "react";
import Form from "react-bootstrap/Form";
import Button from 'react-bootstrap/Button'
import './login.css';
export default class Login extends Component {
handleSubmit = (e) => {
var myRes = null;
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
(result) => {
myRes = {
error: null,
loaded: true,
user: result
};
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
myRes = {
error: error,
loaded: true,
user: {}
};
}
);
this.setState(myRes);
}
render() {
return (
<div className="auth-wrapper">
<div className="auth-inner">
<Form onSubmit={this.handleSubmit}>
<h3>Sign In</h3>
.
.
.
<Button variant="primary" type="submit">Submit</Button>
</Form>
</div>
</div>
);
}
}
I have searched for the answer, but what I got was in (result) => {this.setState({error: null, loaded: true, user: result})}. Unfortunately the this is undefined within the fetch.
I want to in the result and error to set the value in state. Unfortunately the this is not defined within the fetch result. How do I set the state in Login from within the fetch?
The problem is you're calling setState too soon. You need to call it only when your promise has settled. The easiest way to do that is with a subsequent then, see *** comments:
handleSubmit = (e) => {
// *** No `let myRes` here
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
(result) => {
// *** Transform the resolution value slightly
return {
error: null,
loaded: true,
user: result
};
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
// *** Turn rejection into resolution by returning
// a replacement for `myRes`.
return {
error: error,
loaded: true,
user: {}
};
}
)
.then(myRes => {
// *** You always end up here because both fulfillment and rejecction
// result in an object that's used to fulfill the promise
// created by the first call to `then`
this.setState(myRes);
})
// *** Still use a final handler to catch errors from
// fulfillment handlers
.catch(error => {
// Handle/report error
});
};
first of all fetch should always be async
second put const that = this; in the top of the handler and then you can do setState like this:
handleSubmit = (e) => {
const self = this
var myRes = null;
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
result => {
self.setState({error: null, loaded: true, user: result})
myRes = {
error: null,
loaded: true,
user: result
};
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
myRes = {
error: error,
loaded: true,
user: {}
};
}
);
}
and advise you to create a file for the fetch like this:
const nameYouDecided = async () -> {
await fetch("/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh" )
.then((response) => response.json())
}
and then when you call it in your code it is shorter
In addition if you have a couple of get request you should do the following
in different file like get.js
const get = async (url) => (
await fetch(url)
.then(response => response.json())
.then(json => json
)
);
const nameYouDecided = get("/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh")
You need to set the state inside of .then() fetch function.
Fetch need time to fetch urls, setstate doesnt wait this time so it set a Promise. To turn around this, you need to put your setState inside a .then, the setState line will only be executed when your fetch done the job
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
(result) => {
return {
error: null,
loaded: true,
user: result
};
},
(error) => {
return {
error: error,
loaded: true,
user: {}
};
}
).then(myRes => {
this.setState(myRes);
});
}

How do I update an object state in react via hooks

This is a simple question. How do I successfully update state object via react hooks?
I just started using hooks, and I like how it allows to use the simple and pure JavaScript function to create and manage state with the useState() function, and also, make changes that affect components using the useEffect() function, but I can't seem to make update to the state work!
After making a request to an API, it return the data needed, but when I try to update the state for an error in request and for a successful request, it does not update the state. I logged it to the browser console, but no change was made to the state, it returns undefined.
I know that I'm not doing something right in the code.
Here is my App component, Its a single component for fetching and updating:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
export default function App() {
// Set date state
const [data,setData] = useState({
data: [],
loaded: false,
placeholder: 'Loading'
});
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
SetData({placeholder: 'Something went wrong'});
}
response.json()
})
.then(result => {
console.log(data);
setData({data: result});
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
There are a few things you can improve:
the react-hook useState does not behave like the class counterpart. It does not automatically merge the provided object with the state, you have to do that yourself.
I would recommend if you can work without an object as your state to do so as this can reduce the amount of re-renders by a significant amount and makes it easier to change the shape of the state afterwards as you can just add or remove variables and see all the usages immediately.
With a state object
export default function App() {
// Set date state
const [data,setData] = useState({
data: [],
loaded: false,
placeholder: 'Loading'
});
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText); // Goto catch block
}
return response.json(); // <<- Return the JSON Object
})
.then(result => {
console.log(data);
setData(oldState => ({ ...oldState, data: result})); // <<- Merge previous state with new data
})
.catch(error => { // Use .catch() to catch exceptions. Either in the request or any of your .then() blocks
console.error(error); // Log the error object in the console.
const errorMessage = 'Something went wrong';
setData(oldState=> ({ ...oldState, placeholder: errorMessage }));
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
Without a state object
export default function App() {
const [data, setData] = useState([]);
const [loaded, setLoaded] = useState(false);
const [placeholder, setPlaceholder] = useState('Loading');
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText); // Goto catch block
}
return response.json(); // <<- Return the JSON Object
})
.then(result => {
console.log(data);
setData(data);
})
.catch(error => { // Use .catch() to catch exceptions. Either in the request or any of your .then() blocks
console.error(error); // Log the error object in the console.
const errorMessage = 'Something went wrong';
setPlaceholder(errorMessage);
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
The correct way to update an Object with hooks it to use function syntax for setState callback:
setData(prevState => {...prevState, placeholder: 'Something went wrong'})
Following method will override your previous object state:
setData({placeholder: 'Something went wrong'}); // <== incorrect
Your final code should look like this:
.then(response => {
if (response.status !== 200) {
setData(prevObj => {...prevObj, placeholder: 'Something went wrong'});
}
return response.json()
})
.then(result => {
setData(prevObj => {...prevObj, data: result});
});

Delay componentDidMount fetch for this.props to be read

I am fetching data from within componentDidMount as
this.setState({ isLoading: true });
fetch(
`https://api.example.com/location/12345`
)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }));
and this works absolutely fine. But if I want to replace https://api.example.com/location/12345 with https://api.example.com/location/${this.props.id} to allow the id to change I get back errors that the data does not exist.
This is clearly because the fetch inside componentDidMount is fetching the url before before the this.props.id is read.
How can I delay the fetch until this.props.id is available?
One way is, use componentDidUpdate lifecycle method to get the data whenever component receive new id, but make sure to compare the prev id value with new id value and do the call only when they are not same.
Like this:
componentDidUpdate(prevProps) {
if(this.props.id && (prevProps.id != this.props.id)) {
this._getData();
}
}
_getData(){
this.setState({ isLoading: true });
fetch(
`https://api.example.com/location/${this.props.id}`
)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }));
}
I use this pattern quite often :
initFromProps(props: MyComponentProps) {
const { } = props;
}
componentWillMount() {
this.initFromProps(this.props);
}
componentWillReceiveProps(nextProps: MyComponentProps) {
this.initFromProps(nextProps);
}
This ensures your component does whatever is necessary when the props change, but also at startup. Then, in initFromProps, you can do something along the lines of this:
initFromProps(props: MyComponentProps) {
const { id } = props;
if (id !== this.props.id) {
this._getData(id);
}
}

Promise not being executed in Jest test

I'm currently creating a new React component for one of our projects and I'm pretty much stuck with writing a proper test for it. I've read quite a few docs and blog posts and more but I can't seem to get it running.
TL;DR
To me, it seems the Promise is not executed. When I run the test with a debugger, it won't stop in the Promise's function and neither in the then() function. It will, however, stop in the then/catch functions in the test itself.
The Code
So, the component is actually fairly simple. For the time being it is supposed to search for a location via an API. The test for it looks like this:
import axios from 'axios';
import React from 'react';
import {shallowWithIntl} from "../../../Helpers/react-intl-helper";
import Foo from "../../../../src/Components/Foo/Foo";
import {mount} from "enzyme";
const queryTerm = 'exampleQueryTerm';
const locationAgs = 'exampleLocationKey';
const fakeLocationObject = {
search: '?for=' + queryTerm + '&in=' + locationAgs
};
jest.mock('axios', () => {
const exampleLocations = [{
data: {"id": "expected-location-id"}
}];
return {
get: jest.fn().mockReturnValue(() => {
return Promise.resolve(exampleLocations)
})
};
});
let fooWrapper, instance;
beforeEach(() => {
global.settings = {
"some-setting-key": "some-setting-value"
};
global.URLSearchParams = jest.fn().mockImplementation(() => {
return {
get: function(param) {
if (param === 'for') return queryTerm;
else if (param === 'in') return locationAgs;
return '';
}
}
});
fooWrapper = shallowWithIntl(<Foo location={fakeLocationObject} settings={ global.settings } />).dive();
instance = fooWrapper.instance();
});
it('loads location and starts result search', function() {
expect.assertions(1);
return instance
.searchLocation()
.then((data) => {
expect(axios.get).toHaveBeenCalled();
expect(fooWrapper.state('location')).not.toBeNull();
})
.catch((error) => {
expect(fooWrapper.state('location')).toBe(error);
});
});
So, as you can see the test is supposed to call searchLocation on the Foo component instance, which returns a Promise object, as you can (almost) see in its implementation.
import React, { Component } from 'react';
import { injectIntl } from "react-intl";
import {searchLocationByKey} from "../../Services/Vsm";
class Foo extends Component {
constructor(props) {
super(props);
this.state = {
location: null,
searchingLocation: false,
searchParams: new URLSearchParams(this.props.location.search)
};
}
componentDidUpdate(prevProps) {
if (!prevProps.settings && this.props.settings) {
this.searchLocation();
}
}
searchLocation() {
this.setState({
searchingLocation: true
});
const key = this.state.searchParams.get('in');
return searchLocationByKey(key)
.then(locations => {
this.setState({ location: locations[0], searchingLocation: false })
})
.catch(error => console.error(error));
}
render() {
// Renders something
};
}
export default injectIntl(Foo);
Enter searchLocationByKey:
function requestLocation(url, resolve, reject) {
axios.get(url).then(response => {
let locations = response.data.map(
location => ({
id: location.collectionKey || location.value,
rs: location.rs,
label: location.label,
searchable: location.isSearchable,
rawData: location
})
);
resolve(locations);
}).catch(error => reject(error));
}
export const searchLocationByKey = function(key) {
return new Promise((resolve, reject) => {
let url = someGlobalBaseUrl + '?regional_key=' + encodeURIComponent(key);
requestLocation(url, resolve, reject);
});
};
The Problem
This is the output of the test:
Error: expect(received).toBe(expected)
Expected value to be (using ===):
[Error: expect(received).not.toBeNull()
Expected value not to be null, instead received
null]
Received:
null
I have to admit that I'm pretty new to Promises, React and JavaScript testing, so I might have mixed up several things. As I wrote above, it seems that the Promise is not executed properly. When debugging, it will not stop in the then() function defined in Foo.searchLocation. Instead, apparently, both the then() and catch() functions defined in the test are executed.
I've spent way too much time on this issue already and I'm clueless on how to go on. What am I doing wrong?
Update 1: done() function
As El Aoutar Hamza pointed out in an answer below, it is possible to pass a function (usually called "done") to the test function. I've done exactly this:
it('loads location and starts result search', function(done) {
expect.assertions(1);
return instance
.searchLocation()
.then((data) => {
expect(fooWrapper.state('location')).not.toBeNull();
done();
})
.catch((error) => {
expect(fooWrapper.state('location')).toBe(error);
});
});
But I end up getting this error:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Inside requestLocation you are trying to access response.data, and when mocking axios.get, you are returning a Promise resolved with an array ! you should instead return a Promise resolved with an object with data property (that contains the array).
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({
data: [{ "id": "expected-location-id" }]
}))
}));
Another point is that when testing asynchronous code, the test will finish before even calling the callbacks, that's why you should consider providing your test an argument called done, that way, jest will wait until the done callback is called.
describe('Foo', () => {
it('loads location and starts result search', done => {
expect.assertions(1);
return instance
.searchLocation()
.then((data) => {
expect(fooWrapper.state('location')).not.toBeNull();
done();
})
.catch((error) => {
expect(fooWrapper.state('location')).toBe(error);
done();
});
});
});
So like I mentioned in my latest comment under El Aoutar Hamza's answer, I have found a solution thanks to a colleague who was able to help me.
It seems that it is not possible to return the Promise from Foo.searchLocation on to the test. What we needed to do was to wrap the code getting and handling the Promise from searchLocationByKey into yet another Promise, which looks like this:
import React, { Component } from 'react';
import { injectIntl } from "react-intl";
import {searchLocationByKey} from "../../Services/Vsm";
class Foo extends Component {
constructor(props) {
super(props);
this.state = {
location: null,
searchingLocation: false,
searchParams: new URLSearchParams(this.props.location.search)
};
}
componentDidUpdate(prevProps) {
if (!prevProps.settings && this.props.settings) {
this.searchLocation();
}
}
searchLocation() {
this.setState({
searchingLocation: true
});
const key = this.state.searchParams.get('in');
return new Promise((resolve, reject) => {
searchLocationByKey(key)
.then(locations => {
this.setState({ location: locations[0], searchingLocation: false });
resolve();
})
.catch(error => {
console.error(error));
reject();
}
});
}
render() {
// Renders something
};
}
export default injectIntl(Foo);
Only then was Jest able to properly hook into the promise and everything worked as I expected it to be in the first place.
I still didn't understand why the promise cannot simply be returned and needs to be wrapped in another Promise, though. So if someone has an explanation for that it would be greatly appreciated.

Promise.all result fires before actions succeed

I'm searching and searching solution on Stackoverflow and Github but 1.5 day is not enough - I don't see where I'm making mistake. I have following code of my component where I would like to fetch data from two endpoints and after store updates I console log something.
class ExampleComponent extends Component {
componentDidMount(){
this.fetchData()
}
fetchData(){
const {dispatch} = this.props
Promise.all([
dispatch(getTodos()),
dispatch(getPhotos())
])
.then(() => {
console.log('I did everything!');
});
}
render() {
return (
<h1>Something</h1>
);
}
}
export default connect()(ExampleComponent)
And here are my actions...
export function getPhotos() {
return function(dispatch) {
axios.get('https://jsonplaceholder.typicode.com/photos')
.then((response) => {
console.log('photos')
dispatch({type: 'PHOTOS_REQUEST_SUCCESS',payload: response})
})
.catch((err) => {
dispatch(photosRequestError(err))
})
}
}
export function getTodos() {
return function(dispatch){
axios.get('https://jsonplaceholder.typicode.com/todos')
.then(
(response) => {
console.log('todos')
dispatch({type: 'TODOS_REQUEST_SUCCESS', payload: response})
})
.catch((err) => {
dispatch(todosRequestError(err))
})
}
}
In console I get "I did everything", "todos", "photos". How should I modify my code to update store first and then log "I did everything" in console?
Thanks in advance
You need to return the promises themselves so that the chaining apply synchronous, so try adding return to axios.get('endpoint/url') in both functions.

Categories