Redux state change in componentWillMount is not be recognized in componentDidMount? - reactjs

Need to load my main component and, in case a localstorage with the pair value "logged: true" exists redirect to "/app" using react-router.
I am using react-redux and this is my code:
class Main extends Component {
componentWillMount(){
// Return true in redux state if localstorage is found
this.props.checkLogStatus();
}
componentDidMount(){
// redirect in case redux state returns logged = true
if(this.props.logStatus.logged){
hashHistory.push('/app');
}
}
render() {
return (
<App centered={true} className="_main">
{this.props.children}
</App>
);
}
}
My redux action:
checkLogStatus() {
// check if user is logged and set it to state
return {
type: LOGIN_STATUS,
payload: window.localStorage.sugarlockLogged === "true"
};
}
But when the component gets to the componentDidMount stage, my redux state has still not been updated.
Y manage to get this to work by using:
componentWillReceiveProps(nextProps){
if (nextProps.logStatus.logged && nextProps.logStatus.logged !== this.props.logStatus.logged){
hashHistory.push('/app');
}
}
But I am not sure it is the most elegant solution.
Thanks in advance!

Using componentWillReceiveProps is the way to go here since your logStatus object is being passed in as a prop that is being changed.
There is a more elegant way to this using the Redux-thunk middleware which allows you to dispatch a function (which receives dispatch as an argument instead of the object actions. You can then wrap that function in a promise and use it in componentWillMount.
In your actions file:
updateReduxStore(data) {
return {
type: LOGIN_STATUS,
payload: data.logInCheck
};
}
validateLocalStorage() {
...
}
checkLogStatus() {
return function(dispatch) {
return new Promise((resolve, reject) => {
validateLocalStorage().then((data) => {
if (JSON.parse(data).length > 0) {
dispatch(updateReduxStore(data));
resolve('valid login');
} else {
reject('invalid login');
}
});
});
};
}
Then in your component:
componentWillMount() {
this.props.checkLogStatus()
.then((message) => {
console.log(message); //valid login
hashHistory.push('/app');
})
.catch((err) => {
console.log(err); //invalid login
});
}
Redux-thunk middleware is made for such use cases.

Related

How do I prevent methods in an Unmounted class component from being called inadvertently?

I'm working on a React-Redux app which allows Google oauth2 login, and I've created a separate GoogleAuth component that handles all the login/logout processes.
The GoogleAuth component is usually in the header, as a child of the Header component. However, there are some pages (shown when the user is not logged in) in which I place the GoogleAuth component elsewhere, and remove it from the header.
For example, on the Login page, the GoogleAuth component is unmounted from the header, and another instance is mounted within the Login page.
I've included the GoogleAuth component's code below.
class GoogleAuth extends React.Component {
componentDidMount() {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId: 'xxxx',
scope: 'email'
}).then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
const isGoogleSignedIn = this.auth.isSignedIn.get();
if(this.props.isSignedIn !== isGoogleSignedIn) {
this.onAuthChange(isGoogleSignedIn);
}
this.auth.isSignedIn.listen(this.onAuthChange);
})
});
};
componentWillUnmount() {
delete this.auth;
}
onAuthChange = (isGoogleSignedIn) => {
console.log("onAuthChange called:", this.props.place);
//place is a label assigned to the parent component
if (!this.props.isSignedIn && isGoogleSignedIn && this.auth){
this.props.signIn(this.auth.currentUser.get().getId());
} else if (this.props.isSignedIn && !isGoogleSignedIn && this.auth) {
this.props.signOut();
}
}
handleSignIn = () => {
this.auth.signIn();
}
handleSignOut = () => {
this.auth.signOut();
}
renderAuthButton() {
if (this.props.isSignedIn === null) {
return null;
} else if (this.props.isSignedIn) {
return (
<button className="google-auth-button" onClick={this.handleSignOut}>
Sign Out
</button>
)
} else {
return (
<button className="google-auth-button" onClick={this.handleSignIn}>
Sign In With Your Google Account
</button>
)
}
}
render() {
return this.renderAuthButton()
}
}
const mapStateToProps = (state) => {
return {
isSignedIn: state.auth.isSignedIn,
userId: state.auth.userId
}
}
export default connect(
mapStateToProps,
{signIn, signOut}
)(GoogleAuth);
The issue I'm facing is that the onAuthChange method (triggered when the user's auth status changes) is being called once for every time the GoogleAuth component was ever mounted, even if it has been unmounted since. So if I open the app and then go to the Login page (which results in GoogleAuth being unmounted from the Header and inserted into the Login component), and then sign in, I see the following console logs.
onAuthChange called: header
onAuthChange called: login
I have a few things I'd appreciate some advice on:
Is this an issue that I need to resolve? What would happen if I did not do anything to resolve it?
I tried a little clean up when the component unmounts, by deleting the auth object explicitly. However, the onAuthChange method was still called for every instance of the GoogleAuth component whether mounted or not.
I was able to prevent duplicate calls to my action creators by checking whether this.auth exists. Any drawbacks to this approach?
Is there a time after which the unmounted components will get automatically cleared?
Yes it is, as it's basically a memory leak. The logs tell you that the function which calls console.log is not being GCed
The this.auth is set to window.gapi.auth2.getAuthInstance(). delete this.auth only removes the auth key from your this. As long as there are other references to the object returned by window.gapi.auth2.getAuthInstance() (for example in the library itself, looks a lot like a singleton) it will not be GCed -> your delete doesn't make much of a difference
See point 1.
Yes, when the user closes your tab.
To resolve the issue you'll have to remove the listener from this.auth.isSignedIn in your componentWillUnmount, possibly there's a function called this.auth.isSignedIn.removeListener(...). If not you can check the documentation on how to remove the listener.
I'd also advise against loading the API and initialising your client in every instance of your component and much rather do it only a single time.
Edit: To unregister the listener you can have a look at this question: How to remove Google OAuth2 gapi event listener?
However I'd advise against taking the route of adding/removing listeners for every component instance.
Edit 2: Example
class GoogleAuth extends React.Component {
componentDidMount() {
googleAuthPromise.then(
googleAuth => {
const isGoogleSignedIn = googleAuth.isSignedIn.get();
if(this.props.isSignedIn !== isGoogleSignedIn) {
this.onAuthChange(isGoogleSignedIn);
}
this.unregisterListener = listenSignIn(this.onAuthChange);
}
)
};
componentWillUnmount() {
this.unregisterListener();
}
onAuthChange = async (isGoogleSignedIn) => {
console.log("onAuthChange called:", this.props.place);
const googleAuth = await googleAuthPromise;
if (!this.props.isSignedIn && isGoogleSignedIn && googleAuth){
this.props.signIn(googleAuth.currentUser.get().getId());
} else if (this.props.isSignedIn && !isGoogleSignedIn && googleAuth) {
this.props.signOut();
}
}
handleSignIn = async () => {
const googleAuth = await googleAuthPromise;
return googleAuth.signIn();
}
handleSignOut = async () => {
const googleAuth = await googleAuthPromise;
return googleAuth.signOut();
}
renderAuthButton() {
if (this.props.isSignedIn === null) {
return null;
} else if (this.props.isSignedIn) {
return (
<button className="google-auth-button" onClick={this.handleSignOut}>
Sign Out
</button>
)
} else {
return (
<button className="google-auth-button" onClick={this.handleSignIn}>
Sign In With Your Google Account
</button>
)
}
}
render() {
return this.renderAuthButton()
}
}
const mapStateToProps = (state) => {
return {
isSignedIn: state.auth.isSignedIn,
userId: state.auth.userId
}
}
export default connect(
mapStateToProps,
{signIn, signOut}
)(GoogleAuth);
// create a customer listener manager in order to be able to unregister listeners
let listeners = []
const invokeListeners = (...args) => {
for (const listener of listeners) {
listener(...args)
}
}
const listenSignIn = (listener) => {
listeners = listeners.concat(listener)
return () => {
listeners = listeners.filter(l => l !== listener)
}
}
// make the client a promise to be able to wait for it
const googleAuthPromise = new Promise((resolve, reject) => {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId: 'xxxx',
scope: 'email'
}).then(resolve, reject)
})
})
// register our custom listener handler
googleAuthPromise.then(googleAuth => {
googleAuth.isSignedIn.listen(invokeListeners)
})
Disclaimer: this is untested code, you might need to make small corrections.

