Await for completion redux-saga in react class component method - reactjs

I'm sorry if this is a duplicate question, but I've searched for this question, and haven't found an answer that completely gives the solution for my issue. I want to fetch data in sagas and update localStorage after data is fetched. Then I want to this.forceUpdate() in my component method. But obviously this.forceUpdate() function is launched before data
is loaded. Can I wrap my handleSubmit class method into promise or async/await to make my this.forceUpdate() await for data to be fetched? Thank you.
FETCHING FUNCTION
export const fetchAuth = data => {
return axios({
method: "post",
url: "http://url/",
headers: {},
data: {
email: data.email,
password: data.password.toString()
}
}).then(response => {
return response.data;
});
};
REDUX-SAGA
export function* authRequestFlow(action) {
try {
const tokens = yield call(fetchAuth, action.payload);
if (tokens) {
yield call(save, "access", tokens.access);
yield call(save, "refresh", tokens.refresh);
yield call(save, "isAuthorized", true);
yield put(authSuccess());
}
} catch (error) {
yield put(authFailure(error.message));
}
}
COMPONENT
class Login extends PureComponent {
handleSubmit = () => {
authRequest();
this.forceUpdate();
};
render() {
if (localStorage.getItem("isAuthorized") === "true")
return <Redirect to="/products" />;
return (
<div className={styles.root}>
<Button
onClick={this.handleSubmit}
>
Submit
</Button>
</div>
);
}
}

Related

tanstack react query mutate onSettled to access PUT body data

Dears,
Due to some reason, I need access to the PUT request body after the PUT request settled.
Please check the sandbox example I tried to prepare.
My question is - is it ok to return the PUT params in onMutate and then do some logic in onSettled based on these params, for example selectively setting a loading state to false.
And then, why the PUT params are the 3rd argument of the onSettled function?
p.s. please don't argue about state management, the question is about onSettled usage :)
Best regards,
MJ
import React from "react";
import { useMutation } from "react-query";
const someProps = { prop1: "key1" };
export default function App() {
const [isLoading, setIsLoading] = React.useState(false);
const { mutate } = useMutation({
mutationFn: async (someProps) =>
await fetch("https://httpbin.org/put", {
method: "PUT",
body: JSON.stringify(someProps)
}).then((response) => response.json()),
onSuccess: (responseData) => {
console.log("RESPONSE ON SUCCESS: " + JSON.stringify(responseData));
},
onMutate: (data) => {
setIsLoading(true);
console.log(
"Yes, I have access to props before I send the request: " +
JSON.stringify(data)
);
// I return the data so I can use it in on settled
return data;
},
onSettled: (arg1NotUsed, arg2NotUsed, data) => {
console.log(
"Yes, I have access to props after I receive the response: " +
JSON.stringify(data)
);
if (data) {
setIsLoading(false);
}
}
});
return (
<div>
<p>is loading: {isLoading ? "LOADING" : "IDLE"}</p>
<button onClick={() => mutate(someProps)}>trigger mutation</button>
</div>
);
}
variables are available in onSettled even if you don't return them from onMutate. What onSettled receives is:
onSettled(data, error, variables, context)
where context is what you return from onSettled. In your example, you're using the 3rd parameter, which is not the value returned from onMutate, so you can safely leave that out.
There is also no need to separately track an isLoading boolean, because useMutation does this for you and also returns a loading state.
export default function App() {
const { mutate, isLoading } = useMutation({
mutationFn: async (someProps) =>
await fetch("https://httpbin.org/put", {
method: "PUT",
body: JSON.stringify(someProps)
}).then((response) => response.json()),
onSuccess: (responseData) => {
console.log("RESPONSE ON SUCCESS: " + JSON.stringify(responseData));
},
onSettled: (arg1NotUsed, arg2NotUsed, data) => {
console.log(
"Yes, I have access to props after I receive the response: " +
JSON.stringify(data)
);
}
});
return (
<div>
<p>is loading: {isLoading ? "LOADING" : "IDLE"}</p>
<button onClick={() => mutate(someProps)}>trigger mutation</button>
</div>
);
}
Here's a fork of your sandbox with these changes: https://codesandbox.io/s/usequery-forked-vq8kcr?file=/src/App.js

