Updating the react state/component correctly? - javascript

I am trying to make my component reactive on updates. I am using componentDidUpdate() to check if the components prop state has changed, then if it has it is has I need the getPosts() function to be called and the postCount to update if that prop is changed.
export default class JsonFeed extends React.Component<IJsonFeedProps, IJsonFeedState> {
// Props & state needed for the component
constructor(props) {
super(props);
this.state = {
description: this.props.description,
posts: [],
isLoading: true,
jsonUrl: this.props.jsonUrl,
postCount: this.props.postCount,
errors: null,
error: null
};
}
// This function runs when a prop choice has been updated
componentDidUpdate() {
// Typical usage (don't forget to compare props):
if (this.state !== this.state) {
this.getPosts();
// something else ????
}
}
// This function runs when component is first renderd
public componentDidMount() {
this.getPosts();
}
// Grabs the posts from the json url
public getPosts() {
axios
.get("https://cors-anywhere.herokuapp.com/" + this.props.jsonUrl)
.then(response =>
response.data.map(post => ({
id: `${post.Id}`,
name: `${post.Name}`,
summary: `${post.Summary}`,
url: `${post.AbsoluteUrl}`
}))
)
.then(posts => {
this.setState({
posts,
isLoading: false
});
})
// We can still use the `.catch()` method since axios is promise-based
.catch(error => this.setState({ error, isLoading: false }));
}

You can change componentDidUpdate to:
componentDidUpdate() {
if (this.state.loading) {
this.getPosts();
}
}
This won't be an infinite loop as the getPosts() function sets state loading to false;
Now every time you need an update you just need to set your state loading to true.
If what you want to do is load everytime the jsonUrl updates then you need something like:
componentDidUpdate(prevProps) {
if (prevProps.jsonUrl!== this.props.jsonUrl) {
this.getPosts();
}
}
Also I don't get why you expose your components state by making componentDidMount public.

Modify your getPosts to receive the jsonUrl argument and add the following function to your class:
static getDerivedStateFromProps(props, state) {
if(props.jsonUrl!==state.jsonUrl){
//pass jsonUrl to getPosts
this.getPosts(props.jsonUrl);
return {
...state,
jsonUrl:props.jsonUrl
}
}
return null;
}
You can get rid of the componentDidUpdate function.
You can also remove the getPosts from didmount if you don't set state jsonUrl in the constructor.

// This function runs when a prop choice has been updated
componentDidUpdate(prevProps,prevState) {
// Typical usage (don't forget to compare props):
if (prevState.jsonUrl !== this.state.jsonUrl) {
this.getPosts();
// something else ????
}
}
this way you have to match with the updated state

Try doing this
componentDidUpdate(prevState){
if(prevState.loading!==this.state.loading){
//do Something
this.getPosts();
}}

Related

Can I fetch api every time that props change on ComponentDidUpdate?

As you see my code, I want to fetch API contentUrl every its change from props.
but browser throw error like this. Have someone help me?.
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the
componentWillUnmount method.
Issue is clearly states that : Can't perform a React state update on
an unmounted component
So check if the component is unmounted before setting up state, _isMounted
Code Snippet Ref from : HERE , Hope this will clear all your doubts
class News extends Component {
_isMounted = false; // <----- HERE
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true; // <----- HERE
axios
.get('YOUR_URL')
.then(result => {
if (this._isMounted) { // <----- CHECK HERE BEFORE SETTING UP STATE
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false; // <----- HERE
}
render() {
...
}
}
For example, if a call is done and your component is unmounted, a setState will be called.
You can prevent this with a condition:
_isMount = true;
componentDidUpdate() {
this.props.getContentJSON(url).then(() => {
if(this._isMount){
this.setState({...})
}
})
}
componentWillUnmount() {
this._isMount = false;
}
or control your call :
controller = new AbortController();
componentDidUpdate() {
// I use fetch here but you can adapte to your code
fetch(url, { signal: controller.signal }).then(() => {
this.setState({...})
})
}
componentWillUnmount() {
controller.abort();
}

How do i pull data from redux state in react

Im trying to make an api request from redux then take that data and put it in my react state (arrdata). The api call works but i cant seem to get the state on my app.js to update based on the redux api call. Am i missing something?
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
arrdata: []
};
}
componentDidMount() {
this.props.loadData();
console.log(this.props.data);
}
render() {
const {arrdata} = this.state
return ( ......)}}
const mapStateToProps = state => {
return {
data: state.data
};
};
export default connect(mapStateToProps, dataAction)(App);
Action
export function loadData() {
return dispatch => {
return axios.get("https://api.coincap.io/v2/assets").then(response => {
dispatch(getData(response.data.data.slice(0, 10)));
});
};
}
export function getData(data) {
return {
type: "GET_DATA",
data: data
};
}
Reducer
let initialState = {
data: []
};
const mainReducer = (state = initialState, action) => {
if (action.type === "GET_DATA") {
return {
...state,
data: action.data
};
} else {
return {
...state
};
}
};
export default mainReducer;
I think you are misleading store with state. Your arrdata is empty since it's stored inside state, but your data comes from props.
Anyways, arrdata in state remains empty, since you are not setting the state anywhere. To do that, you would have to use e.g. getDerivedStateFromProps lifecycle hook, however I wouldn't recommend that.
render() {
const { data } = this.props;
console.log(this.props.data);
return (
// do something with your data
);
}
It should log your data properly.
Note: You don't need state, actually. It's a better approach to manipulate over props, instead of saving data from props into state (in most cases).

