Hi I'm new to firebase and don't know how to make a screen in my react native app update when a certain firebase variable changes.
In my app's homescreen, a user's posts are fetched from firebase in componentDidMount, and then rendered:
componentDidMount = () => {
this.setup();
};
setup = async () => {
const { currentUser } = await firebase.auth();
this.setState({ currentUser });
await firebase
.database()
.ref("users/" + currentUser.uid + "/posts")
.on("value", snapshot => {
this.setState({posts: snapshot.val()})
});
}
// render posts
In a separate screen, the user can choose to add a post and the firebase database is updated:
addPost = async () => {
const { currentUser } = firebase.auth();
await firebase
.database()
.ref("users/" + currentUser.uid + "/posts")
.push({
post: // data for post
});
However, though the database is successfully changed, the homescreen doesn't update and show the newly added post till it is manually reloaded. How do I add a listener to the homescreen, so that when the posts database variable changes, the screen automatically updates.
If you want to share the same data between multiple screens, the best is to use Redux.
Using Redux, all your renders function (which needs a specific data from the store) will automatically refresh if a data is updated. That way, you can only handle one firebase listener to update your redux store.
I would create a "Post" reducer and dispatch an update action every time I got something new from the firebase listener.
// Actions
const ADD = 'post/ADD';
const UPDATE = 'post/UPDATE';
// Initial state
const initialState = {
posts: [],
};
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case ADD:
return {
...state,
posts: [...state.posts, action.post]
};
case UPDATE:
return {
...state,
posts: action.posts
};
default:
return state;
}
}
// Action Creators
export function addPost(post) {
return {
type: ADD,
post
};
}
export function updatePosts(posts) {
return {
type: UPDATE,
posts
};
}
And the listener would be like :
import { update } from 'PostRedux.js'
firebase
.database()
.ref("users/" + currentUser.uid + "/posts")
.on("value", snapshot => {
Store.dispatch(update(snapshot.val()));
});
Related
I am new in Nextjs, i am trying to integrate [slug.js] page, i want to know that how can we manage/get data in sidebar (similar blogs) ? in other words for blog details i used "get static path" and "props", But now i want to pass "current slug" ( to API) so i can fetch all blogs with this blog category,How can i do this ?
Client-side approach:
Since you pass the post as page-props via getStaticProps, you can either take the slug from there (if it's included in your data model), or extract the slug from the url via next's useRouter hook in case you want to do client-side fetching:
import axios from "axios"; // using axios as an example
import { useRouter } from "next/router";
const Component = () => {
const [similarPosts, setSimilarPosts] = useState([]);
const router = useRouter();
const { slug } = router.query;
const getSimilarPosts = async () => {
if (!router.isReady() || !slug) return [];
const { data } = await axios.get("/api/similar-posts-route/" + slug);
return data;
};
useEffect(() => {
if (similarPosts.length > 0) return;
(async () => {
const posts = await getSimilarPosts(); // assuming API returns an array of posts as data.
setSimilarPosts(posts);
})();
}, []);
return <div>Similar posts: {JSON.stringify(similarPosts)}</div>;
};
[...]
Server-Side approach (preferred):
I believe it would be a better approach to directly fetch similar posts inside getStaticProps to reduce API calls and for a better UX.
Inside getStaticProps you can take the slug from context.params and fetch all similar posts directly from your database/CMS, and pass them directly as props to your page component:
export async function getStaticProps({ params }) {
const { slug } = params;
// fetch similar posts directly from the database using the slug (don't call the API, it's not up yet during build phase)
const similarPosts = await executeDatabaseQueryForSimilarPosts(slug);
// [...] fetch the rest of the page props
return {
props: {
similarPosts,
// [...] return the rest of page props
},
revalidate: 60 * 30 // re-fetch the data at most every 30 minutes, so the posts stay up to date
};
}
// directly take all similar posts from props
const Component = ({similarPosts}) => {
return <div>Similar posts: {JSON.stringify(similarPosts)}</div>;
};
So I have a situation where I have this component that shows a user list. First time the component loads it gives a list of all users with some data. After this based on some interaction with the component I get an updated list of users with some extra attributes. The thing is that all subsequent responses only bring back the users that have these extra attributes. So what I need is to save an initial state of users that has a list of all users and on any subsequent changes keep updating/adding to this state without having to replace the whole state with the new one because I don't want to lose the list of users.
So far what I had done was that I set the state in Redux on that first render with a condition:
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
}
userList = users || usersFromProp
})
The above was working fine as it always saved the users sent the first time in the a prop and always gave priority to it. Now my problem is that I'm want to add attributes to the list of those users in the state but not matter what I do, my component keeps going into an infinite loop and crashing the app. I do know the reason this is happening but not sure how to solve it. Below is what I am trying to achieve that throws me into an infinite loop.
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
} else {
//Users already exist in state
const mergedUserData = userDataFromApi.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
})
setUsers(mergedUserData)
}
}, [users, setUsers, userDataFromApi])
So far I have tried to wrap the code in else block in a separate function of its own and then called it from within useEffect. I have also tried to extract all that logic into a separate function and wrapped with useCallback but still no luck. Just because of all those dependencies I have to add, it keeps going into an infinite loop. One important thing to mention is that I cannot skip any dependency for useCallback or useEffect as the linter shows warnings for that. I need to keep the logs clean.
Also that setUsers is a dispatch prop. I need to keep that main user list in the Redux store.
Can someone please guide me in the right direction.
Thank you!
Since this is based on an interaction could this not be handled by the the event caused by the interaction?
const reducer = (state, action) => {
switch (action.type) {
case "setUsers":
return {
users: action.payload
};
default:
return state;
}
};
const Example = () => {
const dispatch = useDispatch();
const users = useSelector(state => state.users)
useEffect(() => {
const asyncFunc = async () => {
const apiUsers = await getUsersFromApi();
dispatch({ type: "setUsers", payload: apiUsers });
};
// Load user data from the api and store in Redux.
// Only do this on component load.
asyncFunc();
}, [dispatch]);
const onClick = async () => {
// On interaction (this case a click) get updated users.
const userDataToMerge = await getUpdatedUserData();
// merge users and assign to the store.
if (!users) {
dispatch({ type: "setUsers", payload: userDataToMerge });
return;
}
const mergedUserData = users.map(existingUser => {
const matchedUser = action.payload.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
dispatch({ type: "setUsers", payload: mergedUserData });
}
return (
<div onClick={onClick}>
This is a placeholder
</div>
);
}
OLD ANSWER (useState)
setUsers can also take a callback function which is provided the current state value as it's first parameter: setUsers(currentValue => newValue);
You should be able to use this to avoid putting users in the dependency array of your useEffect.
Example:
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);
I want to create a profile page in my React app. The user data is in the state but I want to load the data from the API as I load the page.
I've tried tyo fetch the data with this.props.getUser(this.props.auth._id) in the Constructor or in ComponentDidMount, but it did not load.
The data does comes in through componentWillReceiveProps, but it does not load on the first page load. Although, if I refresh the page, the data comes in.
This is part of my profile.js:
class Profile extends Component {
state = {
_id: '',
name: '',
email: '',
username: '',
errors: {}
};
componentDidMount() {
this.loadCurrentUser();
}
loadCurrentUser = () => {
this.setState(this.props.getUser(this.props.auth._id));
};
// https://hackernoon.com/replacing-componentwillreceiveprops-with-getderivedstatefromprops-c3956f7ce607
UNSAFE_componentWillReceiveProps(nextProps, nextState) {
console.log("received")
const { name, email, username } = nextProps.auth;
this.setState({ name, email, username });
}
// ...
// Some other code
// ...
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps, { getUser, logOut })(Profile);
My question is: How to load the page with the data I get from the API presented in the form fields?
Thank you
EDITED
I have edited my componentDidMount to use promises, but I still can not get it right. Now, My store gets the states right, but my component still does not get updated.
componentDidMount() {
this.props.getUser(this.props.auth.user)
.then(user => this.setState({ name: user.name, email: user.email, username: user.username }))
}
If I add a simple console.log, I still can not get the return from my query (getUser). This console.log is undefined.
componentDidMount() {
this.props.getUser(this.props.auth.user)
.then(user => console.log(user));
}
This is my getUser (src/actions/userActions.js):
export const getUser = _id => async dispatch => {
const res = await axios.get(`${process.env.REACT_APP_USERS_API}/api/v1/users/${_id}`);
dispatch({
type: GET_USER,
payload: res.data.data
});
};
The getUser action does not have return value. Instead, it updates the user data inside the redux store. So you shouldn't reply on the return value and set state from it.
Instead, dispatch the getUser action on page load so that the user data is updated and always access the data from the store (through this.props.auth). If there is an updated version of the user data, React handles the page re-render automatically:
componentDidMount() {
this.props.getUser(this.props.auth.user);
}
If for some reason, you need the user data to be saved in state (for example, you have a form on page where user can update username/password), then use getDerivedStateFromProps method:
static getDerivedStateFromProps(props, state) {
// return an object to update the state.
return props.auth;
}
I am using Vuex with axios to fetch data from my backend. But somehow the state property userName is not updating in my Vue Single File Component(SFC).
approot.js
state
const state = {
userName: 'foo'
};
getter
const getters = {
getUserName: (state) => state.userName
};
Single File Component
<template>
<div id="navbar">
//cut for brievity
<span>{{getUserName}}</span>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name: 'navbar',
computed: mapGetters(['getNumberOfJobMessages','getUserName']),
//cut for brievity
}
</script>
<style scoped>
//cut for brievity
</style>
Action fetching data with axios from the backend
const actions = {
async fetchMenuData({ commit }) {
//fetch data from api controller
const response = await axios.get('../api/Menu/GetMenu');
console.log(response.data.userName); //not undefined
commit('setMenuData', response.data);
}
}
Mutation setting state variables
const mutations = {
setMenuData(state, menuData) {
console.log(menuData.userName); //not undefined
state.userName = menuData.userName;
console.log(state.userName); //not undefined
}
}
Problem
When my single file component calls getUserName it always renders 'foo', the hardcoded value. Im quite baffled by this, since the rest of my state variables are set with the same pattern, and my components have no problems getting them.
Anyone who knows whats going wrong or can see a flaw in my code? It would be highly appreciated.
Use mutations to only set data. and other things do on action. like:
Action:
const actions = {
async fetchMenuData({ commit }) {
const response = await axios.get('../api/Menu/GetMenu');
let userName = response.data.userName;
commit('setUserName', userName);
}
}
And mutations:
const mutations = {
setUserName(state, userName) {
state.userName = userName;
}
}
Dont forget to dispatch the function fetchMenuData
Properly not sure, why this happens. But, I faced this problem and solved by this way.
axios.get('../api/Menu/GetMenu')
.then(({ data }) => {
commit('setUserName', data.userName);
}).catch(error => { })
It is better to make a commit in then()
I am trying to load a notification token (notificationToken) that I've stored within Firebase to a React Native component.
Once the notificationToken is loaded to my redux state, I want to check for my device permissions to see if the notificationToken has expired within the function getExistingPermission() that I run in the componentDidMount().
If the token has expired, then I'll replace the token within Firebase with the new token. If it's the same, then nothing happens (which is intended functionality).
When I'm running my function getExistingPermission() to check if the token is up-to-date the Firebase listener that pulls the notificationToken does not load in time, and so it's always doing a write to the Firebase database with a 'new' token.
I'm pretty sure using async/await would solve for this, but have not been able to get it to work. Any idea how I can ensure that the notificationToken loads from firebase to my redux state first before I run any functions within my componentDidMount() function? Code below - thank you!
src/screens/Dashboard.js
Should I use a .then() or async/await operator to ensure the notificationToken loads prior to running it through the getExistingPermission() function?
import {
getExistingPermission
} from '../components/Notifications/NotificationFunctions';
componentDidMount = async () => {
// Listener that loads the user, reminders, contacts, and notification data
this.unsubscribeCurrentUserListener = currentUserListener((snapshot) => {
try {
this.props.watchUserData();
} catch (e) {
this.setState({ error: e, });
}
});
if (
!getExistingPermission(
this.props.notificationToken, //this doesn't load in time
this.props.user.uid)
) {
this.setState({ showNotificationsModal: true });
}
};
src/components/Notifications/NotificationFunctions.js
The problem is probably not here
export const getExistingPermission = async (
notificationToken,
uid,
) => {
const { status: existingStatus } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);
if (existingStatus !== 'granted') {
console.log('status not granted');
return false;
} else {
let token = await Notifications.getExpoPushTokenAsync();
/* compare to the firebase token; if it's the same, do nothing,
if it's different, replace */
if (token === notificationToken) {
console.log('existing token loaded');
return true;
} else {
console.log('token: ' + token);
console.log('notificationToken: ' + notificationToken);
console.log('token is not loading, re-writing token to firebase');
writeNotificationToken(uid, token);
return false;
}
}
};
src/actions/actions.js
// Permissions stuff
watchPermissions = (uid) => (
(dispatch) => {
getPermissions(uid + '/notificationToken', (snapshot) => {
try {
dispatch(loadNotificationToken(Object.values([snapshot.val()])[0]));
}
catch (error) {
dispatch(loadNotificationToken(''));
// I could call a modal here so this can be raised at any point of the flow
}
});
}
);
// User Stuff
export const watchUserData = () => (
(dispatch) => {
currentUserListener((user) => {
if (user !== null) {
console.log('from action creator: ' + user.displayName);
dispatch(loadUser(user));
dispatch(watchReminderData(user.uid)); //listener to pull reminder data
dispatch(watchContactData(user.uid)); //listener to pull contact data
dispatch(watchPermissions(user.uid)); //listener to pull notificationToken
} else {
console.log('from action creator: ' + user);
dispatch(removeUser(user));
dispatch(logOutUser(false));
dispatch(NavigationActions.navigate({ routeName: 'Login' }));
}
});
}
);
export const loadNotificationToken = (notificationToken) => (
{
type: 'LOAD_NOTIFICATION_TOKEN',
notificationToken,
}
);
Tony gave me the answer. Needed to move the permissions check to componentDidUpdate(). For those having a similar issue, the component looks like this:
src/screens/Dashboard.js
componentDidUpdate = (prevProps) => {
if (!prevProps.notificationToken && this.props.notificationToken) {
if (!getExistingPermission(
this.props.notificationToken,
this.props.user.uid
)) {
this.setState({ showNotificationsModal: true });
}
}
};
Take a look at redux subscribers for this: https://redux.js.org/api-reference/store#subscribe . I implement a subscriber to manage a small state machine like STATE1_DO_THIS, STATE2_THEN_DO_THAT and store that state in redux and use it to render your component. Only the subscriber should change those states. That gives you a nice way to handle tricky flows where you want to wait on action1 finishing before doing action2. Does this help?