Using componentWillUpdate with switch statements - reactjs

I am using React-Native and React-Native-Firebase and am trying to have my Events component make a different Firebase query (and then update redux store) depending what the value of the activityType prop is.
Here is the parent component which is working just fine. It updates state.eventType when I change the dropdown value and passes the value into <Events />.
let eventTypes = [{value: 'My Activity'}, {value: 'Friend Activity'}, {value: 'All Activity'}];
state = {
showEventFormModal: false,
eventType: 'Friend Activity'
}
<View style={styles.container}>
<Dropdown
data={eventTypes}
value={this.state.eventType}
containerStyle={{minWidth: 200, marginBottom: 20}}
onChangeText={val => this.setState({eventType: val})}
/>
<Events activityType={this.state.eventType}/>
</View>
And here is Events component. Using a Switch statement to determine which activityType was passed into props. The issue I am having is an infinite loop because within each case statement I am dispatching the action to update the store which causes a rerender and the componentWillUpdate() to retrigger. What I am trying to understand is what the optimal way to handle this problem is? Because clearly my method now does not function properly. Is there a common react pattern to achieve this?
// GOAL: when this components props are updated
// update redux store via this.props.dispatch(updateEvents(events))
// depending on which type of activity was selected
componentWillUpdate() {
let events = [];
switch(this.props.activityType) {
case 'Friend Activity': // get events collections where the participants contains a friend
// first get current users' friend list
firebase.firestore().doc(`users/${this.props.currentUser.uid}`)
.get()
.then(doc => {
return doc.data().friends
})
// then search the participants sub collection of the event
.then(friends => {
firebase.firestore().collection('events')
.get()
.then(eventsSnapshot => {
eventsSnapshot.forEach(doc => {
const { type, date, event_author, comment } = doc.data();
let event = {
doc,
id: doc.id,
type,
event_author,
participants: [],
date,
comment,
}
firebase.firestore().collection('events').doc(doc.id).collection('participants')
.get()
.then(participantsSnapshot => {
for(let i=0; i<participantsSnapshot.size;i++) {
if(participantsSnapshot.docs[i].exists) {
// if participant uid is in friends array, add event to events array
if(friends.includes(participantsSnapshot.docs[i].data().uid)) {
// add participant to event
let { displayName, uid } = participantsSnapshot.docs[i].data();
let participant = { displayName, uid }
event['participants'].push(participant)
events.push(event)
break;
}
}
}
})
.then(() => {
console.log(events)
this.props.dispatch(updateEvents(events))
})
.catch(e => {console.error(e)})
})
})
.catch(e => {console.error(e)})
})
case 'My Activity': // get events collections where event_author is the user
let counter = 0;
firebase.firestore().collection('events').where("event_author", "==", this.props.currentUser.displayName)
.get()
.then(eventsSnapshot => {
eventsSnapshot.forEach(doc => {
const { type, date, event_author, comment } = doc.data();
let event = {
doc,
id: doc.id,
type,
event_author,
participants: [],
date,
comment,
}
// add participants sub collection to event object
firebase.firestore().collection('events').doc(event.id).collection('participants')
.get()
.then(participantsSnapshot => {
participantsSnapshot.forEach(doc => {
if(doc.exists) {
// add participant to event
let { displayName, uid } = doc.data();
let participant = { displayName, uid }
event['participants'].push(participant)
}
})
events.push(event);
counter++;
return counter;
})
.then((counter) => {
// if all events have been retrieved, call updateEvents(events)
if(counter === eventsSnapshot.size) {
this.props.dispatch(updateEvents(events))
}
})
})
})
case 'All Activity':
// TODO
// get events collections where event_author is the user
// OR a friend is a participant
}
}

Updating the store is best to do on the user action. So, I'd update the store in the Dropdown onChange event vs. in the componentWillUpdate function.

I've figured out what feels like a clean way to handle this after finding out I can access prevProps via componentDidUpdate. This way I can compare the previous activityType to the current and if they have changed, then on componentDidUpdate it should call fetchData(activityType).
class Events extends React.Component {
componentDidMount() {
// for initial load
this.fetchData('My Activity')
}
componentDidUpdate(prevProps) {
if(this.props.activityType !== prevProps.activityType) {
console.log(`prev and current activityType are NOT equal. Fetching data for ${this.props.activityType}`)
this.fetchData(this.props.activityType)
}
}
fetchData = (activityType) => {
//switch statements deciding which query to perform
...
//this.props.dispatch(updateEvents(events))
}
}
https://reactjs.org/docs/react-component.html#componentdidupdate