How to call a function that uses the state (which is set by another function) and updates state

I have two functions one that fetches data from an api and updates state according to that data, and a function that itterates over the data in the state and updates the state with the new data.
My problem is that i cant update the state in the second function. And i dont know where i have to call this function in order for it to be called after the first function and to use the data thats in the state.
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
});
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
addPhotos() {
const newCases = this.state.cases.map(({ photo_id, ...rest }) => {
const obj = { ...rest };
this.state.photos.find(data => {
if (data.id === photo_id) {
obj.file = data.file;
return true;
}
});
return obj;
});
console.log(this.state.cases);
this.setState({
'cases' : newCases
});
}
renderCases() {
this.addPhotos();
}
componentWillMount() {
this.getCases();
}
render() {
return (
<div>
{this.renderCases()}
</div>
)
}
}
This is what i now have
Where should i call the addPhotos function so it can update the state and still use the existing state data from the getCases function?
Thanks in advance!
So, first thing's first. The lifecycle method componentWillMount() is soon to be deprecated and is considered unsafe to use. You should be using componentDidMount().
As far as using the updated state in your addPhotos function, you can pass setState a callback function. A seemingly simple solution would be to just pass the addPhotos function as a callback into the setState being called in your getCases function.
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
}, this.addPhotos);
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
Another solution would be to call addPhotos() from componentDidUpdate instead.
Hope this helps!
Edit: Just some additional background information from the React docs.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Added some refactoring to your code, should work ok now, read comments for details
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(this.addPhotos) // don't need to set state, pass data to consumer function
.catch(console.error); // catch always gives error, don't need to check with if statement
}
addPhotos(response) {
const cases = response.data.cases // extract values
const photos = response.data.photos // extract values
// your .map iterator has O^2 complexity (run each item of cases on each item of photos)
// this new .map iterator has O complexity (run each item of cases)
const newCases = cases.map(({ photo_id, ...rest }) => {
const obj = {...rest};
const data = photos.find(item => item.id === photo_id);
if (data) {
obj.file = data.file
}
return obj;
});
this.setState({
cases: newCases,
photos
});
}
componentDidMount() { // better use didMount
this.getCases();
}
render() {
return (<div />)
}
}

React set-state doesn't update in fetch success function (on key up event)

I have a search component containing an input on which I defined a key up event handler function for fetching data based on entered string. As you can see below:
class SearchBox extends Component {
constructor(props) {
super(props);
this.state = {
timeout: 0,
query: "",
response: "",
error: ""
}
this.doneTypingSearch = this.doneTypingSearch.bind(this);
}
doneTypingSearch(evt){
var query = evt.target.value;
if(this.state.timeout) clearTimeout(this.state.timeout);
this.state.timeout = setTimeout(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1/?name=query' , {
method: "GET"
})
.then( response => response.json() )
.then(function(json) {
console.log(json,"successss")
//Object { userId: 1, id: 1, title: "delectus aut autem", completed: false } successss
this.setState({
query: query,
response: json
})
console.log(this.state.query , "statesssAfter" )
}.bind(this))
.catch(function(error){
this.setState({
error: error
})
});
}, 1000);
}
render() {
return (
<div>
<input type="text" onKeyUp={evt => this.doneTypingSearch(evt)} />
<InstantSearchResult data={this.state.response} />
</div>
);
}
}
export default SearchBox;
The problem is the setState which I used in the second .then(). The response won't update . I want to update it and pass it to the InstantSearchResult component which is imported here. Do you have any idea where the problem is ?
Edit - Add InstantSearchResult component
class InstantSearchBox extends Component {
constructor(props) {
super(props);
this.state = {
magicData: ""
}
}
// Both methods tried but got error => Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
componentDidUpdate(props) {
this.setState({
magicData: this.props.data
})
}
shouldComponentUpdate(props) {
this.setState({
magicData: this.props.data
})
}
render() {
return (
<h1>{ this.state.magicData}</h1>
);
}
}
export default InstantSearchBox;
Edit:
Be aware that setState is asynchronous reading this article.
I've understand that the setState works fine in my fetch success the problem was the console.log which I shouldn't use it after setState instead I console.log in render() and I found out that the state updates correctly .
The other thing I wasn't careful for was the InstantSearchResult Constructor! When I re-render the SearchBox component consequently the InstantSearchResult renders each time but it's constructor runs just once. And if I use setState in InstantSearchResult I will face an infinite loop so I have to use this.props instead to pass the data to the second component.
this has been overridden inside the promise callback function. You to save it to a variable:
doneTypingSearch(evt){
var _this = this,
query = evt.target.value;
if(this.state.timeout) clearTimeout(this.state.timeout);
this.state.timeout = setTimeout(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1/?name=query' , {
method: "GET"
})
.then( response => response.json() )
.then(function(json) {
console.log(json,"successss")
//Object { userId: 1, id: 1, title: "delectus aut autem", completed: false } successss
_this.setState({
query: query,
response: json
})
console.log(_this.state.query , "statesssAfter" )
}/*.bind(this)*/)
.catch(function(error){
_this.setState({
error: error
})
});
}, 1000);
}

