Abort previous running async componentDidMount - javascript

We have a component, connected to the redux store, that has this componentDidMount definition:
async componentDidMount() {
const page = await fetchPage(this.props.page);
const section = await fetchSection(this.props.section);
this.props.saveDataToStore({ page, section });
}
So, every time the component is mounted, it'll run this code.
Now let's imagine that we go to another section of our app, so the component gets unmounted, and then we come back before the previous componentDidMount has finished its execution, mounting the component again and starting fetching new data. Every time the data is fetched, we save it to the redux store.
I think that there is no way to abort previous componentDidMount execution to prevent setting old data from previous calls. So, my question is, what's the best approach to follow to prevent old data being set?
I've been able to solve it having a function that does all of these calls (like fetchData) that handles an AbortController object, but I found it a bit verbose, so do you know what's the recommended approach to follow in this situation?

Using the AbortController or a library like axios that supports cancellable requests is a good solution, but you could also keep an instance variable that you change to false when the component unmounts and check against his before you use setState after the requests have finished.
Example
class App extends React.Component {
_isMounted = true;
async componentDidMount() {
const page = await fetchPage(this.props.page);
const section = await fetchSection(this.props.section);
if (this._isMounted) {
this.setState({ page, section });
}
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
// ...
}
}

Related

React Redux - quick flash of previous state before dispatch issue

