React render not not updating after setting state with Firebase data - reactjs

I am trying to go into my Firebase database and save all groups that a user belongs to to an array then push that array to the components state and render a list of items accordingly but my render is not showing the items from the state.
componentDidMount(){
this.setGroups();
}
setGroups(){
const user = fire.default.auth().currentUser.uid;
const dbRef = fire.default.database();
const currentUserRef = dbRef.ref('users').child(user);
const userGroups = currentUserRef.child('groups');
const groupsRef = dbRef.ref('groups');
const idArray = []
const groupArray = []
userGroups.on('value', snapshot => {
snapshot.forEach(groupId => {
idArray.push(groupId.val().groupId)
})
idArray.forEach(function(group, i) {
groupsRef.child(idArray[i]).on('value', snap =>{
let addGroup = {
id: snap.key,
name: snap.val().groupName,
description: snap.val().groupDescription,
};
groupArray.push(addGroup);
})
this.setState({
hasGroups: true,
groupsHold: groupArray,
})
}.bind(this))
})
}
render(){
console.log(this.state.hasGroups)
if(this.state.hasGroups){
return(
<div>
<ul>
{this.state.groupsHold.map((group, i) =>
<ListItem button
selected={this.state.selectedIndex === group}
onClick={event => this.handleListItemClick(event, group, i)}
key={`item-${group.id}`}>
<ListItemText primary={`${group.name}`} />
</ListItem>
)}
</ul>
</div>
)
}
return(
<div>
<p>You have no groups yet</p>
</div>
)
}
I'm setting the groups by going into my firebase and grabbing all groups that a user is part of from their user node then iterating through the 'groups' node and saving all groups that the user is a part of from that to the state. I was expecting the render to reload on the new state but It is showing up blank. It's not showing the <p>You have no groups yet</p> so it recognizes that the state has groups but is is just showing an empty <ul>.

Calls to Firebase are asynchronous, but setGroups is not an async function as you have it written. I think your problem will be fixed if you make setGroups an async function and await your call to userGroups.on('value', () => {}) inside of it.
Currently, your component is setting state when groupsArray is empty. So even though this.state.hasGroups is true, there are no groups to render!

Related

Paypal button cannot read new React state. How to work with dynamic values and paypal in React?

I'm currently working on the checkout page of an application where a user can purchase up to three items at one of three prices chosen by the user (this is mostly being done as an experiment). When the user chooses a price by clicking a button this triggers a setState and a new price is stored to the state. When doing console.log I see the new state has been set, but upon checkout it appears the state resets to its initial value. I can't tell why and have no idea where to begin on this one. I imagine on initial render paypal is keeping the initial state it was passed and needs to be rerendered when the new state is set, but not sure how to go about this or even if this is the problem. Any help or guidance is appreciated.
I'm using the #paypal/react-paypal-js library for this paypal implementation, but am welcome to alternative suggestions.
Here is the code I'm using but cut down relevant sections:
import React, {useState, useRef, useEffect} from 'react';
import { PayPalButtons, usePayPalScriptReducer } from "#paypal/react-paypal-js";
import PriceButton from './PriceButton.jsx';
import NumberItemButton from './NumberItemButton';
import {priceOptions, amountItems} from './PriceOptions';
const PaymentPage = () => {
const [isLoading, setIsLoading] = useState(false);
const [payAmount, setPayAmount] = useState('5.00');
const [itemAmount, setItemAmount] = useState('1');
const payPalOptions = { //Note: This is used in the higher level component PayPalScriptProvider
"client-id": `${process.env.REACT_APP_PAYPAL_CLIENT_ID}`,
currency: "USD",
intent: "capture",
};
const createOrder = (data, actions) => { //This will show the initial state when triggered
return actions.order.create({
purchase_units : [
{
amount: {
value: payAmount //This stays at the initial State of '5.00' despite the newState being set
}
}
]
})
};
const onApprove = (data, actions) => {
return actions.order.capture().then(function(orderData) {
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
let transaction = orderData.purchase_units[0].payments.captures[0];
alert('Transaction '+ transaction.status + ': ' + transaction.id + '\n\nSee console for all available details');
}
)};
const onError = (error) => {
console.log(error)
}
console.log(payAmount) //Note: This will show the new State
return (
<div>
<h1>Purchase</h1>
<label> Choose number of items
<div>
{amountItems.map((item, index) => {
return <NumberItemButton key={index} setItemAmount={setItemAmount} amount={item.amount} />
})}
</div>
</label>
<label> Pick a price
<div>
{priceOptions.map((item, index) => {
return <PriceButton key={index} itemAmount={itemAmount} setPayAmount={setPayAmount} price={item.price} />
})}
</div>
</label>
<PayPalButtons
createOrder={(data, actions) => createOrder(data, actions)}
onApprove={(data, actions) => onApprove(data, actions)}
onError={onError}
/>
</div>
);
}
export default PaymentPage;
I'll also add the price button component incase the issue is there
const PriceButton = ({itemAmount, setPayAmount, price}) => { //itemAmount is the amount customer buys, price is the value passed through on the mapping function
const multPrice = (itemAmount * price).toFixed(2);
const withTaxPrice = (parseInt(multPrice) + .5).toFixed(2).toString();
return (
<button onClick={() => setPayAmount(withTaxPrice)}>${multPrice}</button>
)
}
export default PriceButton;
Appreciate any help!
I came back to this with a fresh pair of eyes and found the solution (though I'm not sure if it's the best one).
The issue is when the Paypal button renders it pulls in the initial state that is passed through, but it needs to be rerendered when a new state is passed.
My solution to this was to pass a forceReRender={[payAmount]} within the PaypalButtons component. This rerenders the Paypal button upon update to the price state and allows me to pass an updated value.
Hope this helps others!
I found a better solution. Just use useRef and access the ref.current value!

