React function takes two button clicks to run - javascript

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

Related

Adding objects to an array of objects with useState hook in React then rendering

I'm working on web scraping a news website to show top headlines and URLs. In my backend, I put each title and URL in an object within an array, which works fine. When I fetch this information in React, it is not working as expected.
I used the useState hook to initialize an empty array, where I would put each object containing the title and URL. Then, I map through that array and render each title and URL to the page.
When I refresh the webpage, it takes several seconds for each title and URL to pop up, and additionally, they are all the same title and URL.
It seems that my array is not being updated properly by putting in the same article information each time I set the state. However, I do not understand why it is taking so long for all the information to show up on the webpage, and do not know how to make it a faster process. I want all the information to be on the page after the user hits refresh, not appear after several seconds at a time. Could anyone help me see where I'm going wrong?
Here is my code:
import {useState} from 'react';
const News = () => {
const [articles, setArticles] = useState([])
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => {
data.forEach(article => {
setArticles([...articles, {
title: article.article_title,
url: article.article_url}])
})
})
.catch(err => console.log(err))
return (
<div>
{articles.map(article => {
return (
<div className="indv-article">
<h1 className="article-title" key={article.title}>{article.title}</h1>
<p className='article-url' key={article.url}>{article.url}</p>
</div>);
})}
</div>
)
}
export default News
Couple of things that may solve your issues.
First: In React, all side-effects (such as data fetching, for example) should be handled inside a useEffect hook.
Second: As stated in #Khorne07's answer, the key attribute should be on the root DOM node that is being returned from the map.
Third: I don't really know the purpose of looping through your data to set the state. If the reason you are doing this is because the response contains other information that you are not interested to display and you just want the title and url for each article, I suggest you to create an adapter function that will receive this data as a parameter and return just the information that you are interested in.
Additional: You can use a loading state to show a loading indicator while the data is being fetched and improve user experience.
Putting it all together:
const News = () => {
const [articles, setArticles] = useState([])
const [loading, setLoading] = useState(false)
const adaptArticles = (data) => {
return data.map(({ article_title, article_url }) => ({
title: article_title,
url: article_url
}))
}
useEffect(() => {
setLoading(true)
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => setArticles((prevArticles) => prevArticles.concat(adaptArticles(data))))
.catch(err => console.log(err))
.finally(() => {
setLoading(false)
})
}, []) //Insert the corresponding dependencies (if any) in the dependencies array so the useEffect hook gets executed when any of these dependencies change.
if(loading) //Return some loading indicator, like a Spinner for example.
return (
<div>
{articles.map(article => {
return (
<div className="indv-article" key={article.title}>
<h1 className="article-title">{article.title}</h1>
<p className='article-url'>{article.url}</p>
</div>);
})}
</div>
)
}
Edited:
You have some errors on your current code:
First of all, and, as mentioned by the accepted answer, all side effects like fetch calls should be inside a useEffect hook.
The second error is related to the way you are updating your state array. When your new state depends on the previous state value, you should use the callback function inside your setState function, in order to have your data correctly synchronized with the previous value. And in this particular example you are also calling a setState function inside a loop, which is a bad idea and can potentially drive your app into unexpected behavior. The best approach is described in the code snippet bellow.
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => {
const articlesArray = []
data.forEach(article => {
articlesArray.push({
title: article.article_title,
url: article.article_url
})
setArticles(currentArticles => [...currentArticles, ...articlesArray])
})
})
.catch(err => console.log(err))
And on the map function, the key attribute should be on the root node you are returning:
{articles.map(article => {
return (
<div className="indv-article" key={article.title}>{article.title}>
<h1 className="article-title"</h1>
<p className='article-url'</p>
</div>);
})}

Re-render List after deleting item in child component