Related

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

Duplication problem in TodoList application

I am creating a todo-list, the following function handleChange gets the id of the a todo component and changes its attribute of completed from true/false. This is then saved in state of allTodos
function handleChange(id) {
const updatedTodos = allTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
setTodos(updatedTodos)
}
const todoComponents = allTodos.map(item => <Todos key={item.id} item={item} handleChange={handleChange}/>)
the function updateDB takes that value from state and using it to update the database.
function updateDB(event) {
event.preventDefault()
const value = {
completed: false,
text: newTodo,
id: allTodos.length,
}
}
Here's where the problem arises: id: allTodos.length. If one of these are deleted, it will create a todo with a duplicate ID, crashing the whole thing. I don't know how to avoid this problem.
In updateDB, you are setting id to allTodos.length aka 1.

React Native: Duplicate items in component state

I have a two screens in a StackNavigator, one with a FlatList that displays data retrieved from Firestore, and another to add a new data to the database. After returning from the second screen in the stack via navigation.goBack(), the new item should be appended to the list. Instead, the entire state with the new item is being appended to the old state. The database data contains no duplicates and upon refresh, the list contains the correct elements.
I can't tell if I'm misunderstanding the component lifecycle or the query itself so I would appreciate any help.
export default class Main extends React.Component {
state = { chatData:[] }
componentDidMount = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats id array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
this.setState({chatData: [...this.state.chatData,
{id: element, subject: doc.data().subject, course: doc.data().course}]})
})
});
})
}
state after adding a course and returning to the list screen (duplicate element)
When setting state try to use the prevState callback function. Like so:
export default class Main extends React.Component {
state = { chatData:[] }
componentDidMount = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats id array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
// We use the parameter of the first argument of setState - prevState
this.setState(prevState => ({chatData: [...prevState.chatData,
{id: element, subject: doc.data().subject, course: doc.data().course}]}))
})
});
})
}
Because you want to spread the state that was there previously like an accumulator with the new data you're getting from firestore. If you do it with this.state then you'll be adding it again since it concerns the data that is already in the state and therefore repeated/duplicated. Let me know if it helps.
Try to create a new array with unique values and assign that to chatData
componentDidMount = () => {
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
/**
* create a new array with unique values
*/
let newArray = [...this.state.chatData, { id: element, subject: doc.data().subject, course: doc.data().course }]
let uniqueArray = [...new Set(newArray)]
this.setState({
chatData: uniqueArray
})
})
});
})
}
Hope this helps you. Feel free for doubts.
Here is my eventual solution. I'm using react-navigation addListener to call the firestore API whenever the first screen is switched to and clearing the state when the second screen is navigated to. I also switched from onSnapshot() to get() for my firestore calls.
class Main extends React.Component {
state = { currentUser: null, chatData:[]}
componentDidMount = () => {
console.log('A - component did mount')
// definitely works
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus',
payload => {
console.log('A- focus')
this.readCourseData()
})
this.willBlurSubscription = this.props.navigation.addListener(
'willBlur',
payload => {
console.log('A- blur')
this.setState({chatData: []})
})
}
componentWillUnmount() {
console.log('A - component will unmount')
this.willFocusSubscription.remove();
this.willBlurSubscription.remove();
// Remove the event listener
}
readCourseData = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).get().then((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).get().then((doc) => {
let newArray = [...this.state.chatData, { id: element, subject: doc.data().subject, course: doc.data().course }]
let uniqueArray = [...new Set(newArray)]
this.setState({
chatData: uniqueArray
})
})
});
})
}

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Updating state in react

I couldn't get the idea of updating state in react.
state = {
products : []
};
handleProductUpVote = (productId) => {
const nextProducts = this.state.products.map((product)
=> {
if (product.id === productId) {
return Object.assign({}, product, {
votes: product.votes + 1,
});
} else {
return product;
}
});
this.setState({
products: nextProducts,
});
}
Why we need to clone the object? Can we simply write
if (product.id === productId) {
product.votes =
product.votes + 1;
});
State updates can happen asynchronously, and delegating actual change of the state helps to ensure that things won't go wrong. Passing a new Object with the change also lets react make a (shallow) check to see which parts of the state were updated, and which components relying on it should be rerendered.

Resources