Local storage being overwritten with new data

Hello guys I made a history page so the user can look at their past search history and so I used localstorage. On the history page, I am trying to render data that stays there and isn't changed when I go to search the api again. Instead I want it to keep adding data to the page. I was thinking the data would just keep being added to the new array in local storage but it overwrites the existing data with new data. Ive made an attempt to prevent this but I am stuck.
Here is my code of all of my pages
Search page
export default function SearchPage(props) {
// creating state to fetch the api
const [search, setSearch] = useState('')
// this is my function to monitor the words that are put into the input so these keywords that only matches specific data
// in the api and so one the data is fetched it renders the topic related to that keyword
const handleSearch = (event) => {
setSearch(event.target.value)
}
const handleSubmit = (event) => {
event.preventDefault();
// this is where I bring my useState variable to monitor the state of the key words in order to
// target specific data from the api
let url = `http://hn.algolia.com/api/v1/search_by_date?query=${search}`;
axios
.get(url)
.then((response) => {
const result = response.data.hits;
// this pushes the data fetched from the api to the results page using history
props.history?.push ({
pathname: '/results',
state: result,
});
})
// catching any errors to let me know if there is something going on in the .then function
.catch((error) => {
console.log(error)
console.log('Error while fetching data!')
})
}
return (
<div>
<div className="search-form-container">
{/* my form in where I search data by keywords */}
<form onSubmit={handleSubmit}>
<input type='text' placeholder="search" onChange={handleSearch} value={search}/>
<button type="submit">Search</button>
</form>
<hr/>
<Link to="/history">Your Search History</Link>
</div>
</div>
)
}
Results page
export default function ResultsPage(props) {
console.log(props)
// the variable declared below is where I am bringing in the data through history from the api.
const data = props.history.location.state;
let storedData = localStorage.setItem('data', JSON.stringify(data))
console.log(storedData)
// being that I am using history, I can map through the array of objects on the results page as shown below and then render it
const hitList = data.map((data, idx) => {
return (
<ul key={idx}>
<div>
<li> Author: {data.author}</li>
<li>Story Title: {data.story_title}</li>
<li>Comment: {data.comment_text}</li>
<li>Created on: {data.created_at}</li>
<li></li>
</div>
<hr/>
</ul>
)
})
return (
<div>
<Link to='/'><h1>Search</h1></Link>
<Link to='/history'><h1>Your History</h1></Link>
{/* In order for me to show my data I first had to map through the array of objects and then put the variable "hitlist" in the return */}
<h3>{hitList}</h3>
</div>
)
}
History page
export default function SearchHistoryPage(item) {
const storedData = JSON.parse(localStorage.getItem('data'));
storedData.push(item);
localStorage.setItem('data', JSON.stringify(storedData));
console.log(storedData);
const searchHistory = storedData.map((data, idx) => {
return (
<ul key={idx}>
<li> Author: {data.author}</li>
<li>Story Title: {data.story_title}</li>
<li>Comment: {data.comment_text}</li>
<li>Created on: {data.created_at}</li>
</ul>
)
})
return (
<div>
<h2>{searchHistory}</h2>
</div>
)
}
In your Results page, you are overwriting your localStorage 'data' key with only the results fetched from Search page.
What you can do is to fetch your "history" (localStorage 'data') before you push the new results in your Results page, and not in your History page as so:
In Results page:
const data = props.history.location.state;
// ADD THESE 2 LINEs
const historicData = JSON.parse(localStorage.getItem('data'));
historicData.push(data)
// Store "historicData" instead of "data"
// let storedData = localStorage.setItem('data', JSON.stringify(data))
let storedData = localStorage.setItem('data', JSON.stringify(historicData))
console.log(storedData)
In History page:
// Just use localStorage 'data'.
// Do not modify anything in History page since you are not taking any actions here.
const storedData = JSON.parse(localStorage.getItem('data'));
storedData.push(item);
// localStorage.setItem('data', JSON.stringify(storedData)); <-- REMOVE
// console.log(storedData); <-- REMOVE