React passing data or adding to state wrong

When I leave this code as is, I will get the correct console.log (commented with "these appear correct") that I'm looking for. However when I replace the api_url with http://localhost:9000/ipdata/${this.state.inputValue} the console.log is blank. This is why I think I'm either passing the input value wrong or I'm adding it to the state wrong.
I would assume I'm adding it to the state wrong as the spans that I'm trying to render in order to output the data on the client aren't displaying anything either.
Heres my code ...
import React from 'react';
import './App.css';
class App extends React.Component {
constructor(props) {
super(props);
this.state = { apiResponse: '', inputValue: '', result: {} };
}
async callAPI() {
try {
console.log('called API...');
const api_url = `http://localhost:9000/ipdata/8.8.8.8`;
const res = await fetch(api_url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
const result = await res.json();
// these appear correct
console.log(result.city);
console.log(result.region_code);
console.log(result.zip);
this.setState({ result });
} catch (error) {
// handle errors
}
}
render() {
return (
<div className="App">
<h1>IP Search</h1>
<input
type="text"
value={this.state.inputValue}
onChange={(e) => this.setState({ inputValue: e.target.value })}
/>
<button onClick={this.callAPI}>Search IP</button>
<p>
<span>{this.state.result.city}</span>
<span>{this.state.result.region_code}</span>
<span>{this.state.result.zip}</span>
</p>
</div>
);
}
}
export default App;
API call on the Node server...
const fetch = require('node-fetch');
app.get('/ipdata/:ipaddress', async (req, res, next) => {
console.log(req.params);
const ipaddress = req.params.ipaddress;
console.log(ipaddress);
const api_url = `http://api.ipstack.com/${ipaddress}?access_key=API_KEY`;
const response = await fetch(api_url);
const json = await response.json();
res.json(json);
});
The problem is not the way you set state, but the way you access it, because callAPI doesn't have access to this, so you get an error thrown inside the function and as you don't handle errors, it gets swollen. To make it work you either bind the function
onClick={this.callAPI.bind(this)}
or use arrow function instead
callAPI = async ()=> {

A lot of unnecessary requests

I have Posts component:
class ProfilePosts extends React.Component {
constructor(props){
super(props);
this.state = {
posts: []
}
}
getData = async (url) => {
const res = await fetch(url, {
method: 'GET',
headers: {
'Authorization' : `${window.localStorage.getItem('token')}`
}
});
return await res.json();
}
getPosts = async () => {
if (window.location.pathname === '/profile/undefined%7D')
{
window.location.pathname = `/profile/${window.localStorage.getItem('id')}`
}
await this.getData(`https://api.com/posts/${window.location.pathname.slice(9)}`)
.then(data => {
this.setState({posts: data}
);
})
}
componentDidMount(){
if (window.localStorage.getItem('id') != ''){
this.getPosts();
}
}
componentDidUpdate(){
if (window.localStorage.getItem('id') != ''){
this.getPosts();
}
}
renderItems(posts){
return Object.values(posts).map(post => {
return (
<ProfilePost likes={post.likes} comments={post.comments} userId={window.localStorage.getItem('id')} token={window.localStorage.getItem('token')} Postid={post.id} key={post.id} sender={post.sender} content={post.content} time={post.sent_time}/>
)
});
}
render() {
const {posts} = this.state;
const items = this.renderItems(posts);
return(
<div className="profile-posts">
{items}
</div>
);}
}
export default ProfilePosts;
I don't understand what is the reasons for re-rendering this component so many times (I don't know the actual number but it's huge). Therefore, a lot of requests to the database, and so on. How can I fix this?
I tried using another state but that didn't work for me. If you press the button, the state changed and then I checked if the state changed then I call this.getPosts() and then set the state to false again. It caused Error: Maximum update depth exceeded.
Your componentDidUpdate runs every time the component updates, after render, so it's calling this.getPosts();, which in turn calls setState, which results in another re-render and update, and so on.
Remove the componentDidUpdate entirely and let the componentDidMount method (which implements the same logic) alone take care of things.

How to test an async function which does an axios request to an api using JEST in a React application

I am trying to test a async/await function which does an api call using axios to get users. I am new to testing React applications and using JEST (trying first time), unable to get the test running.
I have tried using mock function in JEST. My Code is as follows:
// component --users
export default class Users extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.getUsers = this.getUsers.bind(this);
}
/*does an api request to get the users*/
/*async await used to handle the asynchronous behaviour*/
async getUsers() {
// Promise is resolved and value is inside of the resp const.
try {
const resp = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
if (resp.status === 200) {
const users = resp.data;
/* mapped user data to just get id and username*/
const userdata = users.map(user => {
var userObj = {};
userObj["id"] = user.id;
userObj["username"] = user.username;
return userObj;
});
return userdata;
}
} catch (err) {
console.error(err);
}
}
componentDidMount() {
this.getUsers().then(users => this.setState({ users: users }));
}
/*****************************************************************/
//props(userid ,username) are passed so that api call is made for
//getting posts of s psrticular user .
/*****************************************************************/
render() {
if (!this.state.users) {
return (
<div className="usersWrapper">
<img className="loading" src="/loading.gif" alt="Loading.." />
</div>
);
}
return (
<div className="usersWrapper">
{this.state.users.map(user => (
<div key={user.id}>
<Posts username={user.username} userid={user.id} />
</div>
))}
</div>
);
}
}
//axios.js-mockaxios
export default {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
//users.test.js
describe("Users", () => {
describe("componentDidMount", () => {
it("sets the state componentDidMount", async () => {
mockAxios.get.mockImplementationOnce(
() =>
Promise.resolve({
users: [
{
id: 1,
username: "Bret"
}
]
}) //promise
);
const renderedComponent = await shallow(<Users />);
await renderedComponent.update();
expect(renderedComponent.find("users").length).toEqual(1);
});
});
});
the test fails -
FAIL src/components/users.test.js (7.437s) ● Users ›
componentDidMount › sets the state componentDidMount
expect(received).toEqual(expected)
Expected value to equal:
1
Received:
0
Please help me figure out the problem. i am totally new to testing reactapps
It looks like similar to this one.
The problem is that test if finished earlier then async fetchUsers and then setState (it's also async operation). To fix it you can pass done callback to test, and put the last expectation into setTimeout(fn, 0) - so expect will be called after all async operations done:
it("sets the state componentDidMount", (done) => {
...
setTimeout(() => {
expect(renderedComponent.find("users").length).toEqual(1);
done();
}, 0);
});
As mentioned in comment, it's hacky fix, I hope here will be other answers with more jest way to fix it.
As far as I understood, what you are trying to do, is to wait for the resolution of a promise, which is 'hidden' inside of your componentDidMount() method.
expect(renderedComponent.find("users").length).toEqual(1);
will not work in my view in this context because find() is trying to select DOM Elements. If you want to check the state, you need to use state():
expect(renderedComponent.state("users").length).toEqual(1);
Still, you will have to find the right way to wait for the resolution of the promise:
To refer to the previous posts and comments, I don't see any effect in using async/await in combination with any of the enzyme methods (update, instance or whatsoever. Enzyme docs also give no hint in that direction, since the promise resolution is not the job of enzyme).
The only robust, clean and (jest-default) way forward is somehow a mixture of the different approaches. Here comes your code slightly changed:
// component --users
export default class Users extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.getUsers = this.getUsers.bind(this);
}
/*does an api request to get the users*/
/*async await used to handle the asynchronous behaviour*/
async getUsers() {
// Promise is resolved and value is inside of the resp const.
try {
const resp = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
if (resp.status === 200) {
const users = resp.data;
/* mapped user data to just get id and username*/
const userdata = users.map(user => {
var userObj = {};
userObj["id"] = user.id;
userObj["username"] = user.username;
return userObj;
});
return userdata;
}
} catch (err) {
console.error(err);
}
}
componentDidMount() {
// store the promise in a new field, not the state since setState will trigger a rerender
this.loadingPromise = this.getUsers().then(users => this.setState({users: users}));
}
/*****************************************************************/
//props(userid ,username) are passed so that api call is made for
//getting posts of s psrticular user .
/*****************************************************************/
render() {
if (!this.state.users) {
return (
<div className="usersWrapper">
<img className="loading" src="/loading.gif" alt="Loading.."/>
</div>
);
}
return (
<div className="usersWrapper">
{this.state.users.map(user => (
<div key={user.id}>
<Posts username={user.username} userid={user.id}/>
</div>
))}
</div>
);
}
}
//axios.js-mockaxios
export default {
get: jest.fn(() => Promise.resolve({data: {}}))
};
//users.test.js
describe("Users", () => {
describe("componentDidMount", () => {
it("sets the state componentDidMount",() => {
mockAxios.get.mockImplementationOnce(
() =>
Promise.resolve({
users: [
{
id: 1,
username: "Bret"
}
]
}) //promise
);
const renderedComponent = shallow(<Users/>);
//Jest will wait for the returned promise to resolve.
return renderedComponent.instance().loadingPromise.then(() => {
expect(renderedComponent.state("users").length).toEqual(1);
});
});
});
});
While doing some research, I came across this post: To be honest I don't know whether it is a good idea, but it works in my async tests, and it avoids the additional reference to the promise in your component. So feel free to try this out also!