I want to build a dashboard for a blog. I have a page, listing all blog posts using a component for each list item. Now, inside each list item, I have a button to delete the post.
So far, everything is working. The post gets deleted, and if I reload the page, it is gone from the list. But I can't get it to re-render the page automatically, after deleting a post. I kind of cheated here using window.location.reload() but there has to be a better way?
This is my Page to build the list of all Posts
import {
CCol,
CContainer,
CRow,
CTable,
CTableHead,
CTableRow,
CTableHeaderCell,
CTableBody,
} from "#coreui/react";
import { useState, useEffect } from "react";
import DashboardSidebar from "../../components/dashboard/Sidebar";
import { getAllBlogPosts } from "../../services/blogService";
import BlogListItem from "../../components/dashboard/blog/BlogListItem";
import "./Dashboard.scss";
const AdminBlogListView = () => {
const [blogposts, setBlogposts] = useState([]);
useEffect(() => {
getBlogPosts();
}, []);
async function getBlogPosts() {
const response = await getAllBlogPosts();
setBlogposts(response.data);
}
// console.log(blogposts);
return (
<div className="adminContainer">
<div className="adminSidebar">
<DashboardSidebar />
</div>
<div className="adminContent">
<CContainer fluid>
<CRow className="mb-3">
<CCol>
<CTable>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col">#</CTableHeaderCell>
<CTableHeaderCell scope="col">Titel</CTableHeaderCell>
<CTableHeaderCell scope="col">Content</CTableHeaderCell>
<CTableHeaderCell scope="col"></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{blogposts.map((post) => {
return <BlogListItem key={post._id} post={post} />;
})}
</CTableBody>
</CTable>
</CCol>
</CRow>
</CContainer>
</div>
</div>
);
};
export default AdminBlogListView;
And this is the BlogListItem Component
import { useState, useEffect } from "react";
import {
CTableRow,
CTableHeaderCell,
CTableDataCell,
} from "#coreui/react";
import CIcon from "#coreui/icons-react";
import * as icon from "#coreui/icons";
import {
deleteBlogPost,
getBlogPostById,
// updateBlogPost,
} from "../../../services/blogService";
import { useNavigate } from "react-router-dom";
const BlogListItem = (props) => {
const id = props.post._id;
const [visible, setVisible] = useState(false);
const [post, setPost] = useState({
title: "",
content: "",
});
useEffect(() => {
getBlogPostById(id)
.then((response) => setPost(response.data))
.catch((error) => console.log(error));
}, []);
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
window.location.reload();
};
return (
<>
<CTableRow>
<CTableHeaderCell scope="row">1</CTableHeaderCell>
<CTableDataCell>{post.title}</CTableDataCell>
<CTableDataCell>{post.content}</CTableDataCell>
<CTableDataCell>
<CIcon
icon={icon.cilPencil}
size="lg"
onClick={() => setVisible(!visible)}
/>
<CIcon
icon={icon.cilTrash}
className="deleteButton"
size="lg"
color=""
onClick={handleDelete}
/>
</CTableDataCell>
</CTableRow>
</>
);
};
export default BlogListItem;
What can I do instead of window.location.reload() to render the AdminBlogListView after deleting an item? I tried using useNavigate() but that doesn't do anything
Thanks in advance :)
You can pass a reference to a function from the parent component AdminBlogListView into the child component BlogListItem, such that it is invoked when a blog post is deleted. That function will have the effect of either repopulating the blog posts or manually removing it from the data (that implementation bit is up to you).
Solution 1: Repopulate all blog posts on deletion
This is a quick fix with a bit of code smell (because you're essentially querying the server twice: once to delete the post and another to fetch posts again). However it is an escape-hatch type of situation and is simple to implement.
When you are rendering BlogListItem, we can pass a function, say onDelete, which will invoke getBlogPosts() to manually repopulate the blog posts from your server:
<BlogListItem key={post._id} post={post} onDelete={getBlogPosts} />
Then it is a matter of ensuring BlogListItem invokes onDelete() when deleting a blog post:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props
props.onDelete();
};
Solution 2: Delete a specific blog post by ID in the parent
Similar to the solution above, but ensure that you are passing a function from the parent that can delete a post by a specific ID (from the argument). This saves you an additional trip to the server.
In your component AdminBlogListView, define a function that can mutate the blogposts state by removing a blog post by ID. This can be done by leveraging functional updates:
const onDelete = (id) => {
setBlogposts((currentBlogPosts) => {
const foundBlogPostIndex = currentBlogPosts.findIndex(entry => entry._id === id);
// If we find the blog post with matching ID, remove it
if (foundBlogPostIndex !== -1) currentBlogPosts.splice(foundBlogPostIndex, 1);
return currentBlogPosts;
})
}
NOTE: The code above assumes that the blog post ID is stored in the _id key. I have simply inferred that from your code, since you have not shared the shape of the data.
Then in your BlogListItem component, it's the same logic as solution #1, but you need to pass the ID into it when invoking it:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props with post ID as an argument
props.onDelete(post._id);
};

setState() in multiple axios requests resulting in duplicates

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>
)
}

Use local state for forms when data is in redux

