setState() in multiple axios requests resulting in duplicates - javascript

I'm trying to get some data from the github api, but I'm getting duplicate <Card /> components in my output. Here's the code of my App.js. My card component seems to be working fine.
import logo from './logo.svg';
import './App.css';
import React from 'react';
import Card from './Card';
import axios from 'axios';
class App extends React.Component {
state = {
users: [],
// Enter some github usernames for "followers"
followers: ["abc", "def", "ghi", "jkl", "mnop"]
}
componentDidMount() {
// Use initial github username for mainUser
axios.get(`https://api.github.com/users/mainUser`)
.then((resp)=> {
console.log(resp);
this.setState({
users: [...this.state.users, resp]
});
})
.catch(err=> console.log(err));
this.state.followers.map((user) => {
return(axios.get(`https://api.github.com/users/${user}`)
.then((resp)=> {
console.log(resp);
console.log(this.state.users);
this.setState({
users: [...this.state.users, resp]
});
}));
})
}
render() {
return(
<div className="container">
{
this.state.users.map(user => (
<Card key={Date.now()} user={user} />
))
}
</div>)
}
}
export default App;
I assume I'm failing to understand something about the lifecycle.

I think the main reason why it's failing for you is that you are using Date.now() as your key when rendering <Card /> components.
Keys used within arrays must be unique among their siblings (in this case, <Card /> components). This way React knows which items have been added/removed or changed. Plus, it prevents the unexpected behaviour that you are seeing.
Since the users that are returned from GitHub API include id, you could simply use that for your key. This way, React would render your <Card /> components without duplicates.
Also, in your axios request, instead of trying to add the whole response object to your user's state, try and destructure the data from the response and assign that to your users state. This way you will get only the user data that you need.
Plus, it's a good practice to use the previous state from setState() when assigning new state rather than getting the current state. This way you can be sure that the users state will have all the previous users and the new user that you are concatenating.
Taken all of this into account, the state assignment could look something like this:
.then(({ data }) => {
this.setState((prevState) => ({
users: [...prevState.users, data]
}));
})

As mentioned above, Date.now() makes for a poor component key.
You could also tidy up your Axios requests, including using the response data instead of the entire response object. Something like this
componentDidMount() {
const allUsers = ["mainUser", ...this.state.followers]
Promise.all(allUsers.map(user =>
axios.get(`https://api.github.com/users/${encodeURIComponent(user)}`)
.then(({ data }) => data)
)).then(users => {
this.setState({ users })
}).catch(err => console.error(err))
}
render() {
return(
<div className="container">
{
this.state.users.map(user => (
<Card key={user.id} user={user} />
))
}
</div>
)
}

Related

React function takes two button clicks to run

