How to keep React components state after browser refresh - reactjs

Thank you reading my first question.
I trying to auth With Shared Root use react, react-router and firebase.
So, I want to keep App.js 's user state. but when I tried to refresh the browser, user state was not found.
I've tried to save to localstorage. But is there a way to keep state on component after browser refresh without localStorage?
App.js
import React, { Component, PropTypes } from 'react'
import Rebase from 're-base'
import auth from './config/auth'
const base = Rebase.createClass('https://myapp.firebaseio.com')
export default class App extends Component {
constructor (props) {
super(props)
this.state = {
loggedIn: auth.loggedIn(),
user: {}
}
}
_updateAuth (loggedIn, user) {
this.setState({
loggedIn: !!loggedIn,
user: user
})
}
componentWillMount () {
auth.onChange = this._updateAuth.bind(this)
auth.login() // save localStorage
}
render () {
return (
<div>
{ this.props.children &&
React.cloneElement(this.props.children, {
user: this.state.user
})
}
</div>
)
}
}
App.propTypes = {
children: PropTypes.any
}
auth.js
import Rebase from 're-base'
const base = Rebase.createClass('https://myapp.firebaseio.com')
export default {
loggedIn () {
return !!base.getAuth()
},
login (providers, cb) {
if (Boolean(base.getAuth())) {
this.onChange(true, this.getUser())
return
}
// I think this is weird...
if (!providers) {
return
}
base.authWithOAuthPopup(providers, (err, authData) => {
if (err) {
console.log('Login Failed!', err)
} else {
console.log('Authenticated successfully with payload: ', authData)
localStorage.setItem('user', JSON.stringify({
name: base.getAuth()[providers].displayName,
icon: base.getAuth()[providers].profileImageURL
}))
this.onChange(true, this.getUser())
if (cb) { cb() }
}
})
},
logout (cb) {
base.unauth()
localStorage.clear()
this.onChange(false, null)
if (cb) { cb() }
},
onChange () {},
getUser: function () { return JSON.parse(localStorage.getItem('user')) }
}
Login.js
import React, { Component } from 'react'
import auth from './config/auth.js'
export default class Login extends Component {
constructor (props, context) {
super(props)
}
_login (authType) {
auth.login(authType, data => {
this.context.router.replace('/authenticated')
})
}
render () {
return (
<div className='login'>
<button onClick={this._login.bind(this, 'twitter')}>Login with Twitter account</button>
<button onClick={this._login.bind(this, 'facebook')}>Login with Facebook account</button>
</div>
)
}
}
Login.contextTypes = {
router: React.PropTypes.object.isRequired
}

If you reload the page through a browser refresh, your component tree and state will reset to initial state.
To restore a previous state after a page reload in browser, you have to
save state locally (localstorage/IndexedDB)
and/ or at server side to reload.
And build your page in such a way that on initialisation, a check is made for previously saved local state and/or server state, and if found, the previous state will be restored.

Related

App not re-rendering on history.push when run with jest

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.

Redirect automatically from one component to another one after few seconds - react

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;

ComponentWIllReceiveProps not getting called unless page is refreshed

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.

React doesn't update the view even when Redux state is changed

The problem is when I update state in Redux, React doesn't run the render function. I am a beginner in Redux so I am not getting what exactly should I be doing to solve this. I read about the #connect function but as I am using CreateReactApp CLI tool, I won't be able to provide support for Decorators without ejecting (Which I dont want to do).
Component:
import React from "react";
import Store from "../store";
Store.subscribe(() => {
console.log(Store.getState().Auth);
});
export default class Login extends React.Component {
login = () => {
Store.dispatch({ type: "AUTH_LOGIN" });
// this.forceUpdate(); If I forceUpdate the view, then it works fine
};
logout = () => {
Store.dispatch({ type: "AUTH_LOGOUT" });
// this.forceUpdate(); If I forceUpdate the view, then it works fine
};
render() {
if (Store.getState().Auth.isLoggedIn) {
return <button onClick={this.logout}>Logout</button>;
} else {
return <button onClick={this.login}>Login</button>;
}
}
}
Reducer:
export default AuthReducer = (
state = {
isLoggedIn: false
},
action
) => {
switch (action.type) {
case "AUTH_LOGIN": {
return { ...state, isLoggedIn: true };
}
case "AUTH_LOGOUT": {
return { ...state, isLoggedIn: false };
}
}
return state;
};
Can anyone please point me in the right direction? Thanks
You can make use of connect HOC instead of decorator, it would be implemented like
import { Provider, connect } from 'react-redux';
import Store from "../store";
class App extends React.Component {
render() {
<Provider store={store}>
{/* Your routes here */}
</Provider>
}
}
import React from "react";
//action creator
const authLogin = () => {
return { type: "AUTH_LOGIN" }
}
const authLogout = () => {
return { type: "AUTH_LOGOUT" }
}
class Login extends React.Component {
login = () => {
this.props.authLogin();
};
logout = () => {
this.props.authLogout();
};
render() {
if (this.props.Auth.isLoggedIn) {
return <button onClick={this.logout}>Logout</button>;
} else {
return <button onClick={this.login}>Login</button>;
}
}
}
const mapStateToProps(state) {
return {
Auth: state.Auth
}
}
export default connect(mapStateToProps, {authLogin, authLogout})(Login);

React Native & Redux : how and where to use componentWillMount()

