I'm using redux in my react project. I set initial state in my reducer
const initialState = {
isWarning: false,
};
and I have default props in my react component
LoginView.defaultProps = {
warning: false,
};
and of course I'm destructuring my props in render method
const { warning } = this.props;
here you have code from my component
import React, { Component } from 'react';
import styled from 'styled-components';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import axios from '../../axios';
import Input from '../Input/Input';
import LoggedIn from '../LoggedIn/LoggedIn';
import * as actions from '../../store/actions';
const StyledWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
const StyledTitle = styled.p`
color: #727272;
margin: 89px 0 73px 0;
font-size: 24px;
`;
const StyledButton = styled.button`
border: none;
border-radius: 6px;
background: #1de278;
height: 48px;
width: 397px;
color: #fff;
font-size: 18px;
cursor: pointer;
&:active,
&:focus {
outline: none;
}
`;
class LoginView extends Component {
state = {
email: '',
password: '',
};
onType = event => {
this.setState({ [event.target.id]: event.target.value });
};
onSubmit = (email, password) => {
axios
.post('api/v1/session', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
email,
password,
})
// eslint-disable-next-line react/destructuring-assignment
.then(() => this.props.history.push('/'))
.catch(() => this.props.onLoginError(true));
};
render() {
const { email, password } = this.state;
const { warning } = this.props;
return (
<StyledWrapper>
{console.log(warning)}
<StyledTitle>Log in</StyledTitle>
<Input id="email" placeholderText="Email address"
setInputValue={this.onType} />
<Input
id="password"
placeholderText="Password"
setInputValue={this.onType}
type="password"
/>
<LoggedIn />
<StyledButton onClick={() => this.onSubmit(email,
password)}>Login</StyledButton>
</StyledWrapper>
);
}
}
const mapStateToProps = state => {
return { isWarning: state.login.isWarning };
};
const mapDispatchToProps = dispatch => {
return {
onLoginError: state => dispatch(actions.setErrorLogin(state)),
};
};
LoginView.propTypes = {
warning: PropTypes.bool,
};
LoginView.defaultProps = {
warning: false,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LoginView);
Redux dev tools shows my updated state from false to true, but my warning have false value. In my opinion it's something with default props. Do you have any ideas what could be a problem?
The problem is here:
const mapStateToProps = state => {
return { isWarning: state.login.isWarning };
};
it's supposed to be
const mapStateToProps = state => {
return { warning: state.login.isWarning };
};
Because the component can't find a warning prop it sets it to the default value, so you have to give it the same name.
Related
I am new to Reactjs and building a sample e commerce application in Reactjs. I am using single context API to share multiple filter values across application. When I update values from any of the filter components, all the filter components get re rendered again. Is there any way to restrict this behavior as it causes performance issues. Sharing the sample code link below.
stackblitz
App component
import logo from './logo.svg';
import React, { useState } from 'react';
import './App.css';
import Categories from './components/Categories/Categories';
import Filters from './components/Filters/Filters';
import Products from './components/Products/Products';
import styled from 'styled-components';
import { FilterContext } from './components/Contexts/FilterContext';
const MainContainer = styled.div`
display: flex;
padding: 20px;
`
function App() {
console.log('App component');
const [rangeval, setRangeval] = useState(null);
const [colorCodes, setcolorCodes] = useState([]);
return (
<div className="App">
<Categories/>
<FilterContext.Provider value={React.useMemo(()=>({slider: {rangeval, setRangeval}, color: {colorCodes, setcolorCodes} }), rangeval, setRangeval, colorCodes, setcolorCodes)} >
<MainContainer>
<Filters/>
<Products />
</MainContainer>
</FilterContext.Provider>
</div>
);
}
export default React.memo(App);
Below are my filter components.
Color component
import axios from 'axios';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { FilterContext } from '../../Contexts/FilterContext';
const ColorWrapper = styled.div`
display: flex;
flex-direction: column;
padding: 20px 10px 20px 10px;
border-bottom: 1px solid grey;
`;
function Color() {
console.log('Color Component');
const { color } = useContext(FilterContext);
const [colors, setColors] = useState([]);
const colorCodes = [];
const handleCheckboxState = (data) => {
if (data.isChecked) {
colorCodes = colorCodes.filter((item) => item != data.id);
} else {
colorCodes.push(data.id);
}
colors.find((item) => item.id === data.id).isChecked = !data.isChecked;
setColors(colors);
color.setcolorCodes(colorCodes);
};
useEffect(() => {
axios
.get('https://run.mocky.io/v3/fdbe3884-b824-466c-8da4-0d0ecad17e7c')
.then((response) => {
setColors(response.data);
})
.catch((error) => {})
.finally();
}, []);
return (
<ColorWrapper>
<div>Color</div>
{colors.map((color) => (
<label key={color.id}>
<input
type="checkbox"
checked={color.isChecked}
name="color"
value={color.id}
onChange={() => handleCheckboxState(color)}
/>
{color.name}
</label>
))}
</ColorWrapper>
);
}
export default React.memo(Color);
Slider component
import React, { useContext, useState } from 'react';
import { FilterContext } from '../../Contexts/FilterContext';
import styled from 'styled-components';
const SliderWrapper = styled.div`
display: flex;
flex-direction: column;
padding: 20px 10px 20px 10px;
border-bottom: 1px solid grey;
`;
const SliderInput = styled.input`
width: 100%;
`;
const SliderRange = styled.div`
margin: 20px 0px 20px 0px;
`;
function Slider() {
console.log('Slider component');
const { slider } = useContext(FilterContext);
const [min, setMin] = useState(10);
const [max, setMax] = useState(10000);
return (
<SliderWrapper>
<SliderRange>
Price Range 0 - {slider.rangeval ? slider.rangeval : min}
</SliderRange>
<SliderInput
type="range"
className="range"
min={min}
max={max}
onChange={(event) => slider.setRangeval(event.target.value)}
/>
</SliderWrapper>
);
}
export default React.memo(Slider);
I'm currently working on my first vue application, currently building the login logics.
For State management, pinia is being used. I created a Pinia Store to manage the "isLoggedIn" state globally.
import { defineStore } from "pinia";
export const useLoginStatusStore = defineStore('loginStatus', {
id: 'loginStatus',
state: () => ({
isLoggedIn: false
}),
actions: {
logIn() {
this.isLoggedIn = true
console.log("Login", this.isLoggedIn)
},
logOut() {
this.isLoggedIn = false
console.log("Logout", this.isLoggedIn)
}
}
})
So far so good, its working, i can access the state and actions in the components and router file.
**<roouter.js>**
import { createRouter, createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import { createApp, ref } from 'vue'
import { useLoginStatusStore } from '../stores/loginStatus.js'
import App from '../App.vue'
import WelcomeView from '../views/public/WelcomeView.vue'
import SplashView from '../views/public/SplashView.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
const loginStatusStore = useLoginStatusStore()
let isLoggedIn = ref(loginStatusStore.isLoggedIn)
console.log("isLoggedIn", loginStatusStore.isLoggedIn)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'splash',
component: SplashView
},
{
path: '/welcome',
name: 'welcome',
component: WelcomeView
},
{
path: '/login',
name: 'login',
component: () => import('../views/public/LoginView.vue')
},
{
path: '/signup',
name: 'signup',
component: () => import('../views/public/SignUpView.vue')
},
{
path: '/resetpassword',
name: 'resetpassword',
component: () => import('../views/public/ForgotPasswordView.vue')
},
{
path: '/home',
name: 'home',
component: () => import('../views/protected/HomeView.vue'),
meta: { requiresAuth: true }
},
{
path: '/sounds',
name: 'sounds',
component: () => import('../views/protected/SoundsView.vue'),
meta: { requiresAuth: true }
},
{
path: '/player',
name: 'soundPlayer',
component: () => import('../views/protected/SoundPlayerView.vue'),
meta: { requiresAuth: true }
},
{
path: '/profile',
name: 'profile',
component: () => import('../views/protected/ProfileView.vue'),
meta: { requiresAuth: true }
},
{
path: '/meditation',
name: 'meditation',
component: () => import('../views/protected/MeditationView.vue'),
meta: { requiresAuth: true }
},
{
path: '/tools',
name: 'tools',
component: () => import('../views/protected/ToolsView.vue'),
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
console.log("Router", isLoggedIn.value)
if (!isLoggedIn.value) {
next({
name: 'welcome'
})
} else {
next()
}
} else {
next()
}
})
export default router
In the router it's being used for protected routes and in App.vue for conditional class rendering.
The Problem is, that when the state gets updated, it doesn't get updated in the components and the components themselves don't update either. I tried with the $subscribe method in pinia, but didnt manage to get it working. I know, whats needed is something that creates reactivity here. But no clue how to do that. I'm grateful for any help with this :)
thanks for reading
**App.vue**
<script setup>
import { RouterView } from 'vue-router';
import DevNavItem from '#/components/header/DevNavItem.vue'
import HeaderItem from '#/components/header/HeaderItem.vue'
import FooterItem from '#/components/footer/FooterItem.vue'
import { useLoginStatusStore } from './stores/loginStatus.js';
const loginStatusStore = useLoginStatusStore()
const isLoggedIn = loginStatusStore.isLoggedIn
console.log("App.vue", loginStatusStore.isLoggedIn)
</script>
<template>
<DevNavItem />
<HeaderItem v-if="isLoggedIn" />
<RouterView :class="isLoggedIn ? 'mainProtected' : 'mainPublic'" />
<FooterItem v-if="isLoggedIn" />
</template>
<style>
/*FONT-IMPORT*/
#import url("#/assets/font/alegreya_font.scss");
/* GENERAL STYLES */
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
header {
position: top;
}
.mainProtected {
width: 100vw;
height: 83vh;
overflow: hidden;
}
.mainPublic {
width: 100vw;
height: 100vh;
overflow: hidden;
}
/* GLOBAL CLASSES */
.mainLogo {
height: 350px;
width: 350px;
background: url("./img/icons/main.png") center/cover no-repeat;
}
.leavesBackground {
background-color: #253334;
background-image: url("./src/img/images/background_partial.png");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
.logoSmall {
background: url("./img/icons/main.png") center/contain no-repeat;
height: 100px;
width: 100px;
}
.buttonPublic {
padding: 20px 0;
text-align: center;
background-color: #7c9a92;
color: #fff;
border-radius: 15px;
width: 90%;
text-decoration: none;
font-size: 24px;
border: none;
}
</style>
I tried subscribing to the state with $subscribe, but it didn't work.
storeToRefs()
You need to use storeToRefs() to extract properties from the store while keeping those properties reactive.
import { storeToRefs } from 'pinia'
const themeStore = useThemeStore();
const { isDark } = storeToRefs(themeStore);
Computed property
Thanks to #Fennec for suggesting the computed way of getting reactive state. Although I don't recommend this method since there is a dedicated storeToRefs() available.
import { computed } from 'vue'
const themeStore = useThemeStore();
const isDark = computed(() => themeStore.isDark);
WRONG ways to get reactive state from the Pinia store:
All the ways listed below of getting the state (properties, getters) from the Pinia store are WRONG:
import { useThemeStore } from "./stores/theme.js";
const themeStore = useThemeStore();
// WRONG ways of extracting state from store
let isDark = themeStore.isDark; // not reactive
let isDark = ref(themeStore.isDark); // reactive, but will not be updated with the store
let { isDark } = themeStore; // not reactive, cannot destructure
Destructuring actions directly from the store.
Its worth to note here that "you can destructure actions directly from the store as they are bound to the store itself." (docs)
If you have an action named "increment" in your store, you can just extract it directly from the store in your component:
...
const { increment } = store // actions can be destructured directly
...
Also, according to Pinia docs, the first argument is the unique ID, so you do not need to specify the id again inside the options object. Or you can just ignore the first argument and just specify the id as an option. Either way is fine.
Please tell me why, when I call this.props.getOnEvents(), an error occurs that “getOnEvents() is not a function”, what’s wrong here and how to fix it?
I’m looking at the console.log and there is this function in the props, but when I try to call, an error flies out that it’s not a function
EventCalendar.js
import React, { Component } from 'react';
import { withStyles } from '#material-ui/core/styles';
import EventCalendarTable from './EventCalendarTable';
import EventCalendarView from './EventCalendarView';
const styles = theme => ({
root: {
width: '80%',
margin: '20px auto 0'
},
});
class EventCalendar extends Component {
constructor(props) {
super(props);
this.state = {
viewEvents: 'table'
};
}
componentDidMount() {
this.props.onGetEvents();
}
changeEventsView = (value) => {
this.setState({ viewEvents: value });
}
render() {
console.log(this.props);
const { classes } = this.props;
return (
<div className={classes.root}>
<EventCalendarView changeEventsView={this.changeEventsView}/>
{
this.state.viewEvents === 'table'
? <EventCalendarTable />
: <div>test</div>
}
</div>
);
}
}
export default withStyles(styles)(EventCalendar);
EventPage/component.jsx
import React from 'react';
import EventCalendar from '../../components/EventCalendar';
import './index.scss';
function Events(props) {
return (
<React.Fragment>
<EventCalendar props={props}/>
</React.Fragment>
);
}
export default Events;
EventPage/container.js
import { connect } from 'react-redux';
import EventsPage from './component';
import { getEvents } from '../../../store/modules/Events/operations';
const mapStateToProps = ({ Events }) => ({
Events
});
const mapDispatchToProps = dispatch => ({
onGetEvents: () => {
console.log(123);
dispatch(getEvents());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(EventsPage);
Events/actions.js
import * as types from './types';
export const eventsFetch = value => ({
type: types.FETCHING_EVENTS,
payload: value
});
export const setEvents = ({ objById, arrayIds }) => ({
type: types.SET_EVENTS,
payload: {
eventById: objById,
eventsOrder: arrayIds
}
});
Events/types.js
export const FETCHING_EVENTS = 'Events/FETCHING_EVENTS';
export const SET_EVENTS = 'Events/SET_EVENTS';
Events/operation.js
import FetchClient from 'app/utils/FetchClient';
import IdsAndByIds from 'app/utils/IdsAndByIds';
import { eventsFetch, setEvents } from './actions';
export const getEvents = () => async (dispatch) => {
try {
const { data } = await FetchClient.get('/events');
dispatch(setEvents(IdsAndByIds(data)));
dispatch(eventsFetch(false));
} catch (error) {
console.log(error);
}
};
Events/reducer.js
import { createReducer } from 'store/utils';
import * as types from './types';
const usersInitState = {
fetching: true,
events: {
eventById: null,
usersOrder: null
},
error: null
};
const eventsReducer = createReducer(usersInitState)({
[types.FETCHING_EVENTS]: (state, { payload }) => ({
...state,
fetching: payload
}),
[types.SET_EVENTS]: (state, { payload }) => ({
...state,
events: {
...payload
}
})
});
export default eventsReducer;
Events/index.js
import { combineReducers } from 'redux';
import * as eventsListOperations from './operations';
import reducer from './reducers';
const EventsReducer = combineReducers({
eventsList: reducer
});
export default EventsReducer;
export { eventsListOperations };
The issue here is a minor one, Since you are connecting Events component to connect, you are receiveing the prop onGetEvents in that component, Now inside this component you are passing the props by a name props to the EventCalendar component
<EventCalendar props={props}/>
Now the props in EventCalender will contain a key called as props which wil lhave your data but you are trying to access it directly on props which is why it is undefined.
The correct way to pass the props here would be to use spread syntax like
<EventCalendar {...props}/>
I feel like I'm missing something simple here, but for whatever reason this simple ErrorMessageAlert won't dispatch an action. I've dispatched many times and I can't seem to figure out this error.
Can anyone lend a second pair of eyes?
Here's my Component
import { connect } from 'react-redux';
import styled from 'react-emotion';
import { resetErrorMessage } from 'users/ducks'
const ErrorMessage = styled.div`
width: 100%;
color: red;
position: fixed;
background: #F9DADA;
text-align: center;
border-bottom: 1px solid red;
padding-top: 5px;
padding-bottom: 5px;
z-index: 1;
`
export class ErrorMessageAlert extends Component<props> {
state = { isHidden: true }
componentDidMount() {
console.log("triggered")
// debugger
const { resetErrorMessageAction } = this.props
resetErrorMessageAction()
}
render(){
const {
errorMessage,
} = this.props
console.log(this.state.isHidden)
return (
<div>
{
this.state.isHidden && <ErrorMessage>{errorMessage}</ErrorMessage>
}
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
resetErrorMessageAction: () => dispatch(resetErrorMessage()),
})
export default connect(null, mapDispatchToProps)(ErrorMessageAlert);
in users/ducks
export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE';
export const resetErrorMessage = () => ({
type: RESET_ERROR_MESSAGE,
})
Here's the main error message stack trace:
ErrorMessageAlert.js:29 Uncaught TypeError: resetErrorMessageAction is not a function
at ErrorMessageAlert.componentDidMount (ErrorMessageAlert.js:29)
at ErrorMessageAlert.componentDidMount (react-hot-loader.development.js:654)
at commitLifeCycles (react-dom.development.js:17334)
at commitAllLifeCycles (react-dom.development.js:18736)
at HTMLUnknownElement.callCallback (react-dom.development.js:149)
at Object.invokeGuardedCallbackDev (react-dom.development.js:199)
at invokeGuardedCallback (react-dom.development.js:256)
at commitRoot (react-dom.development.js:18948)
at react-dom.development.js:20418
at Object.unstable_runWithPriority (scheduler.development.js:255)
```
resetErrorMessageAction is undefined because I had imported it incorrectly from a different file since I was using an export defaulted function.
So you need to correctly import the file.
import { ErrorMessageAlert } from 'error/ErrorMessageAlert';
vs
import ErrorMessageAlert from 'error/ErrorMessageAlert';
I have a bug with redux saga cancel effect.
I have the following redux store in my reactjs application :
/*
* The reducer takes care of our data
* Using actions, we can change our application state
* To add a new action, add it to the switch statement in the homeReducer function
*
* Example:
* case YOUR_ACTION_CONSTANT:
* return assign({}, state, {
* stateVariable: action.var
* });
*/
import { fromJS } from 'immutable';
import {
CHANGE_FORM,
SENDING_REQUEST,
REQUEST_SUCCESS,
CLEAR_SUCCESS,
REQUEST_ERROR,
CLEAR_ERROR,
} from './constants';
// The initial application state
const initialState = fromJS({
formState: {
username: 'dka',
password: '',
},
success: false,
error: false,
isCurrentlySending: false,
});
// Takes care of changing the application state
function loginReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_FORM:
return state
.set('formState', fromJS(action.newFormState));
case SENDING_REQUEST:
return state
.set('isCurrentlySending', action.sending);
case REQUEST_SUCCESS:
return state
.set('success', action.success)
.set('isCurrentlySending', false);
case REQUEST_ERROR:
return state
.set('error', action.error)
.set('isCurrentlySending', false);
case CLEAR_SUCCESS:
return state
.set('success', null);
case CLEAR_ERROR:
return state
.set('error', null);
default:
return state;
}
}
export default loginReducer;
This is my LoginForm where I handle the changeForm actions:
/**
* LoginForm
*
* The form with a username and a password input field, both of which are
* controlled via the application state.
*
*/
import React from 'react';
import Input from 'components/bootstrap/atoms/Input';
import Label from 'components/bootstrap/atoms/Label';
import H2 from 'components/bootstrap/atoms/H2';
import Form from 'components/bootstrap/atoms/Form';
import Button from 'components/bootstrap/atoms/Button';
import LoadingButton from 'components/kopax/atoms/LoadingButton';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
import { url } from 'config';
import { changeForm, requestError, clearError, clearSuccess } from 'containers/LoginPage/actions';
import Alert from 'components/bootstrap/atoms/Alert';
import LocaleToggle from 'containers/LocaleToggle';
export class LoginForm extends React.Component { // eslint-disable-line react/prefer-stateless-function
static propTypes = {
isCurrentlySending: React.PropTypes.bool.isRequired,
onSubmit: React.PropTypes.func.isRequired,
data: React.PropTypes.object.isRequired,
success: React.PropTypes.object,
error: React.PropTypes.object,
dispatch: React.PropTypes.func.isRequired,
};
render() {
const { success, error } = this.props;
return (
<Form action={url.login} onSubmit={this.onSubmit}>
<H2><FormattedMessage {...messages.title} /></H2>
{success && <Alert className="alert-success" onDismiss={this.hideSuccess}><FormattedMessage {...success} /></Alert>}
{error && <Alert className="alert-danger" onDismiss={this.hideError}><FormattedMessage {...error} /></Alert>}
<Label htmlFor="username"><FormattedMessage {...messages.username} /></Label>
<Input
type="text"
onChange={this.changeUsername}
placeholder="bob"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
/>
<Label htmlFor="password"><FormattedMessage {...messages.password} /></Label>
<Input
type="password"
onChange={this.changePassword}
placeholder="••••••••••"
/>
{this.props.isCurrentlySending ? (
<LoadingButton className="btn-primary">
<FormattedMessage {...messages.buttonLogin} />
</LoadingButton>
) : (
<div>
<LocaleToggle />
<Button className="primary">
<FormattedMessage {...messages.buttonLogin} />
</Button>
</div>
)}
</Form>
);
}
// Change the username in the app state
changeUsername = (evt) => {
const newState = this.mergeWithCurrentState({
username: evt.target.value,
});
this.emitChange(newState);
}
// Change the password in the app state
changePassword = (evt) => {
const newState = this.mergeWithCurrentState({
password: evt.target.value,
});
this.emitChange(newState);
}
// Merges the current state with a change
mergeWithCurrentState(change) {
return this.props.data.merge(change);
}
// Emits a change of the form state to the application state
emitChange(newState) {
this.props.dispatch(changeForm(newState));
}
// onSubmit call the passed onSubmit function
onSubmit = (evt) => {
evt.preventDefault();
const username = this.props.data.get('username').trim();
const password = this.props.data.get('password').trim();
const isValidated = this.validateForm(username, password);
if (isValidated) {
this.props.onSubmit(username, password);
} else {
this.props.dispatch(requestError(messages.errorFormEmpty));
}
}
// validate the form
validateForm(username, password) {
this.props.dispatch(clearError());
this.props.dispatch(clearSuccess());
return username.length > 0 && password.length > 0;
}
hideError = () => {
this.props.dispatch(clearError());
}
hideSuccess = () => {
this.props.dispatch(clearSuccess());
}
}
export default LoginForm;
Here is the , it is including the and is included in
/**
* FormPageWrapper
*/
import React from 'react';
import Alert from 'components/bootstrap/atoms/Alert';
import styled, { keyframes } from 'styled-components';
import defaultThemeProps from 'styled/themes/mxstbr/organisms/FormPageWrapper';
import LoginForm from '../../molecules/LoginForm';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
import cn from 'classnames';
const propTypes = {
isCurrentlySending: React.PropTypes.bool.isRequired,
onSubmit: React.PropTypes.func.isRequired,
className: React.PropTypes.string,
data: React.PropTypes.object.isRequired,
success: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.bool,
]),
error: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.bool,
]),
dispatch: React.PropTypes.func.isRequired,
};
const defaultProps = {
theme: {
mxstbr: {
organisms: {
FormPageWrapper: defaultThemeProps,
},
},
},
};
class FormPageWrapper extends React.Component {
render() {
const { className, onSubmit, dispatch, data, isCurrentlySending, success, error } = this.props;
return (
<div className={cn(className, 'form-page__wrapper')}>
<div className="form-page__form-wrapper">
<div className="form-page__form-header">
<h2 className="form-page__form-heading"><FormattedMessage {...messages.title} /></h2>
</div>
{success && <Alert className="mx-2 alert-success" onDismiss={this.hideSuccess}><FormattedMessage {...success} /></Alert>}
{error && <Alert className="mx-2 alert-danger" onDismiss={this.hideError}><FormattedMessage {...error} /></Alert>}
<LoginForm
onSubmit={onSubmit}
data={data}
dispatch={dispatch}
isCurrentlySending={isCurrentlySending}
/>
</div>
</div>
);
}
}
const shake = keyframes`
0% {
transform: translateX(0);
}
25% {
transform: translateX(10px);
}
75% {
transform: translateX(-10px);
}
100% {
transform: translateX(0);
}
`;
// eslint-disable-next-line no-class-assign
FormPageWrapper = styled(FormPageWrapper)`
${(props) => `
margin-top: ${props.theme.mxstbr.organisms.FormPageWrapper['$margin-x']};
&.form-page__wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.form-page__form-wrapper {
max-width: 325px;
width: 100%;
border: 1px solid ${props.theme.mxstbr.organisms.FormPageWrapper['$very-light-grey']};
border-radius: 3px;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25);
background-color: #fff;
}
.form-page__form-heading {
text-align: center;
font-size: 1em;
user-select: none;
}
.form-page__form-header {
padding: 1em;
}
& .js-form__err-animation {
animation: ${shake} 150ms ease-in-out;
}
`}
`;
FormPageWrapper.propTypes = propTypes;
FormPageWrapper.defaultProps = defaultProps;
export default FormPageWrapper;
Here is my
/*
* LoginPage
*
* This is the first thing users see of our App, at the '/' route
*
*/
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { selectLogin } from './selectors';
import { loginRequest } from './actions';
import FormPageWrapper from 'components/mxstbr/organisms/FormPageWrapper';
export class LoginPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
static propTypes = {
data: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
onSubmitFormLogin: React.PropTypes.func.isRequired,
};
render() {
const dispatch = this.props.dispatch;
const formState = this.props.data.get('formState');
const isCurrentlySending = this.props.data.get('isCurrentlySending');
const success = this.props.data.get('success');
const error = this.props.data.get('error');
return (
<FormPageWrapper
onSubmit={this.props.onSubmitFormLogin}
success={success}
error={error}
data={formState}
dispatch={dispatch}
isCurrentlySending={isCurrentlySending}
/>
);
}
}
export function mapDispatchToProps(dispatch) {
return {
dispatch,
onSubmitFormLogin: (username, password) => {
dispatch(loginRequest({ username, password }));
},
};
}
const mapStateToProps = createStructuredSelector({
data: selectLogin(),
});
// Wrap the component to inject dispatch and state into it
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);
This is my sagas handling the login :
import { getParameter } from 'utils/request';
import { pages, oauthClient, storage } from 'config';
import { browserHistory } from 'react-router';
import { takeLatest } from 'redux-saga';
import { take, call, put, fork, race, select, cancel } from 'redux-saga/effects';
import { LOCATION_CHANGE } from 'react-router-redux';
import auth from 'services/auth';
import { selectUsername, selectPassword } from './selectors';
// login actions
import { sendingRequest, clearSuccess, clearError, requestError, changeForm } from './actions';
import {
LOGIN_REQUEST,
} from './constants';
// app action solicited in LoginPage
import {
logout,
setAuthState,
} from 'containers/App/actions';
import {
LOGOUT,
} from 'containers/App/constants';
/**
* Effect to handle authorization
* #param {string} username The username of the user
* #param {string} password The password of the user
* #param {object} options Options
* #param {boolean} options.isRegistering Is this a register request?
*/
export function* getAuthorize({ username, password, isRegistering }) {
try { // eslint-disable-line padded-blocks
// We send an action that tells Redux we're sending a request
yield put(sendingRequest(true));
// make a first request to generate the cookie seession and include it in the login request
yield call(auth.preLogin);
// For either log in or registering, we call the proper function in the `auth`
// module, which is asynchronous. Because we're using generators, we can work
// as if it's synchronous because we pause execution until the call is done
// with `yield`!
let links;
if (isRegistering) {
links = yield call(auth.register, username, password);
} else {
links = yield call(auth.login, username, password);
}
if (links.err) {
throw links.err;
}
localStorage.setItem(storage.LINKS, JSON.stringify(links._links)); // eslint-disable-line no-underscore-dangle
// Now that we are logged in, we are eligible for a code request (see oauth2)
const fetchCode = yield call(auth.code, oauthClient.clientId, oauthClient.redirectUri);
const responseCodeUrl = yield fetchCode.url;
// let's get the token
const code = getParameter('code', responseCodeUrl);
if (!code) {
return false;
}
const jwt = yield call(auth.token, oauthClient.clientId, oauthClient.clientSecret, code, oauthClient.redirectUri, oauthClient.scopes);
if (!jwt) {
return false;
}
// TODO : use sessionStorage and localStorage only if Remember me button was checked (do we do a remember me button)
localStorage.setItem(storage.TOKEN, JSON.stringify(jwt));
return jwt;
} catch (error) {
// If we get an error we send Redux the appropiate action and return
yield put(requestError({ id: 'com.domain.api.messages', defaultMessage: error.message }));
return false;
} finally {
// When done, we tell Redux we're not in the middle of a request any more
yield put(sendingRequest(false));
}
}
/**
* Log in saga
*/
export function* getLogin() {
yield put(clearError());
yield put(clearSuccess());
const username = yield select(selectUsername());
const password = yield select(selectPassword());
// A `LOGOUT` action may happen while the `authorize` effect is going on, which may
// lead to a race condition. This is unlikely, but just in case, we call `race` which
// returns the 'winner', i.e. the one that finished first
const winner = yield race({
auth: call(getAuthorize, { username, password, isRegistering: false }),
logout: take(LOGOUT),
});
// If `authorize` was the winner...
if (winner.auth) {
// ...we send Redux appropiate actions
yield put(setAuthState(true)); // User is logged in (authorized)
yield put(changeForm({ username: '', password: '' })); // Clear form
forwardTo(pages.pageDashboard.path); // Go to dashboard page
// If `logout` won...
} else if (winner.logout) {
// ...we send Redux appropiate action
yield put(setAuthState(false)); // User is not logged in (not authorized)
yield call(logout); // Call `logout` effect
forwardTo(pages.pageLogin.path); // Go to root page
}
}
/**
* Watches for LOGIN_REQUEST actions and calls getLogin when one comes in.
* By using `takeLatest` only the result of the latest API call is applied.
*/
export function* getLoginWatcher() {
yield fork(takeLatest, LOGIN_REQUEST, getLogin);
}
/**
* Root saga manages watcher lifecycle
*/
export function* loginData() {
// Fork watcher so we can continue execution
console.log('starting lifecycle');
const watcher = yield fork(getLoginWatcher); // eslint-disable-line no-unused-vars
console.log('take location change');
yield take(LOCATION_CHANGE);
console.log('canceling', watcher.toString());
yield cancel(watcher); // <=== SEE WHY THIS TRIGGER ERROR "utils.js:202 uncaught at getLogin Generator is already running"
console.log('canceled');
}
// Little helper function to abstract going to different pages
function forwardTo(location) {
browserHistory.push(location);
}
export default [
loginData,
];
I'm trying to understand why does the cancel in the root saga throw the error :
utils.js:202 uncaught at getLogin Generator is already running
If I remove the cancel, leaving and going page on the login will result in multiplication of this sagas.
Is there a way to make this error disappear ? I've tried a try catch block but it didn't work.