I have been stuck at this for a while. I'm using redux to maintain the data in my react app and using firebase database to store it. I'm unable to assign the name property of firebase as an id to their respective item.
I get this error message - Warning: Each child in a list should have a unique "key" prop.
This is how I'm assigning the id's.
This is the action that fetches data from firebase.
export const FetchPost = () => {
return dispatch => {
dispatch(FetchPostStart());
axios.get('/Data.json')
.then(response => {
const fetchedData = [];
for(let key in response.data){
fetchedData.push({
...response.data[key],
id: response.data[key].name
});
}
dispatch(FetchPostSuccess(fetchedData));
})
.catch(error => {
dispatch(FetchPostError(error));
});
}
}
This is when I render the data from redux store to the screen.
{this.props.data.reverse().map((res) => (
<div>
<Card
key={res.id}
className="Cards"
>
But the id assigned is null to all the items.
Related
Im building a simple event-page where you can create events and buy tickets for those events. I have all the event ids a user has created stored in a firebase firestore doc to access it i need the current logged in users uid. (thats why im pulling the data client-side, no getStaticProps, ... possible) after i get the created events from the user i search with those ids in my event-collection in firestore to get the data i need.
It seems that i cant access a single array-field. array[0] e.g. is displayed as undefined even though in the next moment I get the data loged on the console for the whole array, where array[0] [1] ... is existent.
My basic idea:
get user & uid from fb
get data from firestore with the uid i got
display data with .map
Code:
Variable Declaration:
const auth = getAuth();
const [events, setEvents] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect to get logged in user:
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
getEvents(user.uid);
setIsLoading(false);
}
});
}, []);
get data from firestore:
const getEvents = async (uid) => {
const createdEvents = [];
await getDoc(doc(db, "users", uid)).then((currentUser) => {
currentUser.data().events.forEach(async (eventId) => {
await getDoc(doc(db, "events", eventId)).then((event) => {
createdEvents.push({ ...event.data(), id: event.id });
});
});
setEvents(createdEvents);
});
};
Display data on website:
{isLoading ? (
<span>loading events..</span>
) : (
events.map((event) => {
return <p key={event.id}>{console.log(event.title)}</p>;
})
)}
console outputs:
{isLoading ? null : console.log("event Array: ", events)} {/* data outputs in console */}
{isLoading ? null : console.log("single array element: ", events[0])} {/* is undefined */}
enter image description here
Note: the array renders twice, first time with no data because of the variable declaration (as expected) an second time with data, i just dont know why it wont display on the website.
I appreciate any help, thanks a lot!
I tried moving some variables and changing when the async function fires, nothing helps. Also i tried waiting for the data in the array with events && events.map ...
I'm trying to display a user's order history using query. The users' orders, dubbed "items", is an array and I can see the array being logged in the console with my code below. But nothing is being displayed in the app. What am I missing? Also attached my Firestore db for reference.
const q = query(ordersCollectionRef, where("items", "==", "userId"));
onSnapshot(q, (snapshot) => {
let orders = [];
snapshot.docs.forEach((doc) => {
orders.push({ ...doc.data(), id: doc.id });
});
console.log(orders);
});
...and in the return:
{orders.map((order) => {
if (order.id === currentUser.uid) {
return (
<View>
<Text>test{orders.items}</Text>
</View>
);
};
})}
For React to properly "understand" that you have updated data in a state variable, you need to use the "set state function" and NOT update the state variable directly.
In your code, put the data into a new and temporary variable, the after the forEach() call: setOrders(theTempVariable)
That puts the data in theTempVariable into the orders variable and it tells React that the variable has a new value....so React will "react" to state changing and it will re-render your component.
I have a list of items on my application and I am trying to create a details page to each one on click. Moreover I am not managing how to do it with useState and useEffect with typescript, I could manage just using componentDidMount and is not my goal here.
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
Here is the codesandbox, when you click on the "details" button you will see that is not printing the name of the selected metric coming from the state: https://codesandbox.io/s/react-listing-forked-6sd99
This is my State:
export type MetricState = {
metrics: IMetric[];
};
My actionCreators.js file that the dispatch calls
export function fetchMetric(catalog, metricId) {
const action = {
type: ACTION_TYPES.CHOOSE_METRIC,
payload: []
};
return fetchMetricCall(action, catalog, metricId);
}
const fetchMetricCall = (action, catalog, metricId) => async (dispatch) => {
dispatch({
type: action.type,
payload: { metrics: [catalog.metrics.filter((x) => x.name === metricId)] } //contains the data to be passed to reducer
});
};
and finally the MetricsDeatail.tsx page where I try to filter the selected item with the id coming from route parametes:
const metricId = props.match.params.metricId;
const catalog = useSelector<RootState, MetricState>(
(state) => state.fetchMetrics
);
const selectedMetric: IMetric = {
name: "",
owner: { team: "" }
};
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(catalog, metricId));
}, []);
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
I agree with you. The metricId comes from the URL through props so it does not also need to be in the state. You want to have a "single source of truth" for each piece of data.
In order to use this design pattern, your Redux store needs to be able to:
Fetch and store a single Metric based on its id.
Select a select Metric from the store by its id.
I generally find that to be easier with a keyed object state (Record<string, IMetric>), but an array works too.
Your component should look something like this:
const MetricDetailsPage: React.FC<MatchProps> = (props) => {
const metricId = props.match.params.metricId;
// just select the one metric that we want
// it might be undefined initially
const selectedMetric = useSelector<RootState, IMetric | undefined>(
(state) => state.fetchMetrics.metrics.find(metric => metric.name === metricId )
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(metricId));
}, []);
return (
<div>
<Link to={`/`}>Go back to main page</Link>
Here should print the name of the selected metric:{" "}
<strong>{selectedMetric?.name}</strong>
</div>
);
};
export default MetricDetailsPage;
But you should re-work your action creators so that you aren't selecting the whole catalog in the component and then passing it as an argument to the action creator.
I am learning ReactJS and trying to update the parent props with the updated state of ingredients from the child component. The setUserIngredients is called and updated ingredients are being passed to parent.
Code :
const [userIngredients, setUserIngredients] = useState([]);
const removeIngredientHandler = id => {
setLoading(true);
fetch(`https://***************.com/ingredients/${id}.json`,{
method:'DELETE'
}).then(response=>{
setLoading(false);
setUserIngredients(prevIngredients =>
prevIngredients.filter(ingredient =>{
return (ingredient.id !== id)
//return ingredient;
})
);
**props.ingredients(userIngredients);**
//userIngredients is still having old value
//need to check on this
}).catch(error => {
setError(error.message);
})
};
The problem is that userIngredients is a variable that is created when the component renders, set to a version fo the state when that component renders. And when you start an asynchronous operation (like a fetch) the callback you pass to that operation will be bound the values from when that callback was created.
The fix here is pretty simple. In the spot where you calculate your new ingredients, simply execute whatever callback you need before returning the value to be stored in the state.
Something like:
fetch(`https://***************.com/ingredients/${id}.json`, {
method: 'DELETE',
}).then(response => {
setLoading(false)
setUserIngredients(prevIngredients => {
// Figure out the new ingredients
const newIngredients = prevIngredients.filter(ingredient => ingredient.id !== id)
// Call your callback with the new ingredients
props.ingredients(newIngredients)
// Return the new ingredients to be stored in your state
return newIngredients
})
})
My react app isn't re-rendering when updating my data. I think its related to updating the state with an array, but I can't quite figure out exactly what is causing the issue.
Here is my action
export const fetchChannels = uid => dispatch => {
db.ref(`chat/userchats/${uid}`).on("value", snapshot => {
if (snapshot.exists()) {
let userchats = snapshot.val()
let channels = []
Object.values(userchats).map(function(chat, i) {
db.ref(`chat/chats/${chat}`).on("value", snap => {
let chatData = snap.val()
channels.push(chatData)
})
})
dispatch({
type: "FETCH_CHANNELS",
payload: channels
})
}
})
}
And my reducer
case 'FETCH_CHANNELS':
return {
...state,
channels:action.payload
}
Since channels is set to an empty array, I'm guessing that redux is not recognizing this as a change of state? But can't quite get the right fix.
Edit
I console.logged snapshot.exists() prior to the if statement, and that came back as True.
I also logged channels prior to the dispatch and this seems to be where there is an issue. channels does not return the updated values, but rather the previous values when updated.
Here is the code I am using to add the channel item to userchats. The action I am looking for is that a user creates a channel, which creates a channel with the member uid's and chat information. That information is then pushed to the userchats directory and is pushed in to those users channels.
export const createChannel = (name,members,purpose) => dispatch =>{
db.ref(`chat/chats/`).push({name,members,purpose})
.then(result=>{
let key = result.getKey()
members.map(function(member,i){
db.ref(`chat/userchats/${member}`).push(key)
.then(result=>{})
.catch(error=> {
console.log(error)
})
})
})
.catch(error => {
console.log(error);
})
}
When I console.log at the end of the createChannel(), and console.log prior to the channels dispatch, in fetchChannel(). When I create the channel, it shows that fetchChannels updates first, and createChannel() second, even though createChannel() is called first, and fetchChannel() is simply listening for an update.
Here is the element I am attempting to rerender when props is updated
{Object.keys(this.props.channels).map(function(item,i){
return(
<Channel channel={this.props.channels[item].name} key={i} notification={''} selected={this.state.selected} item={item} onClick={()=>this.selectChannel(item)}/>
)
},this)}
Thanks for the responses Josh.
I was able to solve my issue. I had to switch the order of database entries. Because the chats push was triggering the userchats update before it was able to create the channels entry. Creating a key and using that to create userchats entries first solved the update issue.
export const createChannel = (name,members,purpose) => dispatch =>{
let newKey = db.ref().push().getKey()
members.map(function(member,i){
db.ref(`chat/userchats/${member}`).push(newKey)
.then(result=>{})
.catch(error=> {
console.log(error)
})
})
db.ref(`chat/chats/${newKey}`).set({name,members,purpose})
}