I have an array of Notes that I get from my database, the notes objects each have a category assigned to it. There are also buttons that allow the user to filter the notes by category and only render the ones with the corresponding one.
Now, it's all working pretty well but there's one annoying thing that I can't get rid of: whenever I click on any of the buttons: <button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>, the filterNotes() function is only called on the second click. I suspect it has to do something with me calling setState() twice, or maybe with the boolean that I set in the functions, but I tried various combinations to call the function on the first click, but to no avail so far.
Here's my MainArea code:
import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import Note from "./Note";
import axios from "axios"
function CreateArea(props) {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [filterOn, setFilter] = useState(false)
const [note, setNote] = useState({
title: "",
content: "",
category: ''
});
useEffect(() => {
fetch('http://localhost:5000/categories')
.then(res => res.json())
.then(json => setCategories(json))
}, [])
useEffect(() => {
if(fetchB) {
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log(json)
setNotes(json)
setFetch(false)
})
}
}, [fetchB])
function handleChange(event) {
const { name, value } = event.target;
console.log("handleChange called")
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(e){
e.preventDefault();
axios.post("http://localhost:5000/notes/add-note", note)
.then((res) => {
setNote({
category: '',
title: "",
content: ""
})
setFetch(true)
console.log("Note added successfully");
console.log(note)
})
.catch((err) => {
console.log("Error couldn't create Note");
console.log(err.message);
});
}
function expand() {
setExpanded(true);
}
function filterNotes(category){
if(filterOn){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log("filter notes")
setNotes(json)
setNotes(prevNotes => {
console.log("setNotes called with category " + category)
return prevNotes.filter((noteItem) => {
return noteItem.category === category;
});
});
setFilter(false)
})
}
}
return (
<div>
<Header/>
<ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFilter={setFilter} filterOn={filterOn} setFetch={setFetch}/>
<form className="create-note">
{isExpanded && (
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
)}
<textarea
name="content"
onClick={expand}
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows={isExpanded ? 3 : 1}
/>
<select
name="category"
onChange={handleChange}
value={note.category}>
{
categories.map(function(cat) {
return <option
key={cat.category} value={cat.value} > {cat.category} </option>;
})
}
</select>
<button onClick={submitNote}>Add</button>
</form>
<Note notes={notes} setFetch={setFetch}/>
<Footer/>
<button onClick={()=>{setFetch(true)}}>All</button>
</div>
);
}
export default CreateArea;
And ListCategories where I get call the function and get the chosen category from the buttons:
import React, { useState } from "react";
import CreateCategory from "./CreateCategory";
export default function ListCategories(props) {
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
return (
<div className="category-group">
<CreateCategory/>
<div className="btn-group">
{props.categories.map((categoryItem, index) =>{
return(
<button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>
)
})}
</div>
</div>
)
}
I'm not sure what the best practice is with such behaviour - do I get the notes from the database each time as I'm doing now or should I do something completely different to avoid the double-click function call?
Thanks in advance for any suggestions!
Your issue is this function:
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
Understand that in React, state is only updated after the current execution context is finished. So in handleClick() when you call setFiler(), that linked filterOn state is only updated when the rest of the function body finishes.
so when your filterNotes() function is called, when it evaluates filterOn, it is still false, as it was initially set. After this function has executed, the handleClick() function has also finished, and after this, the filterOn state now equals true
This is why on the second click, the desired rendering effect occurs.
There are multiple ways to get around this, but I normally use 'render/don't-render' state by including it as an embedded expression in the JSX:
<main>
{state && <Component />}
</main>
I hope this helps.
You diagnosed the problem correctly. You shouldn't be using state like you would a variable. State is set asynchronously. So, if you need to fetch some data and filter it, do that and THEN add the data to state.
function filterNotes(category){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
const filtered = json.filter((noteItem) => (noteItem.category === category));
setNotes(filtered);
})
}
}
It's not clear to me why you would need the filterOn state at all.
Depending on how your frequently your data is updated and if you plan on sharing data across users, the answer to this question will vary.
If these notes are specific to the user then you should pull the notes on load and then store them in a local state or store. Write actions that can update the state or store so that this isn't coupled with your react UI rendering. Example: https://redux.js.org/ or https://mobx.js.org/README.html.
Then update that store and your remote database accordingly through dispatching actions. This avoids lots of calls to the database and you can perform your filtering client-side as well. You can then also store data locally for offline use through this method so if it's for a mobile app and they lose internet connection, it'll still render. Access the store's state and update your UI based on that. Specifically the notes and categories.
If you have multiple users accessing the data then you'll need to look at using websockets to send that data across clients in addition to the database. You can add listeners that look for this data and update that store or state that you will have created previously.
There are many approaches to this, this is just an approach I would take.
You could also create a context and provider that maintains your state on the first load and persists after that. Then you can avoid passing down state handlers through props

infinite api calls in componentDidUpdate(prevProps) despite conditional that compares current prop with previous prop