Google signin is not working properly on ComponentDidMount

componentDidMount() {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId:
process.env.REACT_APP_CLIENT_ID,
scope: "email"
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
this.onAuthChange(this.auth.isSignedIn.get());
this.auth.isSignedIn.listen(this.onAuthChange);
});
});
}
// this function will called when auth status changes, according to gapi
onAuthChange = isSignedIn => {
if (isSignedIn) {
this.props.signIn();
} else {
this.props.signOut();
}
};
when page loads for the first time, depending upon the current auth state of gapi, I changed state through the redux store. but it didn't change the state. It always refers to the initial state, which I hardcoded to null.
Check in your onAuthChange if the 'isSignedIn' value is true after the network call (using a console.log(isSignedIn). It looks like you may be retrieving the value too soon after you are setting the the state in Redux. you may want to use a componentDidUpdate here.
componentDidMount() {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId:
process.env.REACT_APP_CLIENT_ID,
scope: "email"
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
});
});
}
componentDidUpdate(prevProps) {
//you should be importing the isSignedIn into props then you will be able
//to use componentDidUpdate, it will not work otherwise
if (prevProps.isSignedIn !== this.props.isSignedIn) {
this.onAuthChange(this.props.isSignedIn);
}
}
// this function will called when auth status changes, according to gapi
onAuthChange = isSignedIn => {
if (isSignedIn) {
this.props.signIn();
} else {
this.props.signOut();
}
};

