I learn ReactJs and have a design Composition question about ReactJs higher order component (HOC).
In the code below App.jsx I use this withAuthentication HOC that initializes app core processes. This HOC value is not used in the App.js. Therefore I must suppress all withAuthentication HOC render callbaks and I do that in the shouldComponentUpdate by returning false.
(I use this HOC in many other places to the get HOC's value but not in App.jsx)
File App.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { getAlbumData } from './redux/albumData/albumData.actions';
import { getMetaData } from './redux/albumMetaData/albumMetaData.actions';
import Header from './components/structure/Header';
import Content from './components/structure/Content';
import Footer from './components/structure/Footer';
import { withAuthentication } from './session';
import './styles/index.css';
class App extends Component {
componentDidMount() {
const { getMeta, getAlbum } = this.props;
getMeta();
getAlbum();
}
shouldComponentUpdate() {
// suppress render for now boilerplate, since withAuthentication
// wrapper is only used for initialization. App don't need the value
return false;
}
render() {
return (
<div>
<Header />
<Content />
<Footer />
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
getMeta: () => dispatch(getMetaData()),
getAlbum: () => dispatch(getAlbumData()),
});
export default compose(connect(null, mapDispatchToProps), withAuthentication)(App);
The HOC rwapper WithAuthentication below is a standard HOC that render Component(App) when changes are made to Firebase user Document, like user-role changes, user auth-state changes..
File WithAuthentication .jsx
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import AuthUserContext from './context';
import { withFirebase } from '../firebase';
import * as ROLES from '../constants/roles';
import { setCurrentUser, startUserListener } from '../redux/userData/user.actions';
import { selectUserSlice } from '../redux/userData/user.selectors';
const WithAuthentication = Component => {
class withAuthentication extends React.Component {
constructor() {
super();
this.state = {
authUser: JSON.parse(localStorage.getItem('authUser')),
};
}
componentDidMount() {
const { firebase, setUser, startUserListen } = this.props;
this.authListener = firebase.onAuthUserListener(
authUser => {
this.setState({ authUser });
setUser(authUser);
startUserListen();
},
() => {
localStorage.removeItem('authUser');
this.setState({ authUser: null });
const roles = [];
roles.push(ROLES.ANON);
firebase
.doSignInAnonymously()
.then(authUser => {
if (process.env.NODE_ENV !== 'production')
console.log(`Sucessfully signed in to Firebase Anonymously with UID: ${firebase.getCurrentUserUid()}`);
firebase.doLogEvent('login', { method: 'Anonymous' });
firebase
.userDoc(authUser.user.uid)
.set({
displayName: `User-${authUser.user.uid.substring(0, 6)}`,
roles,
date: firebase.fieldValue.serverTimestamp(),
})
.then(() => {
console.log('New user saved to Firestore!');
})
.catch(error => {
console.log(`Could not save user to Firestore! ${error.code}`);
});
})
.catch(error => {
console.error(`Failed to sign in to Firebase: ${error.code} - ${error.message}`);
});
},
);
}
componentWillUnmount() {
this.authListener();
}
render() {
const { currentUser } = this.props;
let { authUser } = this.state;
// ALl changes to user object will trigger an update
if (currentUser) authUser = currentUser;
return (
<AuthUserContext.Provider value={authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
withAuthentication.whyDidYouRender = true;
const mapDispatchToProps = dispatch => ({
setUser: authUser => dispatch(setCurrentUser(authUser)),
startUserListen: () => dispatch(startUserListener()),
});
const mapStateToProps = state => {
return {
currentUser: selectUserSlice(state),
};
};
return compose(connect(mapStateToProps, mapDispatchToProps), withFirebase)(withAuthentication);
};
export default WithAuthentication;
My question is will this hit me later with problems or is this ok to do it like this?
I know a HOC is not suppose to be used like this. The WithAuthentication is taking care of Authentication against Firebase and then render on all user object changes both local and from Firestore listener snapshot.
This HOC is used in many other places correctly but App.jsx only need to initialize the HOC and never use it's service.
My question is will this hit me later with problems or is this ok to do it like this?
Related
I am trying to figure out how to use Firebase.
I have a config with an auth listener:
onAuthUserListener(next, fallback) {
// onUserDataListener(next, fallback) {
return this.auth.onAuthStateChanged(authUser => {
if (!authUser) {
// user not logged in, call fallback handler
fallback();
return;
}
this.user(authUser.uid).get()
.then(snapshot => {
let snapshotData = snapshot.data();
let userData = {
...snapshotData, // snapshotData first so it doesn't override information from authUser object
uid: authUser.uid,
email: authUser.email,
emailVerified: authUser.emailVerifed,
providerData: authUser.providerData
};
setTimeout(() => next(userData), 0); // escapes this Promise's error handler
})
.catch(err => {
// TODO: Handle error?
console.error('An error occured -> ', err.code ? err.code + ': ' + err.message : (err.message || err));
setTimeout(fallback, 0); // escapes this Promise's error handler
});
});
}
// ... other methods ...
// }
I have read the documentation about creating a listener to see if there is an authUser and have got this authentication listener plugged in.
import React from 'react';
import { AuthUserContext } from '../Session/Index';
import { withFirebase } from '../Firebase/Index';
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: null,
};
}
componentDidMount() {
this.listener = this.props.firebase.auth.onAuthStateChanged(
authUser => {
authUser
? this.setState({ authUser })
: this.setState({ authUser: null });
},
);
}
componentWillUnmount() {
this.listener();
};
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withFirebase(WithAuthentication);
};
export default withAuthentication;
Then in the consumer component I have:
import React from 'react';
import {
BrowserRouter as Router,
Route,
Link,
Switch,
useRouteMatch,
} from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';
const { Title, Text } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
class Dashboard extends React.Component {
state = {
collapsed: false,
loading: false,
};
onCollapse = collapsed => {
console.log(collapsed);
this.setState({ collapsed });
};
render() {
return (
<AuthUserContext.Consumer>
{ authUser => (
<div>
<Text style={{ float: 'right', color: "#fff"}}>
{/*
{
this.props.firebase.db.collection('users').doc(authUser.uid).get()
.then(doc => {
console.log( doc.data().name
)
})
}
*/}
</div>
)}
</AuthUserContext.Consumer>
);
}
}
export default withFirebase(Dashboard);
It works fine the first time the page is loaded.
However, on a page refresh, the system is slower than the code and returns null error messages that say:
TypeError: Cannot read property 'uid' of null (anonymous function)
I have seen this article which proposes solutions for Angular.
I can't find a way to implement this so that it works in react.
The article suggests:
firebase.auth().onAuthStateChanged( user =>; {
if (user) { this.userId = user.uid }
});
So, in my listener I tried putting if in front of authUser - but that doesn't seem to be an approach that works.
Any advice on what to try next to make a listener that lets firebase load the user before it runs the check?
Try react-with-firebase-auth this library.
This library makes a withFirebaseAuth() function available to you.
import * as React from 'react';
import * as firebase from 'firebase/app';
import 'firebase/auth';
import withFirebaseAuth, { WrappedComponentProps } from 'react-with-firebase-auth';
import firebaseConfig from './firebaseConfig';
const firebaseApp = firebase.initializeApp(firebaseConfig);
const App = ({
/** These props are provided by withFirebaseAuth HOC */
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signInWithGoogle,
signInWithFacebook,
signInWithGithub,
signInWithTwitter,
signInAnonymously,
signOut,
setError,
user,
error,
loading,
}: WrappedComponentProps) => (
<React.Fragment>
{
user
? <h1>Hello, {user.displayName}</h1>
: <h1>Log in</h1>
}
{
user
? <button onClick={signOut}>Sign out</button>
: <button onClick={signInWithGoogle}>Sign in with Google</button>
}
{
loading && <h2>Loading..</h2>
}
</React.Fragment>
);
const firebaseAppAuth = firebaseApp.auth();
/** See the signature above to find out the available providers */
const providers = {
googleProvider: new firebase.auth.GoogleAuthProvider(),
};
/** providers can be customised as per the Firebase documentation on auth providers **/
providers.googleProvider.setCustomParameters({hd:"mycompany.com"});
/** Wrap it */
export default withFirebaseAuth({
providers,
firebaseAppAuth,
})(App);
I'm trying to test my LoginForm component using jest and react-testing-library. When the login form is submitted successfully, my handleLoginSuccess function is supposed to set the 'user' item on localStorage and navigate the user back to the home page using history.push(). This works in my browser in the dev environment, but when I render the component using Jest and mock out the API, localStorage gets updated but the navigation to '/' doesn't happen.
I've tried setting localStorage before calling history.push(). I'm not sure what is responsible for re-rendering in this case, and why it works in dev but not test.
Login.test.jsx
import 'babel-polyfill'
import React from 'react'
import {withRouter} from 'react-router'
import {Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import {render, fireEvent} from '#testing-library/react'
import Login from '../../pages/Login'
import API from '../../util/api'
jest.mock('../../util/api')
function renderWithRouter(
ui,
{route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
) {
return {
...render(<Router history={history}>{ui}</Router>),
// adding `history` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
}
}
describe('When a user submits the login button', () => {
test('it allows the user to login', async () => {
const fakeUserResponse = {'status': 200, 'data': { 'user': 'Leo' } }
API.mockImplementation(() => {
return {
post: () => {
return Promise.resolve(fakeUserResponse)
}
}
})
const route = '/arbitrary-route'
const {getByLabelText, getByText, findByText} = renderWithRouter(<Login />, {route})
fireEvent.change(getByLabelText(/email/i), {target: {value: 'email#gmail.com '}})
fireEvent.change(getByLabelText(/password/i), {target: {value: 'Foobar123'}})
fireEvent.click(getByText(/Log in/i))
const logout = await findByText(/Log Out/i)
expect(JSON.parse(window.localStorage.getItem('vector-user'))).toEqual(fakeUserResponse.data.user)
})
})
relevant parts of LoginForm.jsx
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
disableActions: false,
formErrors: null,
};
}
handleLoginSuccess = () => {
const { loginSuccessCallback, redirectOnLogin, history } = { ...this.props };
if (loginSuccessCallback) {
loginSuccessCallback();
} else {
history.push('/');
}
}
loginUser = ({ user }) => {
localStorage.setItem('vector-user', JSON.stringify(user));
}
handleLoginResponse = (response) => {
if (response.status !== 200) {
this.handleResponseErrors(response.errors);
} else {
this.loginUser(response.data);
this.handleLoginSuccess();
}
}
handleLoginSubmit = (event) => {
event.preventDefault();
const {
disableActions, email, password
} = { ...this.state };
if (disableActions === true) {
return false;
}
const validator = new Validator();
if (!validator.validateForm(event.target)) {
this.handleResponseErrors(validator.errors);
return false;
}
this.setState(prevState => ({ ...prevState, disableActions: true }));
new API().post('login', { email, password }).then(this.handleLoginResponse);
return true;
}
}
Login.jsx
import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import LoginForm from '../components/LoginForm';
class Login extends React.Component {
constructor({ location }) {
super();
const originalRequest = location.state && location.state.originalRequest;
this.state = {
originalRequest
};
}
render() {
const { originalRequest } = { ...this.state };
return (
<div>
<h1>Login</h1>
<LoginForm redirectOnLogin={originalRequest && originalRequest.pathname} />
<Link to="/forgot">Forgot your password?</Link>
</div>
);
}
}
Login.propTypes = {
location: PropTypes.shape({
state: PropTypes.shape({
originalRequest: PropTypes.shape({
pathname: PropTypes.string
})
})
})
};
export default withRouter(Login);
Currently the await findByText() times out.
I think that's because in your tests you're not rendering any Route components. Without those react-router has no way to know what to render when the route changes. It will always render Login.
I have this URL: /user/Jd5Egbh...
I use react-router-dom for navigation. I have made a UserDetails component.
I use mapStateToProps to get url params like this:
function mapStateToProps({ users }, ownProps) {
return { user: users[ownProps.match.params.uid]};
}
It works fine but I would like to protect this route and allow access only for authenticated users.
In other components, I use a withAuthorization component and compose from recomposing to protect the route.
I tried to combine these two features like below:
export default compose(
withAuthorization(authCondition),
connect(mapStateToProps, { fetchUser })
)(UserDetails);
But ownProps is undefined in mapStateToProps function
How can I access URL params if I use compose to protect route?
EDIT1:
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
import { firebase, db, auth } from '../firebase';
const withAuthorization = (condition) => (Component) => {
class WithAuthorization extends React.Component {
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!condition(authUser)) {
this.props.history.push('/');
} else {
db.onceGetUser(authUser.uid).then(snapshot => {
let user_data = snapshot.val();
if (!user_data.admin) {
auth.doSignOut();
}
});
}
});
}
render() {
return this.props.authUser ? <Component /> : null;
}
}
const mapStateToProps = (state) => ({
authUser: state.auth.authUser,
});
return compose(
withRouter,
connect(mapStateToProps),
)(WithAuthorization);
}
export default withAuthorization;
I am building a react native application but I noticed that componentWillReceiveProps is not getting called as soon as I dispatch some actions to the redux store, it only gets called when I refresh the screen.
Component
import React from 'react';
import { connect } from 'react-redux';
import { renderLogin } from '../../components/Auth/Login';
class HomeScreen extends React.Component {
componentWillReceiveProps(props) {
const { navigate } = props.navigation;
if (props.userData.authenticated) {
navigate('dashboard')
}
}
login = () => {
renderLogin()
}
render() {
const { navigate } = this.props.navigation;
return (
<Container style={styles.home}>
// Some data
</container>
)
}
}
function mapStateToProps(state) {
return {
userData: state.auth
}
}
export default connect(mapStateToProps)(HomeScreen)
RenderLogin
export function renderLogin() {
auth0
.webAuth
.authorize({
scope: 'openid email profile',
audience: 'https://siteurl.auth0.com/userinfo'
})
.then(function (credentials) {
loginAction(credentials)
}
)
.catch(error => console.log(error))
}
loginAction
const store = configureStore();
export function loginAction(credentials) {
const decoded = decode(credentials.idToken);
saveItem('token', credentials.idToken)
store.dispatch(setCurrentUser(decoded));
}
export async function saveItem(item, selectedValue) {
try {
await AsyncStorage.setItem(item, JSON.stringify(selectedValue));
const decoded = decode(selectedValue);
} catch (error) {
console.error('AsyncStorage error: ' + error.message);
}
}
I believe your problem has something to do with mapStateToProps, i.e. when you have updated your state in redux but not yet map the new state to your props, therefore props in HomeScreen will remain unchanged and componentWillReceiveProps will only be triggered once.
Have a read on Proper use of react-redux connect and Understanding React-Redux and mapStateToProps.
I've tried a few ways of going about this where in my action is do the dispatch(push) :
import LoginService from '../../services/login-service';
export const ActionTypes = {
SET_LOGIN: 'SET_LOGIN',
}
export function getLogin(data, dispatch) {
LoginService.getLoginInfo(data).then((response) => {
const login = response.data;
dispatch({
type: ActionTypes.SET_LOGIN,
login
})
// .then((res) => {
// dispatch(push('/'));
// })
})
}
I even saw something about using the render property on the route for react router.
Now I am trying to use renderIf based on whether my state is true or false but I can't seem to get props on this page successfully :
import React from 'react';
import renderIf from 'render-if';
import { Redirect } from 'react-router-dom';
import LoginHeader from '../../components/header/login-header';
import LoginPage from './login-page';
const LoginHome = props => (
<div>
{console.log(props)}
{renderIf(props.loginReducer.status === false)(
<div>
<LoginHeader />
<LoginPage />}
</div>)}
{renderIf(props.loginReducer.status === true)(
<Redirect to="/" />,
)}
</div>
);
export default LoginHome;
Here is loginPage:
import React from 'react';
import { form, FieldGroup, FormGroup, Checkbox, Button } from 'react-bootstrap';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { connect } from 'react-redux';
import { getLogin } from './login-actions';
import LoginForm from './form';
import logoSrc from '../../assets/images/logo/K.png';
import './login.scss'
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
passwordVisible: false,
}
this.handleSubmit= this.handleSubmit.bind(this);
this.togglePasswordVisibility= this.togglePasswordVisibility.bind(this);
}
togglePasswordVisibility() {
this.setState((prevState) => {
if (prevState.passwordVisible === false) {
return { passwordVisible: prevState.passwordVisible = true }
}
else if (prevState.passwordVisible === true) {
return { passwordVisible: prevState.passwordVisible = false }
}
})
}
handleSubmit(values) {
this.props.onSubmit(values)
}
render () {
return (
<div id="background">
<div className="form-wrapper">
<img
className="formLogo"
src={logoSrc}
alt="K"/>
<LoginForm onSubmit={this.handleSubmit} passwordVisible={this.state.passwordVisible}
togglePasswordVisibility={this.togglePasswordVisibility}/>
</div>
</div>
)
}
}
function mapStateToProps(state) {
return {
loginReducer: state.loginReducer,
};
}
function mapDispatchToProps(dispatch) {
return {
onSubmit: (data) => {
getLogin(data, dispatch);
},
};
}
export default connect (mapStateToProps, mapDispatchToProps)(LoginPage);
Let me know if anything else is needed
Use the withRouter HoC from React Router to provide { match, history, location } to your component as follows;
import { withRouter } from "react-router";
...
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginPage));
Modify getLogin to accept history as a third argument; export function getLogin(data, dispatch, history) { ... }
Now in the then method of your async action you can use history.push() or history.replace() to change your current location. <Redirect /> uses history.replace() internally.
You can read about the history module here. This is what is used internally by React Router.
EDIT: In response to your comment...
With your current setup you will need to pass history into getLogin through your mapDispatchToProps provided onSubmit prop.
function mapDispatchToProps(dispatch) {
return {
onSubmit: (data, history) => {
getLogin(data, dispatch, history);
},
};
}
Your handleSubmit method will also need updated.
handleSubmit(values) {
this.props.onSubmit(values, this.props.history)
}
Modified answer depending on how Router was set up I had to use hashHistory: react-router " Cannot read property 'push' of undefined"