React - How to pass returned data from an exported function to a component?

How can I pass data I receive from a get request pass over to a component? Whatever I tried wouldn't work but my thinking was as the code below shows..
Thanks!
export function data() {
axios.get('www.example.de')
.then(function(res) {
return res.data
})
.then(function(data) {
this.setState({
list: data
})
})
}
import {data} from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ""
};
}
componentWillMount() {
data();
}
render() {
return <p > this.state.list < /p>
}
}
You call this.setState inside of data()->then callback, so this is context of the then callback function. Instead you should use arrow functions (it does not have its own context) and pass component's this to data function using call
export function data() {
axios.get('www.example.de')
.then(res => res.data)
.then(data => {
this.setState({
list: data
})
})
}
import {data} from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ""
};
}
componentWillMount() {
data.call(this);
}
render() {
return <p > this.state.list < /p>
}
}
However, your data services must not know about setState and, event more, expect passing this from react component. Your data service must be responsible for retrieving data from server, but not for changing component state, see Single responsibility principle. Also, your data service can be called from another data service. So your data service should return promise instead, that can be used by component for calling setState.
export function data() {
return axios.get('www.example.de')
.then(res => res.data)
}
and then
componentWillMount() {
data().then(data=>{
this.setState({
list: data
})
});
}
your api shouldn't know anything about your component, you can easily do this with callback, like so -
export function data(callback) {
axios.get('www.example.de')
.then(res => callback({ data: res.data }))
.catch(err => callback({ error: err }));
}
By doing this you can easily unit test your api
So in your Test component, you simply do -
componentWillMount() {
data(result => {
const { data, error } = result;
if (error) {
// Handle error
return;
}
if (data) {
this.setState({ list: data });
}
});
}
Your request is a promise so you can simply return that from the imported function and use the eventual returned result of that within the component. You only want to be changing the state of the component from within the component.
export function getData(endpoint) {
return axios.get(endpoint);
}
Note I've changed the name of the function to something more "actiony".
import { getData } from './api.js';
class Test extends React.Component {
constructor(props) {
super(props);
// Your state is going to be an array of things, so
// initialise it with an array to spare confusion
this.state = { list: [] };
}
// I use ComponentDidMount for fetch requests
// https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
componentDidMount() {
// We've returned a promise from `getData` so we can still use the
// promise API to extract the JSON, and store the parsed object as the
// component state
getData('www.example.de')
.then(res => res.data)
.then(list => this.setState({ list }))
}
}
Your external function doesn't have the correct context of this, so you'll need to call it with the correct context from within the component:
componentWillMount() {
data.call(this);
}
However, inside the API call, it still won't have the correct this context, so you can set a variable to point to this inside the data() function:
export function data() {
let that = this;
axios('http://www.url.com')
.then(function(res) {
return res.data
})
.then(function(data) {
that.setState({
list: data
})
})
}
Details of the this keyword
However, it's generally considered better practice to only handle your state manipulation from with the component itself, but this will involve handling the asynchronous nature of the GET request, perhaps by passing in a callback to the data() function.
EDIT: Updated with asynchronous code
//api.js
data(callback){
axios.get('www.url.com')
.then(res => callback(res));
}
//component.jsx
componentWillMount(){
data(res => this.setState({list: res}));
}

Categories