I have a React app with buttons that fetch recent bills and top donors for each Congress member. Both actions (fetchBillsByRep and getRepFinances) fetch correct data and correctly update Redux state. BUT. While fetching bills results in immediate re-render, fetching donors does not.
I have read more than a dozen answers to similar questions, and have tried the solutions. I am not mutating state; I always make a new copy. I am hitting debuggers where I expect to - and getting all values as expected.
inside MemberCard component, handleDonorsClick
//donors
handleDonorsClick = () => {
let id = this.props.crp_id
if (this.props.chamber === "senate"){
this.props.getSenatorFinances(id)
} else if (this.props.chamber === "house"){
this.props.getRepFinances(id)
}
this.setState({ showDonors: true })
debugger
//showDonors = true
}
HouseActions action creators: all values as expected:
export function fetchBillsByRep(id){
return (dispatch) => {
return fetch(API_BASE_URL+'/search/bills/member/'+id)
.then(resp => resp.json())
.then(bills => {
if (!bills.error) {
dispatch({type:"FETCH_BILLS_BY_REP", payload: { bills: bills, id:id}})
} else {
alert(bills.error.fullMessage)
}
}
).catch(error => alert(error))
}
}
export function getRepFinances(id){
return (dispatch) => {
return fetch(API_BASE_URL+'/search/financial_disclosures/member/'+id)
.then((resp) => resp.json())
.then(financialDisclosure => {
if (!financialDisclosure.error) {
dispatch({
type:"GET_REP_FINANCES",
payload: { financialDisclosure: financialDisclosure, id:id }})
} else {
alert(financialDisclosure.error.fullMessage)
}
}
).catch(error => alert(error))
}
}
house.js reducer:
again, all values as expected
export default (state = [], action) => {
switch(action.type){
case "SET_HOUSE":
return action.house
//<truncated>
case "FETCH_BILLS_BY_REP":
let bills = action.payload.bills
let house = state.map(rep => {
//find rep update bills
if (rep.propublica_id === action.payload.id){
rep.bills = bills
}
return rep
}
)
return house
case "GET_REP_FINANCES":
let financialDisclosure = action.payload.financialDisclosure
let house1 = state.map(rep => {
if (rep.crp_id === action.payload.id){
rep.financialDisclosure = financialDisclosure
rep.donors = financialDisclosure.donors
}
console.log(rep.donors)
return rep
//rep now has donors under rep.donors
}
)
return house1
//house1 is being returned as the new state for house
//inside house1, rep has updated rep.donors
default:
return state;
}
}
Redux DevTools State:Diff Tab after clicking get donors button:
(truncated, just showing top 2)
house
51
donors
0{
"id": 621,
"org_name": "Buchanan, Ingersoll & Rooney",
"total": 21600,
"pacs": 0,
"indivs": 21600
}
1{
"id": 622,
"org_name": "Telacu",
"total": 18900,
"pacs": 0,
"indivs": 18900
}
MemberCard component logic for deciding whether to render bills or donors:
//format back of card
let align
let content
let space
if (this.state.showBills){
image = ""
align = "center"
space = <br/>
content =
<>
<MemberBills
member={this.props}
showBills={this.state.showBills}/>
{hideBillsButton}
</>
} else if (this.state.showDonors){
//debugger hits, this.state.showDonors = true
image = ""
align = "center"
space = <br/>
content =
<>
<MemberDonors
member={this.props} showDonors={this.state.showDonors}/>
{/*member has value, showDonors is true*/}
{hideDonorsButton}
{/*hideDonorsButton has value*/}
</>
MemberDonors render:
<div>
<br/>
<hr/>
<h4 className="center">Top Three Donors</h4>
{popUpNotice}
{donorList}
{/* donorList has correct values */}
<br/>
{donorsSource}
{/* donorsSource has correct values */}
<br/>
<br/>
</div>
I don't get any errors. Instead, I get the MembersDonors view, showing "Top Three Donors", the popUp notice ... but no donorList. However, if I refresh the page, find that particular Member again, and re-click the showDonors button, the donors appear.
I am following the same pattern I used for fetching and showing bills.
Any ideas what I'm missing?
You didn't show your reducer in its entirety, but I bet it's because you are mutating your state, not creating a new version.
a = {}
b = a
b.d = 10
console.log(a)
console.log(b)
console.log(a === b)
What you need is to return a new copy of your state, which can be done using Object.assign or the spread operator
a = {}
b = {...a}
b.d = 10
console.log(a)
console.log(b)
console.log(a === b)
So in short, the easiest way to fix your code is to make a copy in your return statement for all of your cases.
return {...rep}
Related
I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);
I try to show my value using checkbox. Value always comes for the console log. But it didn't set for the checkbox. Here is the code and image for my problem:
var NotePage = createClass({
addTags(e) {
console.log("id****************", e.target.id);
let id = e.target.id;
let selectedTags = this.state.selectedTags;
if (selectedTags.includes(id)) {
var index = selectedTags.indexOf(id)
selectedTags.splice(index, 1);
} else {
selectedTags.push(id);
}
console.log("id****************selectedTags", selectedTags);
this.setState({
selectedTags: selectedTags
})
},
render: function () {
assignStates: function (note, token, tagCategories) {
let fields = [];
fields["title"] = note.title_en;
fields["body"] = note.body_en;
let selectedFileName = null
if (note.file_url_en != "") {
console.log("note.file_url_en ", note.file_url_en);
selectedFileName = note.file_url_en
}
let selectedTags = [];
let n = 0;
(note.note_tag).forEach(tag => {
selectedTags.push(tag.id.toString());
n++;
});
console.log("id****************first", selectedTags);
let initial_values = {
note: note,
id: note.id,
api: new Api(token),
message: "",
title: note.title_en,
body: note.body_en,
fields: fields,
isEdit: false,
selectedTags: selectedTags,
tagCategories: tagCategories,
selectedFileName: selectedFileName,
}
return initial_values;
},
const { selectedTags } = this.state;
{(tagCategory.tags).map((tag) => (
<div className="col-3">
<div>
<input
type="checkbox"
value={selectedTags.includes(tag.id)}
id={tag.id}
onChange={this.addTags} />
<label style={{ marginLeft: "10px", fontSize: "15px" }}>
{tag.name_en}
</label>
</div>
</div>
))
}
})
Image related for the problem
You've an issue with state mutation. You save a reference to the current state, mutate it, and then save it back into state. This breaks React's use of shallow reference equality checks during reconciliation to determine what needs to be flushed to the DOM.
addTags(e) {
let id = e.target.id;
let selectedTags = this.state.selectedTags; // reference to state
if (selectedTags.includes(id)) {
var index = selectedTags.indexOf(id)
selectedTags.splice(index, 1); // mutation!!
} else {
selectedTags.push(id); // mutation!!
}
this.setState({
selectedTags: selectedTags // same reference as previous state
});
},
To remedy you necessarily return a new array object reference.
addTags(e) {
const { id } = e.target;
this.setState(prevState => {
if (prevState.selectedTags.includes(id)) {
return {
selectedTags: prevState.selectedTags.filter(el => el !== id),
};
} else {
return {
selectedTags: prevState.selectedTags.concat(id),
};
}
});
},
Use the "checked" attribute.
<input
type="checkbox"
value={tag.id}
checked={selectedTags.includes(tag.id)}
id={tag.id}
onChange={this.addTags} />
also, about the value attribute in checkboxes:
A DOMString representing the value of the checkbox. This is not displayed on the client-side, but on the server this is the value given to the data submitted with the checkbox's name.
Note: If a checkbox is unchecked when its form is submitted, there is
no value submitted to the server to represent its unchecked state
(e.g. value=unchecked); the value is not submitted to the server at
all. If you wanted to submit a default value for the checkbox when it
is unchecked, you could include an inside the
form with the same name and value, generated by JavaScript perhaps.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#value
I think you should use checked property instead of value.
For reference check react js docs here
You are mutating state variable directly with selectedTags.splice(index, 1); and selectedTags.push(id);
What you need to do is make a copy of the state variable and change that:
addTags(e) {
let id = e.target.id;
if (this.state.selectedTags.includes(id)) {
this.setState(state => (
{...state, selectedTags: state.selectedTags.filter(tag => tag !== id)}
))
} else {
this.setState(state => (
{...state, selectedTags: [...state.selectedTags, id]}
))
}
}
I cant understand why my renderMovies() function dont wanna update my component state.data and i cant render component on my screen ?!
Everithing goes ok until renderMovies function.. I think this.setState(newState) in my fetchPostData function is working incorrect... Do somebody know how to fix it? I tried different ways but i cant solve this issue.
class Movies extends React.Component {
constructor(props) {
super(props)
this.state = { data: {}}
this.fetchPostData = this.fetchPostData.bind(this)
this.renderMovies = this.renderMovies.bind(this)
this.populatePageAfterFetch = this.populatePageAfterFetch.bind(this)
}
componentDidMount() {
this.fetchPostData()
}
fetchPostData() {
fetch(`http://localhost/reacttest/wp-json/wp/v2/movies?per_page=100`)
.then(response => response.json())
.then(myJSON => {
let objLength = Object.keys(myJSON).length
let newState = this.state;
for (let i = 0; i < objLength; i++) {
let objKey = Object.values(myJSON)[i].title.rendered;
// console.log(objKey)
let currentMovie = newState.data[objKey];
currentMovie = {};
currentMovie.name = Object.values(myJSON)[i].title.rendered;
currentMovie.description = Object.values(myJSON)[i].content.rendered;
currentMovie.featured_image = Object.values(myJSON)[i]['featured_image_url'];
currentMovie.genre = Object.values(myJSON)[i]['genre'];
}
this.setState(newState)
})
}
renderMovies() {
if(this.state.data) {
const moviesArray = Object.values(this.state.data)
console.log(moviesArray)
return Object.values(moviesArray).map((movie, index) => this.populatePageAfterFetch(movie, index))
}
}
populatePageAfterFetch(movie, index) {
if (this.state.data) {
return (
<div key={index} index={index}>
<h2>{movie.title}</h2>
<h3>{movie.genre}</h3>
<p>{movie.description}</p>
</div>
)
}
}
render() {
return (
<div>
<h1>Movies</h1>
<div>{this.renderMovies()}</div>
</div>
)
}
}
When i try to console.log(moviesArray) it show me:
Issue
You save current state into a variable named newState, never update it, and then save the same object reference back into state. React state never really updates.
let newState = this.state;
for (let i = 0; i < objLength; i++) {
...
}
this.setState(newState);
Additionally you mutate state
let currentMovie = newState.data[objKey];
currentMovie = {};
But this doesn't work either since initial state is an empty object so newState.data[objKey] is aways undefined. (so nothing is ever actually mutated)
Solution
It appears as though you intended to map the myJSON data/values into movie objects to update this.state.data. May I suggest this solution. The key is to always create new object references for any object you update.
fetchPostData() {
fetch(`http://localhost/reacttest/wp-json/wp/v2/movies?per_page=100`)
.then(response => response.json())
.then(myJSON => {
this.setState(prevState => ({
// array::reduce over the JSON values
data: Object.values(myJSON).reduce((movies, movie) => {
// compute movie key
const name = movie.title.rendered;
return {
...movies,
[name]: {
...movies[name], // copy any existing movie properties
// merge in new/updated properties
name,
description: movie.content.rendered,
featured_image: movie.featured_image_url,
genre: movie.genre,
},
}
}, { ...prevState.data }) // use previous state as initial value for reduce
}))
})
}
I have this program that brings an article from my data base in componentDidMount(), fragmentedArticle() grabs each word and put it in this.state.fragmented and each word is put it in a span tag in this.state.fragmentedTags
I print the article in grey color text, but I want to change the style color property of the text (with a setTimeout every 1000 milliseconds) but I don't know if it's posible to changed a property of a tag that is save it in the react state.
import React, { Component } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
export default class ArticleDetails extends Component {
constructor(props) {
super(props);
this.state = {
id: '',
title: '',
article: '',
date: new Date(),
lenguages: [],
articles: [],
fragmented: [],
fragmentedTags: [],
showSpans: false,
counterSpaces: 0,
}
this.deleteArticle = this.deleteArticle.bind(this);
this.fragmentedArticle = this.fragmentedArticle.bind(this);
this.coloredArticle = this.coloredArticle.bind(this);
}
componentDidMount() {
this.setState({
id: this.props.match.params.id
})
// get individual exercise.
axios.get('http://localhost:5000/articles/'+ this.props.match.params.id)
.then(response => {
this.setState({
title: response.data.title,
article: response.data.article,
duration: response.data.duration,
date: new Date(response.data.date)
})
})
.catch(function (error) {
console.log(error);
})
// get all lenguages.
axios.get('http://localhost:5000/lenguages/')
.then(response => {
if (response.data.length > 0) {
this.setState({
lenguages: response.data.map(lenguage => lenguage.lenguage),
})
}
}).catch( error => console.log(error) )
}
deleteArticle( id ) {
axios.delete( 'http://localhost:5000/articles/' + id )
.then( res => console.log( res.data ) );
this.setState({
articles: this.state.articles.filter( el => el._id !== id )
}
)
}
fragmentedArticle = () => {
let length = this.state.article.length;
let word = [];
let fragmentedArticle = [];
let counter = 0;
let p1, p2 = 0;
for (let x = 0; x <= length; x++) {
word[x] = this.state.article[x];
if( this.state.article[x] === ' ' || this.state.article[x] === "\n" ){
p2 = x;
fragmentedArticle[counter] = word.join('').substr(p1,p2);
p1 = p2
p2 = 0;
counter++;
}
}
// we save each word
this.setState({
fragmented: fragmentedArticle,
counterSpaces: counter,
showSpans: !this.state.showSpans,
})
// we save each word wrapped in a span tag with a property of color grey.
this.setState( prevState => ({
fragmentedTags: prevState.fragmented.map( (name, index) =>
<span key={ index } style={{color:'grey'}} >{name}</span>
)
}))
}
coloredArticle = () => {
console.log(this.state.fragmentedTags[0].props.style.color);
// I see the actual value color style property of the span tag (grey) but I want to change it on green from the this.state.fragmentedTags[0] to the last word within a x period of time with the setTimeout js method.
// this code bellow change the color but not one by one.
this.setState( prevState => ({
fragmentedTags:
// map all the elements
prevState.fragmented.map( (name, index) =>
// with a delay of 1500 milliseconds
setTimeout(() => {
<span key={ index } style={{color:'green'}} >{name}</span>
}, 1500)
)
})
)
}
render(props) {
const displaySpan = this.state.showSpans ? 'inline-block' : 'none';
const {fragmentedTags} = this.state
return (
<div>
<h6>{ this.state.title }</h6>
{/* this show/hide the article text */}
<p onClick={ this.fragmentedArticle }>Show</p>
{/* I want to changed the text color one by one within a period of time (velocity, setTimeout) */}
<p onClick={ this.coloredArticle }>Play</p>
{/* Show us the full article (each word wrapped in a span with its property) */}
<div style={{ display:displaySpan }}>
{ fragmentedTags }
</div>
</div>
)
}
}
You shouldn't be transforming state like that. It gets very difficult to debug your application and makes it much more difficult to do simple things.
Download your articles and save them into state but if you need to make any other changes save it into a new part of state rather than overwriting current state. Most likely you do not need to save transformations into state though.
To answer your question, I would set a timestamp for each article and once its downloaded set a timer that will rerender the article with the new changes if sufficient time has passed.
As I am starting my experience with Firebase I am a little bit struggling with the pagination of posts on my blog website :(.
I think I kind of understood the docs from Google and I know how to move the pagination to the next page. However, I have absolutely no clue how to paginate back to the previous page.
Basically, I wanted to have a simple pagination component which will look something like that: < 1 2 3 [...] > (where you can paginate next and back using the arrows).
It is fine to paginate to the next page but, when it comes to paginating back I cannot find any proper tutorial to do it in pure React.
I have tried to use various methods from startAt, endAt, endBefore etc. But the result was or an error or it was moving me back to the first page (even when I was on the third or fourth)
I even tried to find the first object in an array and use it as endBefore but it resulted again in paginating back to the first page.
That's how my code looks right now (yes I know that pageNext() and pagePrev() are the same)
import React, { Component } from 'react'
import { withFirebase } from './Firebase'
import Post from './Post'
import '../scss/Post.scss'
class Posts extends Component {
constructor(props) {
super(props);
this.state = {
loading:false,
posts:[],
post_id:[],
lastVisible:null,
limit:2
}
this.handlePageNext = this.handlePageNext.bind(this);
}
componentDidMount() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.limit(2)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible: lastVisible});
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
handlePageNext() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.startAt(this.state.lastVisible)
.limit(this.state.limit)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible:lastVisible });
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
handlePagePrev() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.startAt(this.state.lastVisible)
.limit(this.state.limit)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible:lastVisible});
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
render() {
return (
<div className='posts'>
<div className='row'>
{this.state.posts.map((post, i) => (
<Post
key={i}
title={post.title}
author={post.author}
desc={post.desc}
text={post.text}
id={this.state.post_id[i]}
date={post.date}
imgURL={post.imgURL}/>
))}
{this.state.loading && <p>Loading...</p>}
<button className='btn' onClick={() => this.handlePagePrev()}>←</button>
<button className='btn' onClick={() => this.handlePageNext()}>></button>
</div>
</div>
)
}
}
export default withFirebase(Posts);
I wanted to have a simple pagination using buttons (left and right arrows) but I am struggling with it for already 3rd hour and cannot find the proper solution to this.
You have to keep the "lastVisible" and pass it to startAfter(). 2 functions I wrote below:
export const getMostRecentPostsFirstPage = (limit, specificUserId) => {
if (!Number.isInteger(limit) || limit < 1) {
throw new Error('limit must be a positive integer');
}
const collection = Firestore.collection('posts');
let query = null;
if (specificUserId) {
query = collection
.where('userId', '==', `${specificUserId}`)
.orderBy('postedTimestamp', 'desc')
.limit(limit);
} else {
query = collection.orderBy('postedTimestamp', 'desc').limit(limit);
}
return new Promise((resolve, reject) => {
const posts = [];
query
.get()
.then(snapshot => {
const lastVisible = snapshot.docs[snapshot.docs.length - 1];
snapshot.forEach(post => {
posts.push(post.data());
});
const hasMore = posts.length == limit;
resolve({ posts: posts, lastVisible: lastVisible, hasMore: hasMore });
})
.catch(error => reject(error));
});
};
export const getMostRecentPostsNextPage = (lastVisible, limit, specificUserId) => {
if (!lastVisible) {
throw new Error('Need to provide lastVisible argument');
}
if (!Number.isInteger(limit) || limit < 1) {
throw new Error('limit must be a positive integer');
}
const collection = Firestore.collection('posts');
let query = null;
if (specificUserId) {
query = collection
.where('userId', '==', `${specificUserId}`)
.orderBy('postedTimestamp', 'desc')
.startAfter(lastVisible)
.limit(limit);
} else {
query = collection
.orderBy('postedTimestamp', 'desc')
.startAfter(lastVisible)
.limit(limit);
}
return new Promise((resolve, reject) => {
const posts = [];
query
.get()
.then(snapshot => {
const lastVisible = snapshot.docs[snapshot.docs.length - 1];
snapshot.forEach(post => {
posts.push(post.data());
});
const hasMore = posts.length == limit;
resolve({ posts: posts, lastVisible: lastVisible, hasMore: hasMore });
})
.catch(error => reject(error));
});
};
It uses redux-saga, but you get the idea.
on first query, do not call "startAfter()", but do on the subsequent queries, and you must save "lastVisible" between each call.
Here is standard pagination by using Firebase in reactjs.