Duplicate components with state values [Updated code for problem]

I have a view that has multiple cards with values inside each one.
I can duplicate a card. However, this doesn't preserve the values in the duplicated card.
If Card A has values inside of its state, how can I tell Parent View to create a Card B with Card A values?
Here is my view. I'm rendering multiple cards
{items.map((item, i) => (
{
'VideoCard': <div><Droppable onChange={droppableResponse} /><CreateVideoCard onChange={cardActionResponse} i={i} /></div>,
'AudioCard': <div><Droppable onChange={droppableResponse} /><CreateAudioCard onChange={cardActionResponse} i={i}></CreateAudioCard></div>,
'DescriptionCard': <div><Droppable onChange={droppableResponse} /><CreateDescriptionCard onChange={cardActionResponse} i={i}></CreateDescriptionCard></div>,
'BreakdownCard': <div><Droppable onChange={droppableResponse} /><CreateDescriptionCard onChange={cardActionResponse} i={i}></CreateDescriptionCard></div>,
'Terms': <div><Droppable onChange={droppableResponse} /><CreateTermsCard onChange={cardActionResponse} i={i}></CreateTermsCard></div>
}[item]
))}
When a card sends a response such as delete or duplicate, I go into my cardActionResponse function. Here is the duplicate part:
if (event[0] == 'duplicate') {
//Retrieve item from items array at location event[1]
console.log(items[event[1]])
var data = JSON.stringify(event[2])
console.log(data)
//Retrieve information about which card was duplicated. Not just AudioCard but AudioCard w/ info
items.splice(event[1], 0, items[event[1]])
forceUpdate()
}
Var data returns the duplicated data that I need to insert into my new duplicated card. However this will require me to change up my current structure of my cardList
const [items, setItems] = useState([
'VideoCard',
'AudioCard' ,
'DescriptionCard',
'BreakdownCard',
'Terms',
]);
In addition, I need the ability to send to each card what location it is (i) and I was able to do that with the items.map function.
I'm thinking about creating something like
<CreateVideoCard onChange={cardActionResponse} i={I} data={data} />
Then in my createvideocard component, I'll check if the data is empty, if not I'll swap the states.
Is there an easier way I can duplicate components with state inside? This seems a little extra?
if you post some code it will be helpful to answer.
Anyway, as you have not done that, here is a proposed soln.
pass a callback function say onDuplicate() from parent to cardList--> card. card component should call that function onClick of duplicate.
this onDuplicate function will update the parent state.. here is a sample example
const data = [{
id: 1111,
name:"some name",
//THERE ARE OTHER FIELDS I HAVE NOT ADDED
}]
const CardList = (props) => (
<div>
{props.profiles.map(profile => <Card key={profile.id} onDuplicate={props.onDuplicate} profile={...profile}/>)}
</div>
);
class Card extends React.Component {
duplicate = ()=>{
console.log(this.props)
this.props.onDuplicate({...this.props.profile,id: 222})
}
render() {
const profile = this.props.profile;
return (
<div className="github-profile">
<img src={profile.avatar_url} />
<div className="info">
<div className="name">{profile.name}</div>
<div className="company">{profile.company}
</div>
<div><button onClick={this.duplicate}>duplicate</button></div>
</div>
</div>
);
}
}
class App extends React.Component {
state = {
profiles: data,
};
onDuplicate = (profileData)=>{
console.log("+++++++"+profileData.name+""+profileData.company)
this.setState(prevState => ({
profiles: [...prevState.profiles, profileData],
}));
}
render() {
return (
<div>
<CardList profiles={this.state.profiles} onDuplicate={this.onDuplicate}/>
</div>
);
}
}
The duplicate function should be inside the card and then call a function in the parent passing the current state.
const Parent = () => {
const handleDuplicate = (state) => {
// your logic to create the new card with the initial state
}
<CardA onDuplicate={handleDuplicate}/>
}
const CardA = (props) => {
const { initialState, onDuplicate } = props;
const [state, setState] = useState(initialState);
const duplicate = () => {
onDuplicate(state);
}
}
The key function I had to use was React.cloneElement
I changed my items array to
const [items, setItems] = useState([
<CreateVideoCard onChange={cardActionResponse}/>,
<CreateAudioCard onChange={cardActionResponse}></CreateAudioCard>,
<CreateDescriptionCard onChange={cardActionResponse}></CreateDescriptionCard>,
<CreateDescriptionCard onChange={cardActionResponse}></CreateDescriptionCard>,
<CreateTermsCard onChange={cardActionResponse}></CreateTermsCard>,
]);
and rendered it with
{items.map((item, i) => (
React.cloneElement(item, {i: i})
))}
When I received a request to duplicate I would use cloneElement to add a data prop to my items array:
items.splice(event[1] + 1, 0, React.cloneElement(items[event[1]], { data: data }))
Then in my card constructor. I checked if props.data was undefined. If it was, I would swap out the state and if it wasn't, it would create base structure.