My problem is in my subcomponent files where every state update in the parent component keeps re-rendering the subcomponents infinitely by making infinite api calls with the default or updated props value passed to the child components. I have a User directory page which contains multiple components in a single page.
class Users extends Component {
constructor(props) {
super(props);
this.state = {
user: "",
listOfUsers: [],
socialData:[],
contactData:[],
videoData:[],
detailsData:[]
}
}
componentDidMount(){
//api call that gets list of users here
//set response data to this.state.listOfUsers
}
userHandler = async (event) => {
this.setState({
user: event.target.value,
});
};
render() {
return (
<div>
<div>
<select
value={this.state.user}
onChange={this.userHandler}
>
// list of users returned from api
</select>
</div>
<div>
<Social Media user={this.state.user} />
<Contact user={this.state.user}/>
<Video user={this.state.user}/>
<Details user={this.state.user}/>
</div>
</div>
);
}
}
I have 1 API call for the parent component Users, and 4 for each of the subcomponents: Social Media, Contact, Video, and Details. The Users api will return a list of users in a dropdown and the value of the user selected is then fed to the other four API's. i.e. https://localhost:3000/social_media?user=${this.state.user}. Thus, the four subcomponents' API is dependent on the Users API. I currently have the parent api call in a componentDidMount() and the other 4 api calls in their respective subcomponents and use props to pass down the value of the user selected in the parent to the subcomponents. Each of the api calls is in a componentDidUpdate(prevProps). All the subcomponents follow this structure:
class Social Media extends Component {
constructor(props) {
super(props);
this.state = {
user: "",
socialData:[],
}
}
componentWillReceiveProps(props) {
this.setState({ user: this.props.user })
}
componentDidUpdate(prevProps){
if (this.props.user !== prevProps.user) {
// make api call here
fetch (`https://localhost:3000/social_media?user=${this.state.user}`)
.then((response) => response.json())
.catch((error) => console.error("Error: ", error))
.then((data) => {
this.setState({ socialData: Array.from(data) });
}
}
render() {
return (
{this.socialData.length > 0 ? (
<div>
<Social Media data={this.state.socialData}/>
</div>
)
:
(<div> Loading ... </div>)
);
}
}
Abortive attempt to answer your question
It's hard to say exactly what's going on here; based on the state shown in the Users component, user should be a string, which should be straightforward to compare, but clearly something is going wrong in the if (this.props.user !== prevProps.user) { comparison.
If we could see the results of the console.log(typeof this.props.user, this.props.user, typeof prevProps.user, typeof prevProps.user) call I suggested in my comment, we'd probably have a better idea what's going on here.
Suggestions that go beyond the scope of your question
Given the moderate complexity of your state, you may want to use some sort of shared state like React's Context API, Redux, or MobX.. I'm partial toward the Context API, as it's built into React and requires relatively less setup.
(Then again, I also prefer functional components and hooks to classes and componentDidUpdate, so my suggestion may not apply to your codebase without a rewrite.)
If this.props.user is an object, then this.props.user !== prevProps.user will evaluate to true because they are not the same object. If you want to compare if they have the same properties and values (i.e. they are shallowly equal) you can use an npm package like shallow-equal and do something like:
import { shallowEqualObjects } from "shallow-equal";
//...
componentDidUpdate(prevProps){
if (!shallowEqualObjects(prevProps.user, this.props.user)) {
// make api call here
fetch (`https://localhost:3000/social_media?user=${this.state.user}`)
.then((response) => response.json())
.catch((error) => console.error("Error: ", error))
.then((data) => {
this.setState({ socialData: Array.from(data) });
}
}

How to handle routes in React which exist byt may not have data if user enters it straight into URL

There is React router route in Main.js as the error indicates line:95:
"/article/:article_id"
const Main = props => {
const { authUser, errors, removeError, currentUser } = props;
return(
<div className="">
<Switch>
<Route exact path="/article/:article_id" render={props => {
return (
<ArticleDetails
currentUser={currentUser}
{...props}
/>
)
}} />
// lots of other routes
{/* Not found component */}
<Route component={PageNotFound} />
</Switch>
</div>
);
};
// and some redux state
function mapStateToProps(state) {
return {
currentUser: state.currentUser,
errors: state.errors
};
};
export default withRouter(connect(mapStateToProps, { authUser, removeError })(Main));
So, for example, there can be "/article/5" but there might not be "/article/6", article with an id of 6.
I have a default component that loads when the route does not exist, but in this case, the route does exist, only dynamic data may not.
If article request returns no article when user types "/article/6/" in URL, he does get redirected back to "/articles" with "this.props.history.push('/articles');".
ArticleDetails component fetches all data of an article, while parent component ArticlesList, has only few details with the link to ArticleDetails page, it does not pass article as prop.
class ArticleDetails extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
article: null,
lists: [],
showBookmark: false,
showPdf: false,
showDelete: false
};
// binded methods...
}
componentDidMount(){
this._isMounted = true;
if (this._isMounted) {
let id = this.props.match.params.article_id;
axios.get('/api/article/' + id)
.then(res => {
this.setState({
article: res.data.article
});
return res.data.article;
})
.then(article => {
if(!article)
{
this.props.history.push('/articles');
}
else if(this.props.currentUser.isAuthenticated)
{
return apiCall("post", `/api/article/${id}/checklike`)
.then(res => {
let newState = Object.assign({}, this.state);
newState.article.isLiked = res.isLiked;
this.setState(newState);
});
}
})
.then( res => {
let newState = Object.assign({}, this.state);
if(this.props.currentUser.isAuthenticated)
{
newState.article.comments.map(comment => {
let temp = comment.likes.find(like => like.user_id ===
this.props.currentUser.user.id)
if(temp) { comment.isLiked = true}
else { comment.isLiked = false}
})
this.setState(newState);
}
})
.catch(err => {
return <Redirect to='/articles' />
});
this.getUserArticleLists();
}
};
// and also a small state
const mapStateToProps = state => ({})
export default connect(mapStateToProps)(ArticleDetails);
but the user also gets this warning of a memory leak which sounds not cool and may signal that I'm doing something incorrectly. I added _isMounted property but I'm either doing it wrong or it does not help.
componentWillUnmount() {
this._isMounted = false;
}
"index.js:1 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.
in ArticleDetails (created by ConnectFunction)
in ConnectFunction (at Main.js:95)"
Question is how to handle situations when component is loaded and only then it is known that the data does not exist or maybe the user is not allowed to access it and redirect the user properly? Am I missing some sort of middleware on the React that I should be using for such cases? or How should be URL access even handled in such cases?
P.S I'm new to React, I have a little redux store, but not getting very deep into it at the moment, trying to solve just by using react, as I'm currently doing the project which has almost no time left. But for future improvement, I accept all answers, feedback, I might not know basic practises so I appreciate every comment.

Fetch one JSON object and use it reactjs

I'm very new to JS and ReactJS and I try to fetch an endpoint which gives me a JSON Object like this :
{"IDPRODUCT":4317892,"DESCRIPTION":"Some product of the store"}
I get this JSON Object by this endpoint :
http://localhost:3000/product/4317892
But I dont how to use it in my react application, I want to use those datas to display them on the page
My current code looks like this but it's not working and I'm sure not good too :
import React, {Component} from 'react';
class Products extends Component {
constructor(props){
super(props);
this.state = {
posts: {}
};
};
componentWillMount() {
fetch('http://localhost:3000/product/4317892')
.then(res => res.json())
.then(res => {
this.setState({
res
})
})
.catch((error => {
console.error(error);
}));
}
render() {
console.log(this.state)
const { postItems } = this.state;
return (
<div>
{postItems}
</div>
);
}
}
export default Products;
In the console.log(this.state) there is the data, but I'm so confused right now, dont know what to do
Since I'm here, I have one more question, I want to have an input in my App.js where the user will be able to type the product's id and get one, how can I manage to do that ? Passing the data from App.js to Products.js which is going to get the data and display them
Thank you all in advance
Your state doesn't have a postItems property which is considered undefined and react therefore would not render. In your situation there is no need to define a new const and use the state directly.
Also, when you setState(), you need to tell it which state property it should set the value to.
componentWillMount() {
fetch('http://localhost:3000/product/4317892')
.then(res => res.json())
.then(res => {
this.setState({
...this.state, // Not required but just a heads up on using mutation
posts: res
})
})
.catch((error => {
console.error(error);
}));
}
render() {
console.log(this.state)
return (
<div>
<p><strong>Id: {this.state.posts.IDPRODUCT}</strong></p>
<p>Description: {this.state.posts.DESCRIPTION}</p>
</div>
);
}
I have got 3 names for the same thing in your js: posts, postItems and res.
React can not determine for you that posts = postItems = res.
So make changes like this:
-
this.state = {
postItems: {}
};
-
this.setState({
postItems: res
});
-
return (
<div>
{JSON.stringify(postItems)}
<div>
<span>{postItems.IDPRODUCT}</span>
<span>{postItems.DESCRIPTION}</span>
</div>
</div>
);
{postItems["IDPRODUCT"]}
Will display the first value. You can do the same for the other value. Alternatively, you can put
{JSON.stringify(postItems)}
With respect to taking input in the App to use in this component, you can pass that input down through the props and access it in this component via this.props.myInput. In your app it'll look like this:
<Products myInput={someInput} />

Access query inside return method

I have a backend Drupal site and react-native app as my frontend. I am doing a graphQL query from the app and was able to display the content/s in console.log. However, my goal is to use a call that query inside render return method and display it in the app but no luck. Notice, I have another REST API call testName and is displaying in the app already. My main concern is how to display the graphQL query in the app.
Below is my actual implementation but removed some lines.
...
import gql from 'graphql-tag';
import ApolloClient from 'apollo-boost';
const client = new ApolloClient({
uri: 'http://192.168.254.105:8080/graphql'
});
client.query({
query: gql`
query {
paragraphQuery {
count
entities {
entityId
...on ParagraphTradingPlatform {
fieldName
fieldAddress
}
}
}
}
`,
})
.then(data => {
console.log('dataQuery', data.data.paragraphQuery.entities) // Successfully display query contents in web console log
})
.catch(error => console.error(error));
const testRow = ({
testName = '', dataQuery // dataQuery im trying to display in the app
}) => (
<View>
<View>
<Text>{testName}</Text> // This is another REST api call.
</View>
<View>
<Text>{dataQuery}</Text>
</View>
</View>
)
testRow.propTypes = {
testName: PropTypes.string
}
class _TestSubscription extends Component {
...
render () {
return (
<View>
<FlatList
data={this.props.testList}
...
renderItem={
({ item }) => (
<testRow
testName={item.field_testNameX[0].value}
dataQuery={this.props.data.data.paragraphQuery.entities.map((dataQuery) => <key={dataQuery.entityId}>{dataQuery})} // Here I want to call the query contents but not sure how to do it
/>
)}
/>
</View>
)
}
}
const mapStateToProps = (state, ownProps) => {
return ({
testList: state.test && state.test.items,
PreferredTest: state.test && state.test.PreferredTest
})
}
...
There are few different things that are wrong there.
Syntax error is because your <key> tag is not properly closed here:
(dataQuery) => <key={dataQuery.entityId}>{dataQuery})
And... there is no <key> element for React Native. You can check at docs Components section what components are supported. Btw there is no such an element for React also.
Requesting data is async. So when you send request in render() this method finishes execution much earlier before data is returned. You just cannot do that way. What can you do instead? You should request data(in this element or its parent or Redux reducer - it does not matter) and after getting results you need to set state with .setState(if it happens inside the component) or .dispatch(if you are using Redux). This will call render() and component will be updated with data retrieved. There is additional question about displaying spinner or using other approach to let user know data is still loading. But it's orthogonal question. Just to let you know.
Even if requesting data was sync somehow(for example reading data from LocalStorage) you must not ever do this in render().This method is called much more frequently that you can expect so making anything heavy here will lead to significant performance degradation.
So having #3 and #4 in mind you should run data loading/fetching in componentDidMount(), componentDidUpdate() or as a part of handling say button click.

Categories

Resources