react -redux component does not re-render after state change

I have been trying and trying by my component wont re-render itself . Below is my reducer code and I have tried everything to not mutate the state. In my component code ,inside render method, I have a log statement console.log("Check Here"); I know the component does not re-render because this log works first time the component renders but after reducer changes the state the log statement is not called . In logs I can clearly see that prev state and next state are different by just that one SearchType that I am changing. Please help!!
const initState = {
searchType: ""
};
const techniqueReducer = (state = initState, action) => {
switch (action.type) {
case actionTypeConstants.GET_SEARCH:
{
return { ...state, searchType: "new string" };
}
default: {
return state;
}
}
};
export default myReducer;
My component code is below
import React, { Component } from "react";
import { connect } from "react-redux";
import * as tDispatchers from "../actions/Actions";
const mapStateToProps = state => {
  return {
  
    searchType: state.searchType
  };
};
class SearchCollection extends Component {
  Search= () => {
    this.props.dispatch(tDispatchers.getSearch(document.getElementById("txtSearch").value));
  }
 
  render() {
console.log("Check Here")
    return (
      <div class="container-fluid">
        <div>
          <input
            type="text"
            id="txtSearch"
            class="form-control"
            placeholder="Enter Search Keywords Here..."
          />
        </div>
        <div>
  <button
            className="btn btn-light btn-sm m-1"
            onClick={this.Search}
          >
            Search
          </button>
         
        </div>
  
      </div>
    );
  }
}
export default connect(mapStateToProps)(SearchCollection);
GetSearch looks like below
I plan to pass payload to reducer eventually but currently I am not
import * as actionTypeConstants from "../action_type_constants";
import axios from "axios";
export function getSearch(searchtext) {
return dispatchFunction => {
axios
.get("<api call>"+searchtext)
.then(response => {
dispatchFunction({
type: actionTypeConstants.GET_SEARCH,
payload: response.data.data
});
})
};
}
ActionTypeConstant
export const GET_SEARCH = "GET_SEARCH";
I suppose you are using redux-thunk to work with async actions. But you don't return an async function from getSearch. I believe it should be
export function getSearch(searchtext) {
return dispatchFunction => {
return axios
.get("<api call>"+searchtext)
.then(response => {
dispatchFunction({
type: actionTypeConstants.GET_SEARCH,
payload: response.data.data
});
})
};
}
or
export function getSearch(searchtext) {
return async dispatchFunction => {
const response = await axios
.get("<api call>"+searchtext);
dispatchFunction({
type: actionTypeConstants.GET_SEARCH,
payload: response.data.data
});
};
}
You are not updating searchType value, which is hardcoded to string new string. Try setting the new state from the action, for example:
return { ...state, searchType: action.payload};
Or check this, https://jsfiddle.net/xt3sqoc6/1/ and open your dev tools to see the rerenders.
You can use componentDidUpdate(prevProps, prevState). It is invoked immediately after updating occurs & you can compare the current props to previous props. Using that you can re-render your component by changing state
componentDidUpdate(prevProps) {
if (this.props.SearchType !== prevProps.SearchType) {
//Do whatever needs to happen!
}
}
You may call setState() immediately in componentDidUpdate but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.
Hope this helps you. Feel free for doubts.

