Reputation:
I'm trying to write a HOC in typescript to render a spinner while the user have to wait.
I started from this article.
Here is the WithFullScreenSpinnerHOC.ts
typescript code I have:
import RX = require('reactxp');
const styles = {
semiTransparentBlackBbackground: RX.Styles.createViewStyle(
{ backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center' }
),
};
export interface withFullScreenSpinnerProps {
};
export interface withFullScreenSpinnerState {
showSpinner: boolean
};
// Higher-Order component that will allow any component to display a fullscreen activity indicator
const withFullScreenSpinner = <P extends withFullScreenSpinnerProps, S extends withFullScreenSpinnerState>(
WrappedComponent: new (props: P) => RX.Component<P, S>
) =>
class WithFullScreenSpinner extends RX.Component<P & withFullScreenSpinnerProps, S & withFullScreenSpinnerState> {
constructor(props) {
super(props);
this.state = {
showSpinner: false
} as S & withFullScreenSpinnerState;
}
render() {
return (
<RX.View style={{ flex: 1 }}>
<WrappedComponent {...this.props}>
</WrappedComponent>
{this.state.showSpinner &&
<RX.View
style={styles.semiTransparentBlackBbackground}
>
<RX.ActivityIndicator
size="large"
color="black"
/>
</RX.View>}
</RX.View>
);
}
};
export default withFullScreenSpinner;
The component that will use this HOC must have a showSpinner state variable.
So I have a SignIn page for instance that is wrapped with this HOC.
import WithFullScreenSpinner from './WithFullScreenSpinnerHOC'
// The state of the sign-in component (data entered by the user)
interface signInState {
username: string,
password: string,
errorMessage?: string
ready: boolean,
session: any,
showSpinner: boolean
};
class SignIn extends RX.Component<signInProps, signInState> {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
errorMessage: null,
ready: false,
session: null,
showSpinner: false
};
}
private showSpinner() {
this.setState((prevState, props) => {
let newState = { ...prevState };
newState.showSpinner = true;
return newState;
})
}
private hidepinner() {
this.setState((prevState, props) => {
let newState = { ...prevState };
newState.showSpinner = false;
return newState;
})
}
// Do the Sign In using the auth API that comes through the props of the WithAuth HOC
doSignIn = async (username: string, password: string) => {
const { authLayer } = this.props;
try {
this.showSpinner();
const user = await authLayer.signIn(username, password);
const requireMFA = (user.Session !== null);
this.setState((prevState, props) => {
let newState = { ...prevState };
newState.showMFAPrompt = requireMFA;
newState.session = user.session;
return newState;
});
// Should be null if MFA enabled
return user.signInUserSession;
}
catch (err) {
console.log(err);
this.setState((prevState, props) => {
let newState = { ...prevState };
newState.errorMessage = err.message;
return newState;
});
}
finally {
this.hidepinner();
}
}
render() {
return (
);
}
};
export default WithFullScreenSpinner(SignIn);
But I'm missing something here because changing the state in the wrapped component does not trigger the HOC render method.
I've set a breakpoint in the HOC's render method to confirm that it is called only once.
Any help appreciated.
EDIT 1: This is by design, the components are not related, thus I need a way to communicate between each others. I'll try Resub and answer my own question if this works.
EDIT 2: I have posted my solution below. I have not accepted it as the correct answer yet because I would like to have suggestions/comments.
Upvotes: 0
Views: 137
Reputation:
The solution I have found is to use Resub
I have defined a store that will hold the visibility value of the spinner:
import { autoSubscribe, AutoSubscribeStore, StoreBase } from "resub";
// Resub Store that will allow any component to communicate with the FullScreenSpinner HOC
// https://github.com/Microsoft/ReSub
@AutoSubscribeStore
class SpinnerStateStore extends StoreBase {
private visible: boolean = false;
// Call this from the Wrapped Component to ask for the spinner to be shown/hidden
public toggleVisibility(visible: boolean) {
this.visible = visible;
// Do not forget this call otherwise autosubscriptions won't be called
this.trigger();
}
// Reserved for the FullScreenSpinnerHOC
@autoSubscribe
public isVisible() {
return this.visible;
}
}
export = new SpinnerStateStore();
The HOC have to extends ComponentBase instead of RX.Component. The code in the question becomes:
import RX = require("reactxp");
import ComponentBase from "resub/dist/ComponentBase";
import WithFullScreenSpinnerStateStore = require("./WithFullScreenSpinnerStore");
const styles = {
semiTransparentBlackBbackground: RX.Styles.createViewStyle(
{ backgroundColor: "rgba(0, 0, 0, 0.7)", justifyContent: "center" },
),
};
// tslint:disable-next-line:no-empty-interface
export interface IWithFullScreenSpinnerProps {
}
export interface IWithFullScreenSpinnerState {
pendingIO: boolean;
}
// Higher-Order component that will allow any component to display a fullscreen activity indicator
const withFullScreenSpinner = <P extends IWithFullScreenSpinnerProps, S extends IWithFullScreenSpinnerState>(
WrappedComponent: new (props: P) => RX.Component<P, S>,
) =>
// tslint:disable-next-line:max-line-length
class WithFullScreenSpinner extends ComponentBase<P & IWithFullScreenSpinnerProps, S & IWithFullScreenSpinnerState> {
constructor(props) {
super(props);
this.state = {
pendingIO: false,
} as S & IWithFullScreenSpinnerState;
}
// tslint:disable-next-line:max-line-length
protected _buildState(props: P & IWithFullScreenSpinnerProps, initialBuild: boolean): S & IWithFullScreenSpinnerState {
return {
pendingIO: WithFullScreenSpinnerStateStore.isVisible(),
} as S & IWithFullScreenSpinnerState;
}
// tslint:disable-next-line:member-ordering
public render() {
return (
<RX.View style={{ flex: 1 }}>
<WrappedComponent {...this.props}>
</WrappedComponent>
{this.state.pendingIO &&
<RX.View
style={styles.semiTransparentBlackBbackground}
>
<RX.ActivityIndicator
size="large"
color="black"
/>
</RX.View>}
</RX.View>
);
}
};
export default withFullScreenSpinner;
This is working because each time you call SpinnerStateStore.toggleVisibility()
, WithFullScreenSpinner._buildState()
will be called-as a result of this.trigger()
;.
Upvotes: 0