Use React for ephemeral state that doesn’t matter to the app globally
and doesn’t mutate in complex ways. For example, a toggle in some UI
element, a form input state. Use Redux for state that matters globally
or is mutated in complex ways. For example, cached users, or a post
draft.
My redux state is only representative of what has been saved to my backend database.
For my use case there is no need for any other part of the application to know about a record in an adding/editing state..
However, all my communication with my API is done through redux-thunks. I have found, getting data from redux into local state for editing is tricky.
The pattern I was trying to use:
const Container = () => {
// use redux thunk to fetch from API
useEffect(() => {
dispatch(fetchThing(id));
}, [dispatch, id]);
// get from redux store
const reduxThing = useSelector(getThing);
const save = thing => {
dispatch(saveThing(thing));
};
return (
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
);
};
const ThingForm = ({defaults, submit}) => {
const [values, setValues] = useState({ propA: '', propB: '', ...defaults});
const handleChange = { /*standard handleChange code here*/ };
return (
<form onSubmit={() => submit(values)}>
<input type="text" name="propA" value={values.propA} onChange={handleChange} />
<input type="text" name="propB" value={values.propB} onChange={handleChange} />
</form>
);
};
How I understand it, ThingForm is unmounted/mounted based upon "fetching." However, it is a race condition whether or not the defaults get populated. Sometimes they do, sometimes they don't.
So obviously this isn't a great pattern.
Is there an established pattern for moving data into local state from redux for editing in a form?
Or should I just put my form data into redux? (I don't know if this would be any easier).
EDIT: I think this is essentially what I am fighting: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
But no recommendation really clearly fits. I am strictly using hooks. I could overwrite with useEffect on prop change, but seems kind of messy.
EDIT:
const Container = () => {
// use redux thunk to fetch from API
useEffect(() => {
dispatch(fetchThing(id));
}, [dispatch, id]);
// get from redux store
const reduxThing = useSelector(getThing);
const save = thing => {
dispatch(saveThing(thing));
};
return (
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
);
};
const ThingForm = ({defaults, submit}) => {
const [values, setValues] = useState({ propA: '', propB: '', ...defaults});
const handleChange = { /*standard handleChange code here*/ };
useEffect(() => {
setValues({...values, ...defaults})
}, [defaults]);
const submitValues = (e) => {
e.preventDefault();
submit(values)
}
return (
<form onSubmit={submitValues}>
<input type="text" name="propA" value={values.propA} onChange={handleChange} />
<input type="text" name="propB" value={values.propB} onChange={handleChange} />
</form>
);
};
What you are doing is the right way, there's no reason why you should put the form data in the redux store. Like you said, "there is no need for any other part of the application to know about a record in an adding/editing state"
And that's correct.
The only problem you have is here:
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
Assuming fetching is true on every dispatch:
Instead of trying to hide the component (unmounting essentially), you should maybe use a spinner that overlays the page?
I don't know the rest of your code to comment on a better approach.
You also don't have to add dispatch to the dependency array
useEffect(() => {
dispatch(fetchThing(id));
}, [id]);
From the react docs:
React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

React hooks - useState() is not re-rendering the UI with the new state updates

I am trying out the new React Hooks, and I am a little stuck as the UI is not updating when the local state is updated. Here is my code,
import React, { useState, useEffect } from 'react';
import Post from './Post'
import PostForm from './PostForm';
import axios from 'axios';
function PostsList() {
const [posts, setPosts] = useState([]);
// setting up the local state using useEffect as an alternative to CDM
useEffect(() => {
axios.get('...')
.then(res => {
// the resposne is an array of objects
setPosts(res.data)
})
})
const handleSubmit = (data) => {
// the data I am getting here is an object with an identical format to the objects in the posts array
axios.post('...', data)
.then(res => {
// logging the data to validate its format. works fine so far..
console.log(res.data);
// the issue is down here
setPosts([
...posts,
res.data
])
})
.catch(err => console.log(err))
}
return (
<div>
<PostForm handleSubmit={handleSubmit} />
<h3>current posts</h3>
{ posts.map(post => (
<Post key={post.id} post={post} />
)) }
</div>
)
}
when I submit the form, the UI flickers for a split second and then renders the current state without the new update, it seems that something is preventing it from re-rendering the new state.
If more code/clarification is needed please leave a comment below.
thanks in advance.
alright, problem solved with the helpful hint from #skyboyer,
so what happened initially is, the useEffect() acts like componentDidMount() & componentDidUpdate() at the same time, that means whenever there is an update to the state, the useEffect() gets invoked, which means resetting the state with the initial data coming from the server.
to fix the issue I needed to make the useEffect() renders the component only one time when it's created/rendered as opposed to rendering it every time there is an update to the state. and this is done by adding an empty array as a second argument to the useEffect() function. as shown below.
useEffect(() => {
axios.get('...')
.then(res => {
setPosts(res.data)
})
}, [])
thanks :)

Categories

Resources