How to code a good re-render parent call?

render my parent component from a call in its child component. But I don't know how to write it correctly.
The context is: When the page is loaded, the code search the userId in localStorage to download the data of a user to display its information.
A link exists in child component to removed the idUser and got an empty form. The problem is that my parent don't re-render automatically.
<Link to="/consultation" onClick={() => {localStorage.removeItem('idUser'); this.props.callBack();}}>here.</Link>
-
else if(user){contents = [<Message_UserIdentified user callBack={this.forceUpdate()}/>, contentform];}
I tried something, but it's not work. Could you help me please ?
I don't know how to parse it (the item "user" and in same time the callBack).
The code write me that it don't find the function forceUpdate().
This is my parents page called "Consultation" (I removed some parts of code to be clearer):
import { Message_EmptyUserID, Message_NeedToBeConnected, Message_UserIdentified } from '../component/message_consultation';
const Consultation = () => {
const [user, setUser] = useState(null);
const [login] = useState(jwtUtils.checkToken());
const [userId, setUserId] = useState(false);
function handleChange(event) {
setUser({
...user,
[event.target.name]: event.target.value
})
};
useEffect(() => {
if (login === false || localStorage.getItem("idUser") === null) return;
axios.get(`http://localhost:5000/users/${localStorage.getItem("idUser")}`, {
headers: {
'token': localStorage.getItem('token')
}
})
.then(res => {
setUser(res.data);
if(res.data !== undefined){
setUserId(true);
}
})
.catch(error => console.log(error))
}, []);
let contents, contentform;
contentform = (
<div >...
</div>
);
if (login === false) contents = Message_NeedToBeConnected();
else if (userId === false) contents = contentform;
else if(user){contents = [<Message_UserIdentified user callBack={this.forceUpdate()}/>, contentform];}
return (
<div className="case card border-secondary mb-3" styles="max-width: 20rem;">
<div className="card-header">Consultation</div>
<div className="card-body">
{contents}
</div>
</div>
);
}
export default Consultation;
This is my child component called "Message_UserIndentified":
const Message_UserIdentified = (user) => {
return(
<Alert color="primary" className="alert alert-dismissible alert-info">
<h4>{user === null || user === undefined ? "" : user.firstname} {user === null || user === undefined ? "" : user.lastname}</h4>
If you are not {user === null || user === undefined ? "" : user.firstname} and you are <mark>already registered</mark>, find your consultation <Link to="/register" onClick={localStorage.removeItem('idUser')}>here.</Link> <hr/>
If you are not {user === null || user === undefined ? "" : user.firstname} and your are <mark>not registered</mark>, add your consultation <Link to="/consultation" onClick={() => {localStorage.removeItem('idUser'); this.props.callBack();}}>here.</Link>
</Alert>
);
}
First of all, you mix up the hooks API with the class API. You need to choose one, React doesn't allow to use both in a single component.
React functional components don't have this and this.forceUpdate. See the official note on how to force update in a functional component with hooks:
const Consultation = () => {
const [ignored, forceUpdate] = React.useReducer(x => x + 1, 0);
// ...
return <Message_UserIdentified user callBack={forceUpdate} />;
};
Second, you mustn't call the function when you want to pass it as a callback. When you run callBack="forceUpdate()", the function is executed once and the returned value is passed to the child prop. You need to run callBack="forceUpdate" to pass the function itself so that the child component can execute it whenever it wants.
This rule is applicable to class components too, but you should also bind the forceUpdate method to the current component:
<button onClick={() => this.forceUpdate()}>Demo</button>
// or
<button onClick={this.forceUpdate.bind(this)}>Demo</button>
From the architecture point of view, you shouldn't use localStorage to share data between components of the application because localStorage isn't reactive (it doesn't notify when a content changes) so you'll struggle updating all the components manually. You'd rather read'n'write localStorage only in Consultation or use a reactive state storage like Redux and MobX (in more complex applications). Local storage may be used at the same time: read on an application start and write when something changes.

Accessing Apollo's loading boolean outside of Mutation component

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

Resources