React doesn't render data coming from an api response

I've seen a lot of questions and I couldn't get the solution
here is my code:
import React, { Component } from "react";
import axios from "axios";
import "./tree.css";
import "./mainTree";
class TablesTree extends Component {
constructor(props) {
super(props);
this.data = this.props.info;
this.state = {
fields: [],
data: [],
show: false
};
}
componentDidMount() {
var dataGet = [];
this.props.tables.forEach((name, i) => {
this.getFieldsTable(name.TABLE_NAME, (err, res) => {
if (res) {
dataGet.push({
TABLE_NAME: name.TABLE_NAME,
columns: res
});
}
});
});
this.setState({ data: dataGet });
}
getFieldsTable(table, callback) {
axios
.get(`table/columns?name=${this.data.user}&psw=${this.data.password}&schema=${this.data.schema}&table=${table}`)
.then(response => {
callback(null, response.data);
})
.catch(error => {
console.log(error);
});
}
render() {
return (
<div>
{this.state.data
? this.state.data.map((itm, i) => {
return (
<div>
<h1>{itm.TABLE_NAME}</h1>
</div>
);
})
: null}
</div>
);
}
}
export default TablesTree;
I've made console.log of the this.state.data
and the data is in there, but it doesn't renders anything
I've tried a lot of soutions, but I still without rendering the data, I will apreciate your help.
There's a few things I would change about your code, but most importantly you need to do this.setState after your push to dataGet (inside of your callback function).
Because your API call is asynchronous, you are only calling setState once when your component is initially mounted (and while dataGet is still empty).
getFieldsTable is asynchronous, so the dataGet array will be empty when you call setState.
You could return the promise from getFieldsTable and use Promise.all on all the promises, and use the data when all of them have resolved.
Example
class TablesTree extends Component {
// ...
componentDidMount() {
const promises = this.props.tables.map(name => {
return this.getFieldsTable(name.TABLE_NAME).then(res => {
return {
TABLE_NAME: name.TABLE_NAME,
columns: res
};
});
});
Promise.all(promises).then(data => {
this.setState({ data });
});
}
getFieldsTable(table) {
return axios
.get(`table/columns?name=${this.data.user}&psw=${this.data.password}&schema=${this.data.schema}&table=${table}`)
.then(response => {
return response.data;
})
.catch(error => {
console.log(error);
});
}
// ...
}

Resources