redux doesn't update props immediately

I have a functional component in a react native application, and I'm dispatching an HTTP call. If some error occurs, I store it in redux, the problem is when I access to the error value, I get null
function IncomingOrder(props) {
function acceptOrder(orderId, coords) { // I call this from a button
const response = await actions
.accept(token, orderId, coords) //this is the async function
.catch((e) => {
showError(props.error);
});
}
...
}
...
function mapStateToProps(state) {
return {
token: state.auth.token,
error: state.deliveries.error
};
}
function mapDispatchToProps(dispatch) {
return {
actions: {
accept: (token, id, coords) => {
return dispatch(Order.accept(token, id, coords));
}
}
};
}
The problem is most likely on acceptOrder() async action creator.
Redux dispatch wont update immediately after your promise resolves/rejects. So by the time your error handler (Promise.prototype.catch or catch(){}) kicks in, there is no guarantee the action has been dispatched or the state tree updated.
What you want to do instead is to have this logic on the async action creator
// action module Order.js
export const accept = (token, id, coords) => dispatch => {
fetch('/my/api/endpoint')
.then(response => doSomethingWith(response))
.catch(err => dispatch(loadingFailed(err)); // <----- THIS is the important line
}
// reducer.js
// you want your reducer here to handle the action created by loadingFailed action creator. THEN you want to put the error in the state.
// IncomingOrder.js
function IncomingOrder(props) {
function acceptOrder(orderId, coords) { // I call this from a button
actions.accept(token, orderId, coords);
// You don't need to wait for this, as you're not gonna work with the result of this call. Instead, the result of this call is put on the state by your reducer.
}
render() {
const { error } => this.props; // you take the error from the state, which was put in here by loadingFailed()
if (error) {
// render your error UI
} else {
// render your regular UI
}
}
}
function mapStateToProps(state) {
return {
token: state.auth.token,
error: state.deliveries.error // <--- I suppose your error is already here (?)
};
}

Realize a confirm component with react, redux and promise

I try to build a generic confirm component with redux and native promise. I read Dan Abramovs solution here: How can I display a modal dialog in Redux that performs asynchronous actions? but i am looking for a more generic appoach.
Basically i want to do this:
confirm({
type: 'warning',
title: 'Are you sure?',
description: 'Would you like to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort'
})
.then(() => {
// do something after promise is resolved
})
The confirm method basically opens the modal and returns a promise. Inside the promise i subscribe my redux store, listen for state changes and resolve or reject the promise:
export const confirm = function(settings) {
// first dispatch openConfirmModal with given props
store.dispatch(
openConfirmModal({
...settings
})
);
// return a promise that subscribes to redux store
// see: http://redux.js.org/docs/api/Store.html#subscribe
// on stateChanges check for resolved/rejected
// if resolved or rejected:
// - dispatch closeConfirmModal
// - resolve or reject the promise
// - unsubscribe to store
return new Promise((resolve, reject) => {
function handleStateChange() {
let newState = store.getState();
if (newState.confirmModal.resolved) {
store.dispatch(closeConfirmModal());
resolve();
unsubscribe();
}
if (newState.confirmModal.rejected) {
store.dispatch(closeConfirmModal());
reject();
unsubscribe();
}
}
let unsubscribe = store.subscribe(handleStateChange);
})
}
My confirm component is connected to redux store and is included once in some kind of layout component - so it is useable on all routes in the app:
class ConfirmModal extends Component {
constructor(props) {
super(props)
}
confirm() {
this.props.dispatch(resolveConfirmModal());
}
abort() {
this.props.dispatch(rejectConfirmModal());
}
render() {
// my modal window
}
}
export default connect(
state => ({
confirmModal: state.confirmModal
})
)(ConfirmModal);
Reducer/Action looks like this:
export const openConfirmModal = (settings) => {
return {
type: 'OPEN_CONFIRM_MODAL',
settings
};
};
export const resolveConfirmModal = () => {
return {
type: 'RESOLVE_CONFIRM_MODAL'
};
};
export const rejectConfirmModal = () => {
return {
type: 'REJECT_CONFIRM_MODAL'
};
};
export const closeConfirmModal = () => {
return {
type: 'CLOSE_CONFIRM_MODAL'
};
};
const initialState = {
open: false,
type: 'info',
title: 'Are you sure?',
description: 'Are you sure you want to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort',
};
export const ConfirmModalReducer = (state = initialState, action) => {
switch (action.type) {
case 'OPEN_CONFIRM_MODAL':
return { ...action.settings, open: true };
case 'RESOLVE_CONFIRM_MODAL':
return { ...state, resolved: true };
case 'REJECT_CONFIRM_MODAL':
return { ...state, rejected: true };
case 'CLOSE_CONFIRM_MODAL':
return initialState;
default:
return state;
}
};
The redux part is working. My confirm window can be open/closed and renders depending on my options. But how i can define a promise in my confirm method that can be resolved in my component? How i get everything connected?
Found a working Solution!
Found a solution that is pretty much what i was looking for:
The modal properties are driven by my Redux state
The modal component is included once AND it lives inside my
applicition not as a different rendered app like
here:http://blog.arkency.com/2015/04/beautiful-confirm-window-with-react/
The confirm method returns a native promise that is
resolved/rejected driven by Redux state
What do you think?
Well, you can do it, but it won't be pretty. Basically, you need a map of outstanding promises next to your confirm():
var outstandingModals = {}
const confirm = function(settings) {
return new Promise((resolve, reject) => {
let id = uuid.v4();
outstandingModals = resolve;
store.dispatch(
openConfirmModal({
...settings,
confirmationId: id,
})
);
}
and then later:
case 'CLOSE_CONFIRM_MODAL':
let resolve = outstandingModals[state.confirmationId];
if (resolve) {
resolve();
delete outstandingModals[state.confirmationId];
}
return initialState;
Like I said - ugly. I don't think you can do much better than that using promises.
But you can do better by NOT using promises. What I would do is simply render a Confirm component whenever necessary, say:
render() {
return <div>
... My stuff ...
{confirmationNecessary && <Confirm text='Are you sure?' onAction={this.thenConfirmed}/>}
</div>
}
confirmationNecessary can come from this.state or from the store.
I wrote a blog post that discusses one possible approach for managing "generic" modals like this: Posts on PacktPub: "Generic Redux Modals" and "Building Better Bundles".
The basic idea is that the code that requested the modal can include a pre-written action as a prop, and the modal can dispatch that action once it's closed.
There's also an interesting-looking library at redux-promising-modals that appears to implement modal result promises through middleware.
possible so :)
export const MsgBox = {
okCancel : s =>
new Promise((ok, cancel) =>
confirm(s) ? ok() : cancel() ),
......etc

Resources