I'm building a React + Redux app with a Node.js backend, and one of the features is that a user can view profiles of other users. To do this, I have a section in the Redux state called users that looks like:
{
...,
users: {
user: {},
isLoading: true
}
}
Every time the /users/:id route is rendered, a getUser(id) action is dispatched and fills the state with the received information.
The main issue is when a user views user1's profile page (therefore redux state is now filled with user1's profile) and then views user2's profile page, the dispatch getUser(2) action is called right after the page is rendered. Since user1's info is still in the state, it will flash their info for a very short time, and then show the loading spinner until the new user is loaded.
I read about dispatching a resetUser(id) action on every unmount of the page, but I'm not sure if this is the right way to go. Also, one of the features is if a user is viewing their own page, they have an edit button which redirects them to /edit-profile. If I reset the state on every unmount, I'll have to fetch their profile again (in the edit page), even though I just did that when they viewed their page.. And that doesn't seem like it makes sense.
Any ideas how to solve this? Thanks!
The render phase runs after mounting. And you stated that previous data is being shown before new data. It seems that you have asynchronous mounting:
async componentDidMount() {}
It will continue rendering even before mounting phase is completed. To avoid issue, you may use synchronous nature of mounting:
componentDidMount(){}
Where you'll call api data.
Now, when you reach to rendering phase it will have new data available before rendering and won't flash you old data.
You now may be wondering how to call api asynchronously. You can create a asynchronous function and call that function inside the synchronous componentDidMount.
componentDidMount() {
yourAsyncFunc()
}
async yourAsyncFunc() {} // api call
How can I do this with React hooks?
While using useEffect, don't implement async:
useEffect(async () =>
Implement it simply:
useEffect(() => {
// you can still use asynchronous function here
async function callApi() {}
callApi()
}, []) // [] to run in similar to componentDidMount
If you miss second parameter to useEffect then you are not ensuring it to run on mounting phase. It will run before, after, and in the rendering phase depending on case.
Implementing something like resetUser(id) seems to be the right way here. I use this approach in my current project and this does solve the problem. The other approach of removing async keyword from useEffect callback as mentioned in another answer didn't work for me (I use hooks, redux-toolkit, Typescript).
After dispatching this action, your state should look something like
{
...,
users: {
user: null,
isLoading: false,
}
}
If you are using hooks, you can dispatch the action this way:
useEffect(() => {
const ac = new AbortController();
...
return () => {
dispatch(resetUser(null));
ac.abort();
};
}, []);
Action could look something like this:
resetListingDetails(state, action) {
// Immer-like mutable update
state.users = {
...state.users,
user: null,
};
}

React Native callback after SocketIO response

I have a React Native App which on componentWillMount() calls a function to save everything in that list.
var list = []
const getMatchList = (logKey) => {
global.socket.emit("getMatches", logKey, (data) => {
//adding to list logic
})
console.log("Matches Loaded");
}
class MatchesScreen extends React.Component {
async componentWillMount() {
logKey = await getPreferences("logKey");
await getMatchList(logKey)
}
componentDidMount() {
console.log(list);
}
}
When I access that screen, it shows the empty list first, then I get the message "Matches Loaded". How can I first load matches?
Thank you.
whats wrong
I looks like you expect that await will turn your async function to a sync one, but this is not the case. Your componentWillMount will be called before rendering, but since it loads data asynchronously and since react will not wait for the promise to resolve, the page will be rendered with no data and will be rerendered later whenever data is available.
how to solve
this need more context to have a good answer, but you have at least following options:
Use the same logic, but add a loading spinner/progress bar for that view.
Switch to the view when data is loaded - but it will be perceived sluggish by users.
If possible preload and cache the data while in previous view.

Memory leak in my React app because of firebase and how to avoid it?

The app I'm working on displays a user dashboard on login with a sidebar for navigation. It uses firebase. I do most of my data fetch from firebase in my async componentDidMount() and store the data in my component state. It takes a couple of seconds to finish all fetches. But if the user decides to navigate to another screen before the fetch is complete, I get the
Can't call setState on unmounted component
warning (as expected). So I do some digging and find that
if(this.isMounted()) this.setState({updates:updates})
makes the warning go away, but then I also find that using isMounted is an antipattern.
The official documentation on this issue suggests tracking the mounted state ourselves by setting _isMounted=true in componentDidMount and then set it to false in the componentWillUnmount. The only way I see to achieve this would be through a variable in component state. Turns out, setState doesn't work in componentWillUnmount. [Issue 1] (I tried calling an external function from componentWillUnmount which in turn sets the state variable. Didn't work.)
The documentation suggests another way, to use cancellable promises. But I'm clueless about how to achieve that with await firebase calls. I also couldn't find any way to stop firebase calls mid-track. [Issue 2]
So now I'm stuck with the warning and data leaks.
a. How do I resolve this problem?
b. Is this something I need to take seriously?
It's good practice to check if the component is still mounted when a request completes, if there is a risk of the component unmounting.
You don't need to put _isMounted in your component state since it will not be used for rendering. You can put it directly on the component instance instead.
Example
class MyComponent extends React.Component {
state = { data: [] };
componentDidMount() {
this._isMounted = true;
fetch("/example")
.then(res => res.json())
.then(res => {
if (this._isMounted) {
this.setState({ data: res.data });
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
// ...
}
}

what is right way to do API call in react js?

I have recently moved from Angular to ReactJs. I am using jQuery for API calls. I have an API which returns a random user list that is to be printed in a list.
I am not sure how to write my API calls. What is best practice for this?
I tried the following but I am not getting any output. I am open to implementing alternative API libraries if necessary.
Below is my code:
import React from 'react';
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
person: []
};
}
UserList(){
return $.getJSON('https://randomuser.me/api/')
.then(function(data) {
return data.results;
});
}
render() {
this.UserList().then(function(res){
this.state = {person: res};
});
return (
<div id="layout-content" className="layout-content-wrapper">
<div className="panel-list">
{this.state.person.map((item, i) =>{
return(
<h1>{item.name.first}</h1>
<span>{item.cell}, {item.email}</span>
)
})}
<div>
</div>
)
}
}
In this case, you can do ajax call inside componentDidMount, and then update state
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {person: []};
}
componentDidMount() {
this.UserList();
}
UserList() {
$.getJSON('https://randomuser.me/api/')
.then(({ results }) => this.setState({ person: results }));
}
render() {
const persons = this.state.person.map((item, i) => (
<div>
<h1>{ item.name.first }</h1>
<span>{ item.cell }, { item.email }</span>
</div>
));
return (
<div id="layout-content" className="layout-content-wrapper">
<div className="panel-list">{ persons }</div>
</div>
);
}
}
You may want to check out the Flux Architecture. I also recommend checking out React-Redux Implementation. Put your api calls in your actions. It is much more cleaner than putting it all in the component.
Actions are sort of helper methods that you can call to change your application state or do api calls.
Use fetch method inside componentDidMount to update state:
componentDidMount(){
fetch('https://randomuser.me/api/')
.then(({ results }) => this.setState({ person: results }));
}
This discussion has been for a while and #Alexander T.'s answer provided a good guide to follow for newer of React like me. And I'm going to share some additional know-how about calling the same API multiple times to refresh the component, I think it's probably a common question for beginners.
componentWillReceiveProps(nextProps), from official documentation :
If you need to update the state in response to prop changes (for
example, to reset it), you may compare this.props and nextProps and
perform state transitions using this.setState() in this method.
We could conclude that here is the place we handle props from the parent component, have API calls, and update the state.
Base on #Alexander T.'s example:
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {person: []};
}
componentDidMount() {
//For our first load.
this.UserList(this.props.group); //maybe something like "groupOne"
}
componentWillReceiveProps(nextProps) {
// Assuming parameter comes from url.
// let group = window.location.toString().split("/")[*indexParameterLocated*];
// this.UserList(group);
// Assuming parameter comes from props that from parent component.
let group = nextProps.group; // Maybe something like "groupTwo"
this.UserList(group);
}
UserList(group) {
$.getJSON('https://randomuser.me/api/' + group)
.then(({ results }) => this.setState({ person: results }));
}
render() {
return (...)
}
}
Update
componentWillReceiveProps() will be deprecated.
Here are only some methods (all of them in Doc) in the life cycle I think that they are related to deploying API in the general cases:
By referring to the diagram above:
Deploy API in componentDidMount()
The proper scenario to have API call here is that the content (from the response of API) of this component will be static, componentDidMount() only fire once while the component is mounting, even new props are passed from the parent component or have actions to lead re-rendering.
The component do check difference to re-render but not re-mount.
Quote from doc:
If you need to load data from a remote endpoint, this is a good place to
instantiate the network request.
Deploy API in static getDerivedStateFromProps(nextProps, prevState)
We should notice that there are two kinds of component updating, setState() in current component would not trigger this method but re-rendering or new props from parent component would.
We could find out this method also fires while mounting.
This is a proper place to deploy API if we want to use the current component as a template, and the new parameters to make API calls are props coming from parent component.
We receive a different response from API and return a new state here to change the content of this component.
For example:
We have a dropdown list for different Cars in the parent component, this component needs to show the details of the selected one.
Deploy API in componentDidUpdate(prevProps, prevState)
Different from static getDerivedStateFromProps(), this method is invoked immediately after every rendering except the initial rendering. We could have API calling and render difference in one component.
Extend the previous example:
The component to show Car's details may contain a list of series of this car, if we want to check the 2013 production one, we may click or select or ... the list item to lead a first setState() to reflect this behavior (such as highlighting the list item) in this component, and in the following componentDidUpdate() we send our request with new parameters (state). After getting the response, we setState() again for rendering the different content of the Car details. To prevent the following componentDidUpdate() from causing the infinity loop, we need to compare the state by utilizing prevState at the beginning of this method to decide if we send the API and render the new content.
This method really could be utilized just like static getDerivedStateFromProps() with props, but need to handle the changes of props by utilizing prevProps. And we need to cooperate with componentDidMount() to handle the initial API call.
Quote from doc:
... This is also a good place to do network requests as long as you
compare the current props to previous props ...
I would like you to have a look at redux
http://redux.js.org/index.html
They have very well defined way of handling async calls ie API calls, and instead of using jQuery for API calls, I would like to recommend using fetch or request npm packages, fetch is currently supported by modern browsers, but a shim is also available for server side.
There is also this another amazing package superagent, which has alot many options when making an API request and its very easy to use.
You can also fetch data with hooks in your function components
full example with api call: https://codesandbox.io/s/jvvkoo8pq3
second example: https://jsfiddle.net/bradcypert/jhrt40yv/6/
const Repos = ({user}) => {
const [repos, setRepos] = React.useState([]);
React.useEffect(() => {
const fetchData = async () => {
const response = await axios.get(`https://api.github.com/users/${user}/repos`);
setRepos(response.data);
}
fetchData();
}, []);
return (
<div>
{repos.map(repo =>
<div key={repo.id}>{repo.name}</div>
)}
</div>
);
}
ReactDOM.render(<Repos user="bradcypert" />, document.querySelector("#app"))
1) You can use Fetch API to fetch data from Endd Points:
Example fetching all Github repose for a user
/* Fetch GitHub Repos */
fetchData = () => {
//show progress bar
this.setState({ isLoading: true });
//fetch repos
fetch(`https://api.github.com/users/hiteshsahu/repos`)
.then(response => response.json())
.then(data => {
if (Array.isArray(data)) {
console.log(JSON.stringify(data));
this.setState({ repos: data ,
isLoading: false});
} else {
this.setState({ repos: [],
isLoading: false
});
}
});
};
2) Other Alternative is Axios
Using axios you can cut out the middle step of passing the results of
the http request to the .json() method. Axios just returns the data
object you would expect.
import axios from "axios";
/* Fetch GitHub Repos */
fetchDataWithAxios = () => {
//show progress bar
this.setState({ isLoading: true });
// fetch repos with axios
axios
.get(`https://api.github.com/users/hiteshsahu/repos`)
.then(result => {
console.log(result);
this.setState({
repos: result.data,
isLoading: false
});
})
.catch(error =>
this.setState({
error,
isLoading: false
})
);
}
Now you can choose to fetch data using any of this strategies in componentDidMount
class App extends React.Component {
state = {
repos: [],
isLoading: false
};
componentDidMount() {
this.fetchData ();
}
Meanwhile you can show progress bar while data is loading
{this.state.isLoading && <LinearProgress />}
Render function should be pure, it's mean that it only uses state and props to render, never try to modify the state in render, this usually causes ugly bugs and decreases performance significantly. It's also a good point if you separate data-fetching and render concerns in your React App. I recommend you read this article which explains this idea very well. https://medium.com/#learnreact/container-components-c0e67432e005#.sfydn87nm
This part from React v16 documentation will answer your question, read on about componentDidMount():
componentDidMount()
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. This method is a good place to set up
any subscriptions. If you do that, don’t forget to unsubscribe in
componentWillUnmount().
As you see, componentDidMount is considered the best place and cycle to do the api call, also access the node, means by this time it's safe to do the call, update the view or whatever you could do when document is ready, if you are using jQuery, it should somehow remind you document.ready() function, where you could make sure everything is ready for whatever you want to do in your code...
As an addition/update to Oleksandr T.'s excellent answer:
If you use class components, backend calls should happen in componentDidMount.
If you use hooks instead, you should use the effect hook
For example:
import React, { useState, useEffect } from 'react';
useEffect(() => {
fetchDataFromBackend();
}, []);
// define fetchDataFromBackend() as usual, using Fetch API or similar;
// the result will typically be stored as component state
Further reading:
Using the Effect Hook in the official docs.
How to fetch data with React Hooks? by Robin Wieruch
A clean way is to make an asynchronous API call inside componentDidMount with try/catch function.
When we called an API, we receive a response. Then we apply JSON method on it, to convert the response into a JavaScript object. Then we take from that response object only his child object named "results" (data.results).
In the beginning we defined "userList" in state as an empty array. As soon as we make the API call and receive data from that API, we assign the "results" to userList using setState method.
Inside the render function we tell that userList will be coming from state. Since the userList is an array of objects we map through it, to display a picture, a name and a phone number of each object "user". To retrieve this information we use dot notation (e.g. user.phone).
NOTE: depending on your API, your response may look different. Console.log the whole "response" to see which variables you need from it, and then assign them in setState.
UserList.js
import React, { Component } from "react";
export default class UserList extends Component {
state = {
userList: [], // list is empty in the beginning
error: false
};
componentDidMount() {
this.getUserList(); // function call
}
getUserList = async () => {
try { //try to get data
const response = await fetch("https://randomuser.me/api/");
if (response.ok) { // ckeck if status code is 200
const data = await response.json();
this.setState({ userList: data.results});
} else { this.setState({ error: true }) }
} catch (e) { //code will jump here if there is a network problem
this.setState({ error: true });
}
};
render() {
const { userList, error } = this.state
return (
<div>
{userList.length > 0 && userList.map(user => (
<div key={user}>
<img src={user.picture.medium} alt="user"/>
<div>
<div>{user.name.first}{user.name.last}</div>
<div>{user.phone}</div>
<div>{user.email}</div>
</div>
</div>
))}
{error && <div>Sorry, can not display the data</div>}
</div>
)
}}
As best place and practice for external API calls is React Lifecycle method componentDidMount(), where after the execution of the API call you should update the local state to be triggered new render() method call, then the changes in the updated local state will be applied on the component view.
As other option for initial external data source call in React is pointed the constructor() method of the class. The constructor is the first method executed on initialization of the component object instance. You could see this approach in the documentation examples for Higher-Order Components.
The method componentWillMount() and UNSAFE_componentWillMount() should not be used for external API calls, because they are intended to be deprecated. Here you could see common reasons, why this method will be deprecated.
Anyway you must never use render() method or method directly called from render() as a point for external API call. If you do this your application will be blocked.
You must try "axios" library for API call.
Instead of direct using jQuery.
Thanks.
It would be great to use axios for the api request which supports cancellation, interceptors etc. Along with axios, l use react-redux for state management and redux-saga/redux-thunk for the side effects.

Subscriptions with react-meteor-data

From the docs, I wrote my container like so
export default InventoryItemsList = createContainer(() => {
const itemsCollection = Meteor.subscribe('allInventoryItems');
const loading = !itemsCollection.ready();
return {
loading,
items: !loading ? InventoryItems.find().fetch() : []
};
}, class InventoryItemsListComponent extends Component {
render() {
let items = this.props.items;
/* some render logic */
return /*... react component template ...*/ ;
}
});
The problem I see is that
The container function is executed many times, thus calling Meteor.subscribe more than once; is that good? Will Meteor just ignore subsequent subscriptions?
According to this tutorial, subscriptions need to be stopped, but the docs do not mention it at all. This does not take care on it's own, does it?
What is the recommended way to stop (i.e. unsubscribe) or resolve the 2 issues that I see from that point?
Is TrackerRact actually better? (yes, I know this is opinionated, but surely there is some form of a convention with meteor react, here!)
1) The container component is a reactive component, so whenever documents are changed or added to the given collection, it's going to call via your container and update the DOM.
2) As far as I know, the container will only subscribe to the collection via the actual component you bind it to. Once you leave that component, the subscriptions should stop.
If you want to unsubscribe directly, you can just call this.props.items.stop() in your componentWillUnmount() method.
Finally, I will have to say that using React specific implementations are always better than using Meteor specific functions (i.e. it's always better to use state variables over Sessions with React, as it's always better to use containers than Tracker.autorun() with React, etc, etc).
About your problem 2), this is how I solve it
1- When you subscribe to something, store those subscriptions references and pass them to the component.
Here an example with 2 subscriptions, but subscribing to only 1, is even easier.
createContainer((props) =>{
const subscription = Meteor.subscribe('Publication1', props.param1);
const subscription2 = Meteor.subscribe('Publication2', props.param1, props.param2);
const loading = !(subscription.ready() && subscription2.ready());
const results1 = loading ? undefined : Collection1.find().fetch();
const results2 = loading ? undefined : Collection2.findOne({a:1});
return {subscriptions: [subscription, subscription2], loading, results1, results2};
}, MyComp);
Then in my component:
class MyComp extends Component {
......
componentWillUnmount() {
this.props.subscriptions.forEach((s) =>{
s.stop();
});
}
....
}
This way, the component will get in props.subscriptions all the subscriptions it needs to stop before unmounting.
Also you can use this.props.loading to know if the subscriptions are ready (of course, you can have 2 different ready1 and ready2 if it helps).
Last thing, after subscribing, if you .find(), dont forget to .fetch(), else the results will not be reactive.

Categories

Resources