I work on app with facebook login using react-native and redux. Right now I'm face to an issue :
Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
So I think I have to use componentWillMount() just before my render method, but I don't know how to use it ..
containers/Login/index.js
import React, { Component } from 'react';
import { View, Text, ActivityIndicatorIOS } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actionCreators from '../../actions';
import LoginButton from '../../components/Login';
import reducers from '../../reducers';
import { Card, CardSection, Button } from '../../components/common';
class Login extends Component {
// how sould I use it ?
componentWillMount() {
}
render() {
console.log(this.props.auth);
const { actions, auth } = this.props;
var loginComponent = <LoginButton onLoginPressed={() => actions.login()} />;
if(auth.error) {
console.log("erreur");
loginComponent = <View><LoginButton onLoginPressed={() => actions.login()} /><Text>{auth.error}</Text></View>;
}
if (auth.loading) {
console.log("loading");
loginComponent = <Text> LOL </Text>;
}
return(
<View>
<Card>
<CardSection>
{ auth.loggedIn ? this.props.navigation.navigate('Home') : loginComponent }
</CardSection>
</Card>
</View>
);
}
}
function mapStateToProps(state) {
return {
auth: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actionCreators, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
the reducer :
import { LOADING, ERROR, LOGIN, LOGOUT } from '../actions/types';
function loginReducer(state = {loading: false, loggedIn: false, error: null}, action) {
console.log(action);
switch(action.type) {
case LOADING:
console.log('Inside the LOADING case');
return Object.assign({}, state, {
loading: true
});
case LOGIN:
return Object.assign({}, state, {
loading: false,
loggedIn: true,
error: null,
});
case LOGOUT:
return Object.assign({}, state, {
loading: false,
loggedIn: false,
error: null
});
case ERROR:
return Object.assign({}, state, {
loading: false,
loggedIn: false,
error: action.err
});
default:
return state;
}
}
export default loginReducer;
and the action :
import {
LOADING,
ERROR,
LOGIN,
LOGOUT,
ADD_USER
} from './types';
import { facebookLogin, facebookLogout } from '../src/facebook';
export function attempt() {
return {
type: LOADING
};
}
export function errors(err) {
return {
type: ERROR,
err
};
}
export function loggedin() {
return {
type: LOGIN
};
}
export function loggedout() {
return {
type: LOGOUT
};
}
export function addUser(id, name, profileURL, profileWidth, profileHeight) {
return {
type: ADD_USER,
id,
name,
profileURL,
profileWidth,
profileHeight
};
}
export function login() {
return dispatch => {
console.log('Before attempt');
dispatch(attempt());
facebookLogin().then((result) => {
console.log('Facebook login success');
dispatch(loggedin());
dispatch(addUser(result.id, result.name, result.picture.data.url, result.picture.data.width, result.data.height));
}).catch((err) => {
dispatch(errors(err));
});
};
}
export function logout() {
return dispatch => {
dispatch(attempt());
facebookLogout().then(() => {
dispatch(loggedout());
})
}
}
If you need more code here is my repo :
https://github.com/antoninvroom/test_redux
componentWillMount is one the first function to be run when creating a component. getDefaultProps is run first, then getInitialState then componentWillMount. Both getDefaultProps and getInitialState will be run only if you create the component with the react.createClass method. If the component is a class extending React.Component, those methods won't be run. It is recommended to use componentDidMount if you can instead of componentWillMount because your component can still be updated before componentWillMount and the first render.
You can find more info on the react component lifecycle here
Also, it is recommended to set the state or the default props inside the class constructor or using getDefaultProps and getInitialState.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { bar: 'foo' };
}
static defaultProps = {
foo: 'bar'
};
}
EDIT: Here's the component handling login
import React, { Component } from 'react';
import { View, Text, ActivityIndicatorIOS } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actionCreators from '../../actions';
import LoginButton from '../../components/Login';
import reducers from '../../reducers';
import { Card, CardSection, Button } from '../../components/common';
class Login extends Component {
componentDidMount() {
// If user is already logged in
if(this.props.auth.loggedIn) {
// redirect user here
}
}
componentWillReceiveProps(nextProps) {
// If the user just log in
if(!this.props.auth.loggedIn && nextProps.auth.loggedIn) {
// Redirect user here
}
}
render() {
console.log(this.props.auth);
const { actions, auth } = this.props;
var loginComponent = <LoginButton onLoginPressed={() => actions.login()} />;
if(auth.error) {
console.log("erreur");
loginComponent = <View><LoginButton onLoginPressed={() => actions.login()} /><Text>{auth.error}</Text></View>;
}
if (auth.loading) {
console.log("loading");
loginComponent = <Text> LOL </Text>;
}
return(
<View>
<Card>
<CardSection>
{ auth.loggedIn ? this.props.navigation.navigate('Home') : loginComponent }
</CardSection>
</Card>
</View>
);
}
}
function mapStateToProps(state) {
return {
auth: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actionCreators, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
Based on your comment to Ajay's answer, you are looking to set the initial state in the component. To do so, you would set the state inside the constructor function.
class Login extends Component {
constructor(props) {
super(props);
this.state = {
color: props.initialColor
};
}
If you have data that is fetched asynchronously that is to be placed in the component state, you can use componentWillReceiveProps.
componentWillReceiveProps(nextProps) {
if (this.props.auth !== nextProps.auth) {
// Do something if the new auth object does not match the old auth object
this.setState({foo: nextProps.auth.bar});
}
}
componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method.
if you need more info componentWillMount()
read this https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/birth/premounting_with_componentwillmount.html

Resources