I have a ReactJS/Redux/Saga app which currently sends and reads data from a Firebase Realtime Database. As data is sent and received, there's a global redux state value loading, which toggles between true and false between sending data and confirming that data is now in Firebase. loading defaults to false for this case.
When a user updates their data, the flow is currently:
Redux reducer SEND_TO_FIREBASE
return { ...state, loading: true };
This reducer triggers a Saga function sendToFirebaseSaga()
function* syncToFirebaseSaga({ payload: userData }) {
try {
var uid = firebase.auth().currentUser.uid;
const database = (path, payload) => {
firebase
.database()
.ref(path)
.set(payload);
};
yield call(database, "users/" + uid + "/userData", userData);
yield console.log("successfully written to database");
} catch (error) {
alert(error);
}
}
So, at this point loading:true (confirmed that this works)
Then, as a part of componentDidMount of one of my root components, I have a listener for changes to the Firebase Database:
var props = this.props
function updateStateData(payload, props) {
props.syncFirebaseToState(payload);
}
function syncWithFirebase(uid, props) {
var syncStateWithFirebaseListener = firebase.database().ref("users/" + uid + "/userData");
syncStateWithFirebaseListener.on("value", function(snapshot) {
var localState = snapshot.val();
updateStateData(localState, props);
});
}
and this.props.syncFirebaseToState(payload) is a Redux action with this reducer:
return { ...state, data: action.payload, loading: false };
which then confirms that the data has been written to the Firebase Realtime Database, and then takes down the loading page, letting the user know that their update is now safe.
For most cases, this flow works fine. However, I run into problems when the user has a bad internet connection or if I refresh the page too fast. For example:
User loads app.
Disconnects from internet.
Submits data.
Full loop works immediately and loading:false (Firebase Realtime Database wrote it in 'offline mode' and is waiting to be reconnected to the internet)
User reconnects online.
Once online, user immediately refreshes the page (reloading the React app)
Firebase Realtime Database didn't have time to sync the queued updates to the remote database, and now after page refresh, the edits don't make it.
Sometimes, the user doesn't have to lose their internet connection. If they submit an edit (the page instantly returns a 'successful read') and then refresh before the remote server writes it down, the data is loss after the refresh is complete.
Anyway, as you can see, this is a really bad user experience. I really need a way to confirm that the data has actually been written to Firebase before removing the loading screen. I feel like I must be doing something wrong here and somehow getting a successful callback when it isn't.
This is my first time using React/Redux/Saga/Firebase, so I appreciate the patience and the help!
You could just disable offline mode.
I am assuming you don't want to do that so the next thing is to add a condition to check if your update is coming from the cache or the database.
Firebase Realtime Database provides a special location at /.info/connected which is updated every time the Firebase Realtime Database client's connection state changes. Here is an example:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
You can then run this check alongside your update to turn to load off and then propagate the change depending on whether it's coming from cache or the actual database.
Related
I am in the process of building a dapp for a project. I have one last thing to adjust: detect when a user changes metamask account to reset the state but it doesn't work.
//Doesn't work
window.ethereum.on('accountsChanged', function (accounts) {
console.log('accountsChanges', accounts);
setDefaultAccount(null);
});
// This works perfectly
window.ethereum.on('chainChanged', (chainId) => {
if(chainId !== "0x13881") {
setErrorMessage("Please connect on testnet Polygon Mumbai");
} else {
setErrorMessage(null);
window.location.reload();
}
});
I was also struggling with the same issue. Being unable to find the answer in the docs anywhere.
Until I'd realized that it's not meant to detect you switching from a connected account to a disconnected one.
In other words: it only detects it when you switch between accounts that are already connected to your Dapp. In that case - it functions perfectly. And detects an account change.
Go on ahead an test it on some popular Dapp out there. Connect just one of your accounts to it - then change it to another account on the same wallet, that is not yet connected - and it will also not be able to detect you changing it.
But if you connect two accounts right away - it will detect you switching between them and reflect your changes on its interface.
I tested this with PCS.
this is the correct way of implementation:
useEffect(() => {
ethereum?.on("accountsChanged", handleAccountChange);
return () => {
ethereum?.removeListener("accountsChanged", handleAccountChange);
};
});
Now write a listener for account change
const handleAccountChange = (...args) => {
// you can console to see the args
const accounts = args[0] ;
// if no accounts that means we are not connected
if (accounts.length === 0) {
console.log("Please connect to metamask");
// our old data is not current connected account
// currentAccount account that you already fetched and assume you stored it in useState
} else if (accounts[0] !== currentAccount) {
// if account changed you should update the currentAccount so you return the updated the data
// assuming you have [currentAccount,setCurrentAccount]=useState
// however you are tracking the state currentAccount, you have to update it. in case of redux you have to dispatch update action etc
setCurrentAccount(accounts[0)
}
};
I am simply trying to just print out the object that should be returning from my firebase DB call but instead, it by-passes the once method and jumps straight to the catch method. I posted the code below.
const db = firebase.database();
var info = db
.ref('/customers')
.once('value')
.then(snapshot => {
console.log('User data: ', snapshot.val());
})
.catch(console.error());
i resolved this mistake....nothing was actually wrong with the code its just that the device i was using was not connected to wi-fi so I am assuming it couldn't actually reach the Firebase servers. Silly mistake.
Looking at the sample code: https://www.apollographql.com/docs/tutorial/mutations/
Lets say login form mutation returns the JWT token and also the user profile. Is there a way to save the user profile to client cache so we can display the "home" page with this data immediately?
In other words, as we are already going to the server to authenticate, server return the data to start to display something straight away to the user.
I don't want to write purely client-side data such as isLoggedIn. I would like to write the whole profile data in a way the Home page can read from cache or server as we usually do with useQuery(...)
I figured it out. It's simple.
const client = useApolloClient();
const [loginUser, { loading }] = useMutation(LOGIN_USER, {
onCompleted(result) {
const membership = _.get(result, 'loginUser.`profile`');
const loginUserResult = _.get(result, 'loginUser');
sessionStorage.setItem(
'auth', '...'
);
client.writeData({ data: { profile } });
},
});
Then when home page renders, profile data is available in local cache and graphql doesn't make another request for profile
I've searched around a lot but have been unable to find a simple way to get flash messages from Express and render them in React.
I need to access the data on my Express server, but what is the best way of storing this and passing it down to React? I was thinking of passing an object down when the React index.html file is rendered, but I'm not sure how I can access this data, or send the correct data when certain events happen, for example a user enters the wrong password.
I solved the issue.
I simply have a variable in my session called flash which is set to false by default.
In the correct part of the passport flow I redefine this to a string, depending on the error. I have a React action and reducer to get this data and if it's truthy, render it to the screen. When the component unmounts or the site is refreshed I reset it to false.
EDIT: I have found a better solution
1. In the passport middleware set an optional message if something goes wrong.
return done(null, false, { message: 'Email not found' });
2. In the login route send this information as a response.
router.post('/login', (req, res, next) => {
passport.authenticate('local-login', (e, user, info) => {
if(e) return next(e);
if(info) return res.send(info);
req.logIn(user, e => {
if(e) return next(e);
return res.send(user);
});
})(req, res, next);
});
3. Handle the submission and response in a Redux action generator. If the user authenticates, then the message property will be undefined.
const res = await axios.post('/auth/login', { email, password });
dispatch({
type: 'FLASH',
payload: res.data.message
});
4. In the reducer, the state will be either a string or false:
return action.payload || false;
5. Then it's a question of rendering the state to the screen. Another action can be sent when the component unmounts to reset the state.
Hope this helps someone else out there.
expressjs/flash will place an array of flash objects onto res.locals. Per the docs: https://github.com/expressjs/flash#reslocalsflash
res.locals.flash
An array of flash messages of the form:
{
"type": "info",
"message": "message"
}
From my understanding, anything placed on res.locals is available in the global scope. In other words, you should be able to do window.flash which should return an Array of flash objects.
So you would simply loop over the array as you would normally in JavaScript. That is just my guess.
const makeFlashElement = ({type, message}) => {
return (
<div>
<h1>message</h1>
<h2>type</h2>
</div>
)
}
for (message in flash) {
makeFlashElement(message)
// ...
}
Typically you'd return a JSON response which React can easily digest.
See Karl Taylor's comment.
I am trying to understand a bit more about flux architecture and am designing a simple login component. Suppose when you login (POST ajax) and an error comes back. How would the information flow in flux?
I think the LoginComponent should do the work on a handleSubmit function.
After the ajax call comes back with an error and message should the component create an action such as "UpdateLoginStatus with payload {message: "no e-mail found"}. This would then trigger a LoginStore or something to save the status message and then it would emit an event such as "LoginStatusMessageChanged".
Another totally different component called LoginStatusMessage would register to listen to events on the LoginStore. It would get notified of this event and then proceede to update it's own state with the message. It would go out to LoginStore and fetch the message and display it to the user via render.
I believe this example covere your question quite well: https://github.com/przeor/react-router-flux-starter-kit/
Don't let your Component do the request. That would be mixing UI with business logic. That is always bad.
Entered username, password and button should be handled by your React component. Whenever the button gets clicked, the component should trigger an action.
//Component.js
handleSubmit: function(){
UserActions.login(this.state.username, this.state.password);
}
The action informs the store:
//UserActions.js
login(username, password) {
this.dispatch({username: username, password: password});
}
The Store then executes the needed AJAX (as the actions never should cause changes or requests themselves).
Depending on success or error, your store then triggers a new action. It should never save the response directly. Never save data in stores without an preceding action. If you want to, you can save that you are currently logging in (For instance, if you want to animate a spinner)
//UserStore.js
handleLogin(credentials) {
this.isLoggingIn = true;
AuthService.login(credentials.username, credentials.password).then((user) => {
UserActions.loginSuccess(user);
}).catch((errorMessage) => {
UserActions.loginFailed(errorMessage);
})
}
The actions again do their dumb thing, as they always should be nothing but dumb messengers.
//UserActions.js
loginSuccess(user) {
this.dispatch(user);
}
loginFailed(errorMessage) {
this.dispatch(errorMessage);
}
Up next: handle the actions in your store!
//UserStore.js
handleLoginSuccess(user) {
this.user = user;
this.isLoggedIn = true;
this.isLoggingIn = false;
}
handleLoginFailed(errorMessage) {
this.errorMessage = errorMessage;
this.isLoggingIn = false;
}
That's it. As long as your component listens to the UserStore, it get's informed when your user logs in, is successfully logged in or had an error, while having a clear separation of Logic and UI and a unimanual flow.
EDIT: Code examples are mostly pseudocodish. Actual implementation depend on your Flux framework.