I'm giving the react context api a look, I decided to try setting it up to handle a login popup window:
import React, { Component } from 'react';
export const LoginContext = React.createContext();
import firebase from 'firebase';
export class LoginProvider extends Component {
state = {
loginOpen: false,
user: Object
}
render() {
return <LoginContext.Provider value={{
state: this.state,
showLogin: () => this.setState({
loginOpen: true
}),
closeLogin: () => this.setState({
loginOpen: false
}),
login: async (username, password) => {
var auth = firebase.auth();
try {
var that = this;
auth.signInWithEmailAndPassword(username, password).then((user)=>{
that.setState({user: user, loginOpen: false});
});
}
catch (E) {
return E;
}
}
}}>
{this.props.children}
</LoginContext.Provider>;
}
}
Used within a React component with the relevant functions being called.
It occurred to me that LoginProvider is a view model.
Which makes me wonder: Am I using the context feature correctly? Was this its intended usage?
If so why is the usage so archaic? What benefits does it provide? I mean it's less code than going through redux or sagas but still unnecessarily cumbersome.
Related
I have a simple login component and a MobX store which is supposed to hold some user information. I want to test this integration with Jest. The app is created with Create React App, so I am just building off that for my tests.
My Login component looks like this:
const Login = () => {
const { accountStore, userStore } = useStores();
const { user } = userStore;
return(
<>
{user ?
<>
<h1>{user.fullName}</h1>
<h2>{user.username}</h2>
</>
:
<main className="form-signin">
<Button onClick={accountStore.RedirectToFeide} aria-label='sign-in-with-feide-button'>Sign in with Feide</Button>
<Button onClick={userStore.GetCurrentUser} aria-label='get-info-button'>Get Current User Info</Button>
</main>
}
</>
)
}
export default observer(Login);
My RootStore looks like this and provides the useStores hook:
export class RootStore {
public userStore: UserStore;
public accountStore: AccountStore;
constructor() {
this.userStore = new UserStore(this);
this.accountStore = new AccountStore(this);
}
}
const StoresContext = createContext(new RootStore());
export const useStores = () => useContext(StoresContext);
If the user in the store is present I would like to see their information. If not I would like to see the two buttons.
I have mocked the UserStore in a folder called __mocks__ like this:
class UserStore {
private rootStore: RootStore;
public user?: User;
constructor (rootStore: RootStore) {
makeAutoObservable(this)
this.rootStore = rootStore;
}
#action
GetCurrentUser = async () => {
this.user = {
username: 'username',
fullName: 'Test User',
dateOfBirth: new Date('1994-03-15'),
email: 'test#example.com'
}
}
}
export default UserStore;
When I use jest.mock() in my test the GetCurrentUser method is mocked correctly, and the mocked data is set in the user object. However, I have not been able to reset the user object before doing a new test in any way.
I have tried:
Mocking in beforeEach/afterEach
jest.clearAllMocks(), jest.resetAllMocks() and jest.restoreAllMocks() both within beforeEach and without
Adding and removing describes to isolate tests
The test looks like this:
import React from 'react';
import { fireEvent, render } from '#testing-library/react';
import Login from './Login';
jest.mock('../../stores/UserStore')
beforeEach(() => {
jest.clearAllMocks()
})
describe('User is logged in', () => {
test('Login shows users name after getting current user information', async () => {
const { getByText } = render(<Login />);
fireEvent.click(getByText('Get Current User Info'));
expect(getByText('Test User')).toBeInTheDocument()
})
})
describe('User not logged in', () => {
test('Login should show sign in button', () => {
const { getByText } = render(<Login />)
expect(getByText('Sign in')).toBeInTheDocument();
})
})
Since the first test presses the button and sets the user observable the second test fails. How can I reset the entire mocked store between each test?
I had similar issue because I was using React Context without Provider.
After wrapping the main app with provider like:
const StoreContext = createContext<RootStore | undefined>(undefined);
export function RootStoreProvider({
children,
rootStore,
}: {
children: ReactNode;
rootStore: RootStore;
}) {
return (
<StoreContext.Provider value={root}>{children}</StoreContext.Provider>
);
}
export const useStores = () => useContext(StoresContext);
I also modified render function from #testing-library/react to accept more arguments:
type CustomRenderTypes = RenderOptions & {rootStore?: RootStore;};
function customRender(
ui: ReactElement,
{rootStore, ...options}: CustomRenderTypes = {},
): RenderResult {
const store = rootStore || new RootStore();
return render(ui, {
wrapper: ({ children }) => {
return (
<RootStoreProvider rootStore={store}>
{children}
</RootStoreProvider>
);
},
...options,
});
}
And after that every test case was getting the fresh copy of store and there was no need to reset mocks
Try to include import of the hook you are mocking in your test-file.
OR
You can try to mock it another way: include import of useStores hook in the test. Then you call jest.mock('useStores'). And then you should mock its implementation.
import useStores from '../../stores/UserStore')
jest.mock('useStores')
useStores.mockImplementation(() => someDummyData)
Described solution you can find in docs.
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?
I have a React UI kit and want to get some functionality to it. I also have some functionality without the UI. Both are working separately, but I cannot manage to work together due to the error
TypeError: Cannot destructure property 'authenticate' of
'Object(...)(...)' as it is undefined.
I have an account object which is the context provider (Accounts.js, shortened for brevity):
import React, { createContext } from 'react'
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js'
import Pool from 'UserPool'
const AccountContext = createContext()
const Account = (props) => {
const getSession = async () =>
await new Promise((resolve, reject) => {
...
})
const authenticate = async (Email, Password) =>
await new Promise((resolve, reject) => {
...
})
const logout = () => {
const user = Pool.getCurrentUser()
if (user) {
user.signOut()
}
}
return (
<AccountContext.Provider
value={{
authenticate,
getSession,
logout
}}
>
{props.children}
</AccountContext.Provider>
)
}
export { Account, AccountContext }
And I have SignIn.js Component which throws the error (also shortened):
import React, { useState, useEffect, useContext } from 'react';
import { Link as RouterLink, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import validate from 'validate.js';
import { AccountContext } from 'Accounts.js';
const SignIn = props => {
const { history } = props;
const [status, setStatus] = useState(false);
const { authenticate, getSession } = useContext(AccountContext);
const classes = useStyles();
const [formState, setFormState] = useState({
isValid: false,
values: {},
touched: {},
errors: {}
});
useEffect(() => {
const errors = validate(formState.values, schema);
setFormState(formState => ({
...formState,
isValid: errors ? false : true,
errors: errors || {}
}));
getSession()
.then(session => {
console.log('Session:', session);
setStatus(true);
});
}, [formState.values]);
const handleSignIn = event => {
event.preventDefault();
authenticate(formState.values.email, formState.values.password)
.then(data => {
console.log('Logged in!', data);
//setStatus(true);
})
.catch(err => {
console.error('Failed to login!', err);
//setStatus(false);
})
history.push('/');
};
return (
<div className={classes.root}>
</div>
);
};
SignIn.propTypes = {
history: PropTypes.object
};
export default withRouter(SignIn);
I guess something is wrong with the Accounts.js because the SignIn.js cannot use the authenticate or getSession functions. I need those in the context because other components will render differently when a user is signed in and getSession exactly retrieves this info. Accounts.js is calling against AWS Cognito. I understand how to use variables or states in context but functions seem to work differently. How do I define the functions in Accounts.js to add them to the context so that I can use them in other components as well?
I have tried similar approach in my application.
As per your code, everything is looking fine. The error you have mentioned can be because of wrapping SignIn component wrongly in Provider i.e Account.
Try wrapping SignIn Component inside Account Provider like below:
Import {Account} from './Accounts.js' // Path of Account.js file
<Account> // Account act as Provider as per your code
<SignIn />
...
</Account>
Rest of your code seems fine.
Technologies: I'm using Firebase Auth with NextJS & React.
Problematic: Logged users can use the web app with firebase auth normally only if they navigate within the app via Next routing, whenever they refresh the page or open a new account tab they are not logged in anymore.
Issue: It is extremely frustrating because this problem only occurs on production. There's no problem at all on the staging & localhost environment.
firebase.js: Initialize firebase.
import getConfig from "next/config";
import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/analytics';
const { publicRuntimeConfig } = getConfig();
export async function initializeFirebase() {
if (!firebase.apps.length) {
firebase.initializeApp(JSON.parse(publicRuntimeConfig.FIREBASE_CONFIG));
if (publicRuntimeConfig.FIREBASE_ANALYTICS) {
firebase.analytics();
}
}
}
export const auth = firebase.auth
export const db = firebase.firestore;
export default firebase;
AuthHoC.js: To make sure the user is connected I wrapper my pages with a HOC.
export default App => (
class AuthHoC extends App {
_isMounted = false;
constructor(props) {
super(props)
this.state = {
loading: false,
isVerified: false,
idToken: undefined,
isAuthenticated: false
}
}
async componentDidMount() {
this._isMounted = true;
await initializeFirebase();
// onAuthStateChanged return a function that we'll use to unsubscribe our listener
this.unsubscribeMethod = await auth().onAuthStateChanged(this._handleStateChange);
}
// is user is null, we're no longer authenticated
_handleStateChange = (user) => {
let that = this;
if (user) {
// NOT PASSING HERE ON PAGE REFRESH...
user.getIdToken().then(function(idToken) {
that.setState({
loading: true,
idToken: idToken,
isVerified: user.emailVerified,
isAuthenticated: !!user
});
});
} else {
...
}
}
componentWillUnmount() {
if (this.unsubscribeMethod) {
this.unsubscribeMethod();
}
this._isMounted = false;
}
render() {
return ( <>
{this.state.loading ?
<App {...this.props} {...this.state} />
:
... loading ...
}
</> )
}
});
_app.js: (NextJS) Wrap every pages with the Higher Order Component.
import App from "next/app";
import AuthHoC from '../utils/authentication/authHoC';
class MyApp extends App {
render() {
const { Component, pageProps, isAuthenticated, idToken, isVerified } = this.props;
return (
<Component
{...pageProps}
isAuth={isAuthenticated}
idToken={idToken}
isVerified={isVerified}
/>
);
}
}
export default AuthHoC(MyApp);
What could be the issue? All these codes work on localhost & staging url, just not on production.
EDIT:
I pinpointed the problem, I just switched my production keys with staging and it works, that means that the problem is not coming from the Heroku or my code but my Firebase configuration itself.
The main difference is that the prod use analytics. If you have any info I forgot to configure any suggestion would help.
I try to redirect the user from one component to another after a few seconds.
The user lands on a page and after few second he is automatically redirect to another page.
I thought to redirect in an action but I am not sure if it is the best idea (if you have easier way to do it I am interested).
My code so far:
a basic component:
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { redirectToProfile } from "../../actions/searchActions";
class Search extends Component {
componentDidMount() {
setTimeout(this.props.redirectToProfile(this.props.history), 3000);
}
render() {
return (
<div>
<h1>search page</h1>
</div>
);
}
}
export default connect(
null,
{ redirectToProfile }
)(withRouter(Search));
and the action:
export const redirectToProfile = history => {
history.push("/");
};
So far I have an error message:
Actions must be plain objects. Use custom middleware for async actions.
After some research I see that some people resolve the problem with the middleware thunk but I am already using it so I don't know what to do.
Thank you for your help.
Why not use the <Redirect/> component that react-router provides? I think that's clearer and more in keeping with React's declarative model, rather than hiding away the logic in an imperative thunk/action.
class Foo extends Component {
state = {
redirect: false
}
componentDidMount() {
this.id = setTimeout(() => this.setState({ redirect: true }), 1000)
}
componentWillUnmount() {
clearTimeout(this.id)
}
render() {
return this.state.redirect
? <Redirect to="/bar" />
: <div>Content</div>
}
}
state = {
redirect: false // add a redirect flag
};
componentDidMount() {
// only change the redirect flag after 5 seconds if user is not logged in
if (!auth) {
this.timeout = setTimeout(() => this.setState({ redirect: true }), 5000);
}
}
componentWillUnmount() {
// clear the timeer just in case
clearTimeout(this.timeout);
}
render() {
// this is the key:
// 1. when this is first invoked, redirect flag isn't set to true until 5 seconds later
// 2. so it will step into the first else block
// 3. display content based on auth status, NOT based on redirect flag
// 4. 5 seconds later, redirect flag is set to true, this is invoked again
// 5. this time, it will get into the redirect block to go to the sign in page
if (this.state.redirect) {
return <Redirect to="/signin" />;
} else {
if (!auth) {
return (
<div className="center">
<h5>You need to login first to register a course</h5>
</div>
);
} else {
return <div>Registration Page</div>;
}
}
}
If you are already using redux thunk and it's included in your project, you can create the action as following.
export const redirectToProfile = history => {
return (dispatch, setState) => {
history.push('/');
}
};
// shorter like this.
export const redirectToProfile = history => () => {
history.push('/');
}
// and even shorter...
export const redirectToProfile = history => () => history.push('/');
Alternative:
You can also call history.push('/'); right in the component if you adjust you default export of the Search component. This is preferred as you don't have overhead of creating an additional action and dispatching it through redux.
Change your export to...
export default withRouter(connect(mapStateToProps)(Search));
Then in your component use it as following...
componentDidMount() {
setTimeout(this.props.history.push('/'), 3000);
}
//This is an example with functional commponent.
import React, {useEffect, useState} from 'react'
import { useNavigate } from "react-router-dom";
function Splash() {
let navigate = useNavigate();
const [time, setTime] = useState(false);
useEffect(() => {
setTimeout(() => {
setTime(true)
}, 2000)
setTime(false);
}, []);
return time ? navigate('/dashboard') : navigate('/account');
}
export default Splash;