React-redux mapStateToProps values not updated - reactjs

I have a weird scenario in which inside my redux dev-tools I can see that the redux state has been updated but that value is not being reflected in the component which is connected to that.
So I have a component CreateEnvironment like
export interface StateProps {
environment: EnvironmentStep;
selectProduct: SelectProductStep;
eula: EULAStep;
license: LicenseStep;
infrastructure: InfrastructureStep;
network: NetworkStep;
certificate: CertificateStep;
products: any;
}
export interface DispatchProps {
onFormValuesUpdate: (key: string, value: any) => void;
}
type Props = StateProps & DispatchProps;
interface State {
dashboard: boolean;
}
class CreateEnvironment extends Component<Props, State> {
private page: RefObject<any>;
constructor(props: Props) {
super(props);
this.state = { dashboard: false };
this.page = React.createRef();
}
public render() {
console.log("Hi"); //tslint:disable-line
if (this.state.dashboard) {
return <Redirect to="/dashboard" />;
}
const {
environment,
eula,
selectProduct,
infrastructure,
network,
license,
certificate,
onFormValuesUpdate
} = this.props;
return (
<Fragment>
<h2>Create Environment</h2>
<div style={{ height: '100%', paddingBottom: '30px' }}>
<WorkFlow
orientation="vertical"
onNext={this.onNext}
onFinish={this.onFinish}
>
<WorkFlowPage pageTitle="Environment">
<Environment
environment={environment}
onUpdate={onFormValuesUpdate}
ref={this.page}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Products & Solutions">
<SelectProduct
ref={this.page}
selectProduct={selectProduct}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage
pageNavTitle="EULA"
pageTitle="End User License Agreement Details"
>
<Eula eula={eula} ref={this.page} onUpdate={onFormValuesUpdate} />
</WorkFlowPage>
<WorkFlowPage pageTitle="License">
<License
license={license}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Infrastructure">
<Infrastructure
infrastructure={infrastructure}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Network">
<Network
network={network}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Certificate">
<Certificate
certificate={certificate}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Products">I am products step</WorkFlowPage>
<WorkFlowPage pageTitle="Summary">I am summary step</WorkFlowPage>
<WorkFlowButton role="previous">Back</WorkFlowButton>
<WorkFlowButton role="next">Next</WorkFlowButton>
<WorkFlowButton role="finish">Finish</WorkFlowButton>
</WorkFlow>
</div>
</Fragment>
);
}
private onNext = () => {
if (this.page.current) {
this.page.current.handleSubmit();
}
}
private onFinish = () => {
this.setState({
dashboard: true
});
}
}
export default CreateEnvironment;
The corresponding container looks like:
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import {
CreateEnvironmentAction,
updateEnvironmentWorkflow
} from '../actions/CreateEnvironment';
import {
CreateEnvironment,
DispatchProps,
StateProps
} from '../components/create-environment';
import { StoreState } from '../types';
export const mapStateToProps = ({
createEnvironment
}: StoreState): StateProps => ({
...createEnvironment
});
export const mapDispatchToProps = (
dispatch: Dispatch<CreateEnvironmentAction>
): DispatchProps => ({
onFormValuesUpdate: (key: string, values: any) => {
dispatch(updateEnvironmentWorkflow(key, values));
}
});
export default connect<StateProps, DispatchProps>(
mapStateToProps,
mapDispatchToProps
)(CreateEnvironment);
All the components inside WorkflowPage like Environment are forms which use the Formik pattern.
A sample component is:
interface Props {
certificate?: CertificateStep;
onUpdate?: (key: string, value: any) => void;
}
class Certificate extends Component<Props> {
private submitForm: (
e?: React.FormEvent<HTMLFormElement> | undefined
) => void;
public render() {
const { formValues } = this.props.certificate!;
console.log(formValues); //tslint:disable-line
return (
<Form
className="license"
type="horizontal"
initialValues={formValues}
onSubmit={this.onSubmit}
>
{formProps => {
this.submitForm = formProps.handleSubmit;
return (
<Fragment>
<CheckboxField
name="productSpecific"
id="productSpecific"
type="toggle"
>
Provide Product Specific Certificate
</CheckboxField>
<SelectField name="certificate" label="Certificate">
<SelectOption value="">---Select Certificate---</SelectOption>
</SelectField>
</Fragment>
);
}}
</Form>
);
}
public handleSubmit = () => {
this.submitForm();
}
private onSubmit = (values: FormValues, actions: FormActions) => {
this.props.onUpdate!('certificate', values);
actions.setSubmitting(false);
}
}
export default Certificate;
On clicking the next WorkflowButton the redux state is updated by passing an action updateEnvironmentWorkflow. The action is like:
export interface UpdateEnvironmentWorkflow {
type: Constants.UPDATE_ENVIRONMENT_WORKFLOW;
payload: {
key: string;
value: any;
};
}
export type CreateEnvironmentAction = UpdateEnvironmentWorkflow;
export const updateEnvironmentWorkflow = (
key: string,
value: any
): UpdateEnvironmentWorkflow => ({
payload: { key, value },
type: Constants.UPDATE_ENVIRONMENT_WORKFLOW
});
And my reducer is like:
const createEnvironment = (
state: CreateEnvironment = initialState.createEnvironment,
action: CreateEnvironmentAction
) => {
switch (action.type) {
case Constants.UPDATE_ENVIRONMENT_WORKFLOW:
return {
...state,
[action.payload.key]: {
...state[action.payload.key],
formValues: action.payload.value
}
};
default:
return state;
}
};
export default createEnvironment;
Now the weird part is that if I click the next button the redux state is updated with new values. If I navigate to some other page and come back to create environment my form values are showing the updated values from the redux store state.
BUT if I just hit the previous button my state values are not showing updated values.
The console.log(formValues) inside render() of Certificate is showing initial values only.
Any help will be appreciated. Thanks.
Also initialValues of Form is Formik initialValues. It maybe a formik issue as well

Related

Why children component with redux-form doesn't re-render?

Parent Component send data but children don't re-render, only when press a key on a input.
SMART
I send userValues form state, if i put a console.log(this.props.state.userValues) in render(), component smart render when redux get new properties.
import React, { Component, RefObject } from 'react';
import { bindActionCreators } from 'redux';
import CSSModules from 'react-css-modules';
import { connect } from 'react-redux';
import WebappHeader from '../../containers/WebappHeader/WebappHeader';
import BlueSection from '../../containers/BlueSection/BlueSection';
import Title from '../../components/Title/Title';
import { getBase64, isString, userData } from '../../helpers';
import * as styles from './MyAccount.css';
import * as actions from './../../actions/accounts';
import { updateAccountInformation, getAccountInformation } from '../../services/AccountService';
import UserProfile from '../../components/UserProfile/UserProfile';
interface State {
tabActiveClass: string;
extensions: string;
file_64: string;
unsavedChanges: boolean;
}
interface Props { state: any, actions: any }
class MyAccount extends Component <Props, State> {
private fileInput: RefObject <HTMLInputElement>;
private avatarImage: RefObject <HTMLImageElement>;
constructor(props: any) {
super(props);
this.state = { ...this.props.state }
this.avatarImage = React.createRef();
this.fileInput = React.createRef();
}
componentDidMount() {
setTimeout(() => {
getAccountInformation().then(result => {
result.user
? this.props.actions.userAccountLoad(result.user)
: null
});
}, 1000)
// setea la primera tab como la que esta activa
this.setState({ tabActiveClass: document.getElementsByClassName('tablinks')[0].classList[2] });
}
handleSubmit = (userData: any): void => {
// console.log(userData)
updateAccountInformation(userData)
.then((result: any) => {
!result.error
? this.props.actions.userAccountUpdate(result)
: this.props.actions.userAccountError()
})
}
private uploadAvatar(files: any): void {
if (files.length > 0){
let file = files[0], extensions_allowed = this.state.extensions.split(',');
let extension = `.${file.name.split('.').pop().toLowerCase()}`;
if(extensions_allowed.indexOf(extension) === -1){
alert(`This extension is not allowed. Use: ${this.state.extensions}`);
this.fileInput.current!.value = '';
} else {
getBase64(file, (result: any) => {
this.setState({file_64: result, unsavedChanges: true});
this.avatarImage.current!.src = result;
console.log(result); // nueva img, ejecutar disparador
});
}
}
}
private changeTab(e: any, name: string): void {
let i: number;
const future_tab: any = document.getElementById(name);
const tabcontent: any = document.getElementsByClassName('tabcontent');
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = 'none';
}
const tablinks: any = document.getElementsByClassName('tablinks');
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(` ${this.state.tabActiveClass}`, '');
}
future_tab.style.display = 'flex';
e.currentTarget.classList.add(this.state.tabActiveClass);
}
public render() {
return (
<BlueSection styleName="section">
<WebappHeader />
<div styleName="tabs-headers">
<div className="wrapper">
<Title text="Account Information" />
<ul styleName="list">
<li styleName="item active" className="tablinks" onClick={(e: any) => this.changeTab(e, 'profile')}>
Profile
</li>
<li styleName="item" className="tablinks" onClick={(e: any) => this.changeTab(e, 'activity')}>
Activity
</li>
<li styleName="item" className="tablinks" onClick={(e: any) => this.changeTab(e, 'plan')}>
Plan & Billing
</li>
</ul>
</div>
</div>
<div styleName="tabs-body">
<div className="wrapper">
<ul styleName="list">
<li styleName="item" id="profile" className="tabcontent" style={{'display':'flex'}}>
<UserProfile
onSubmit={this.handleSubmit}
userValues={this.props.state.values}
// avatarImage={this.avatarImage}
// uploadAvatar={this.uploadAvatar}
// fileInput={this.fileInput}
/>
</li>
<li styleName="item" id="activity" className="tabcontent">
</li>
<li styleName="item" id="plan" className="tabcontent">
</li>
</ul>
</div>
</div>
</BlueSection>
)
}
}
const mapStateToProps = (state: any) => ({ state: state.account });
const mapDispatchToProps = (dispatch: any) => ({ actions: bindActionCreators(actions, dispatch) });
const ComponentWithCSS = CSSModules(MyAccount, styles, { allowMultiple: true });
export default connect(mapStateToProps, mapDispatchToProps)(ComponentWithCSS);
CHILDREN
I receive userValues form smart component, if i put a console.log(this.props.userValues) in render(), component doesn't render when get new properties.
import React, { Component, Fragment } from 'react';
import CSSModules from 'react-css-modules';
import { InjectedFormProps, reduxForm, Field } from 'redux-form';
import { connect } from 'react-redux';
import Heading from '../Heading/Heading';
import Button from '../Button/Button';
import Input from '../Input/Input';
import * as styles from './UserProfile.css';
interface Props {
userValues: any,
avatarImage: any,
uploadAvatar: any,
fileInput: any,
handleSubmit: any,
}
const inputField = ({ input, label, type, meta, disabled, field_value }: any) => (
<div>
<Input
{...input}
labelText={label}
styleName="input"
type={type ? type : "text"}
disabled={disabled ? disabled : false}
placeholder={field_value ? field_value : ''}
/>
{/* fix mejorar mensajes css */}
{
meta.error && meta.touched && <span>{meta.error}</span>
}
</div>
)
const isRequired = (value: any) => (
value ? undefined : 'Field Required'
)
class UserProfile extends Component<Props & InjectedFormProps<{}, Props>> {
constructor(props: any) {
super(props);
}
componentWillMount() { this.props.initialize({ name: this.props.userValues.name }) }
public render() {
console.log(this.props.userValues)
// setTimeout(() => {
// this.forceUpdate() // temp (bad) fix
// }, 2000)
return (
<Fragment>
<div styleName="right-info">
<Heading text="Your data" styleName="heading" />
<form id="UserProfile" onSubmit={this.props.handleSubmit} method="POST" styleName="form">
<fieldset styleName="fieldset">
<Field
name="name"
label="Name"
validate={isRequired}
placeholder={this.props.userValues.name}
component={inputField} />
<br />
<Input
labelText="Email"
styleName="input"
type="email"
disabled={true}
placeholder={this.props.userValues.email} />
</fieldset>
<fieldset styleName="fieldset">
<Field
name="phone_number"
label="Phone Number"
validate={isRequired}
component={inputField} />
<br />
<Field
name="company"
label="Company"
validate={isRequired}
component={inputField} />
</fieldset>
</form>
<Heading text="Notifications" styleName="heading" />
<form styleName="notification-form">
<label styleName="label">
I wish to recieve newsletters, promotions and news from BMS
<input type="checkbox" name="notifications" />
</label>
<p styleName="disclaimer">
<span styleName="bold">Basic information on Data Protection:</span> BMS collects your data too improve our services and, if given consent, will keep you updated on news and promotions of BMS projects. +Info Privacy Policy
</p>
</form>
</div>
<div styleName="cta-wrapper">
<Button
onClick={this.props.handleSubmit}
text="SAVE CHANGES"
filled={true}
// disabled={!this.props.state.unsavedChanges}
/>
</div>
</Fragment>
)
}
}
const UserProfileWithCSS = CSSModules(UserProfile, styles, { allowMultiple: true });
export default connect()(reduxForm<{}, Props>({ form: 'UserProfile' })(UserProfileWithCSS));
REDUCER
I think it's okay
import { USER_ACCOUNT_UPDATE, USER_ACCOUNT_LOAD, USER_ACCOUNT_ERROR } from './../actions/types';
import { userData } from '../helpers';
const engine = require('store/src//store-engine');
const storages = require('store/storages/sessionStorage');
const store = engine.createStore(storages);
const INITIAL_STATE = {
tabActiveClass: '',
extensions: '.jpeg,.jpg,.gif,.png',
file_64: '',
unsavedChanges: false,
values: {
name: '',
email: '',
phone_number: '',
company: '',
notifications: false
},
};
export default function (state = INITIAL_STATE, action: any) {
switch (action.type) {
case USER_ACCOUNT_LOAD: {
let newState = state;
newState.values.name = action.payload.profile.first_name;
newState.values.email = action.payload.email;
newState.values.phone_number = action.payload.profile.phone_number;
newState.values.company = action.payload.profile.company;
return { ...newState };
}
case USER_ACCOUNT_UPDATE: {
let newState = state;
let storage = store.get('user_data');
storage.profile.first_name = action.payload.data.name;
storage.profile.company = action.payload.data.company;
storage.profile.phone_number = action.payload.data.phone_number;
newState.values.name = action.payload.data.name;
newState.values.phone_number = action.payload.data.phone_number;
newState.values.company = action.payload.data.company;
store.set('user_data', storage);
return { ...newState };
}
case USER_ACCOUNT_ERROR:
return { ...state };
default:
return state;
}
}
You're correctly creating a new state object in your account reducer, which is sufficient to signal to your parent component that the mapped props have changed. However, in your reducer, you're always carrying forward the same values object.
case USER_ACCOUNT_LOAD: {
let newState = state;
newState.values.name = action.payload.profile.first_name;
newState.values.email = action.payload.email;
newState.values.phone_number = action.payload.profile.phone_number;
newState.values.company = action.payload.profile.company;
return { ...newState };
}
This isn't a problem when it comes to triggering renders in your parent component, since state.account has an updated reference after each action. However:
<UserProfile
...
userValues={this.props.state.values}
...
/>
UserProfile is specifically being passed the values object, and will thus be doing its shallow reference checks for incoming props on the values object, not the state object. Since values always refers to the same object according to your reducers, UserProfile will not re-render when props.userValues changes. If you're going to be passing a property of one of your props to a child component, you need to ensure that it ALSO passes the shallow reference check:
case USER_ACCOUNT_LOAD: {
let newState = { ...state };
newState.values = {
...state.values, // Copy all properties from old state
name: action.payload.profile.first_name, // Replace individual properties with new values.
email: action.payload.email,
phone_number = action.payload.profile.phone_number,
company = action.payload.profile.company
};
return newState;
}
And the same idea for USER_ACCOUNT_UPDATE.
This is why its much safer to always treat objects and arrays as immutable in your reducers. Your reducers are currently fine for signalling Redux of changed state, but you lose the guarantee when/if you start prop-drilling as you're doing with UserProfile.

Dialog plays animation when state changes

I'm using Redux and material-ui
I'm trying to run Dialog with <Slide direction="up"/> animation using this attribute: TransitionComponent
email is a state value that came from reducer and changes when I enter value on TextField
When I try to enter some value on, animation plays but, I want to play it only one time.
interface IProps extends WithStyles<typeof styles> {
// ...
setEmail: (email: string) => void;
email: string;
// ...
}
const LoginDialog: React.SFC<IProps> = props => {
const handleClose = () => {
props.setIsOpen(false);
};
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.setPassword(event.target.value);
};
const handlePasswordVisibility = () => {
props.setPasswordVisibility(!props.passwordVisibility);
};
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.setEmail(event.target.value);
};
return (
<div>
<Dialog
open={props.isOpen}
//dialog plays animation when props.isOpen changes
TransitionComponent={props => <Slide direction="up" {...props} />}
onClose={handleClose}
aria-labelledby="login-dialog-slide-title"
aria-describedby="login-dialog-slide-description"
disableBackdropClick={true}
keepMounted
>
<DialogTitle id="login-dialog-slide-title">
<FormattedMessage id="logindialog_title" />
</DialogTitle>
<DialogContent>
<TextField value={props.email} onChange={handleEmailChange} autoFocus type="email" label={<FormattedMessage id="logindialog_email" />}/>
<TextField type="password" label={<FormattedMessage id="logindialog_password" />} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
<FormattedMessage id="logindialog_cancle" />
</Button>
<Button onClick={handleClose} color="primary">
<FormattedMessage id="logindialog_ok" />
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default withStyles(styles)(withRouter(LoginDialog));
I updated my container which has mapStateToProps and action, reducer for email
and also you can see my full code here: codesandbox.io/s/nkrmw3wjxj
import { connect } from "react-redux";
import { ICombineReducersState } from "../../../reducers";
import LoginDialog from "./LoginDialog";
import {
setIsOpen,
setPassword,
setPasswordVisibility,
setEmail,
setNickname,
DuplicatedEmail,
setIsEmailDuplicated
} from "../../../actions";
const mapStateToProps = (state: ICombineReducersState) => ({
isOpen: state.open.isOpen,
password: state.password.password,
passwordVisibility: state.password.passwordVisibility,
email: state.email.email,
isPasswordError: state.password.isPasswordError,
isEmailError: state.email.isEmailError,
isEmailDuplicated: state.email.isEmailDuplicated
});
const mapDispatchToProps = (dispatch: any) => ({
setIsOpen: (isOpen: boolean) => dispatch(setIsOpen(isOpen)),
setPassword: (password: string) => dispatch(setPassword(password)),
setPasswordVisibility: (passwordVisibility: boolean) =>
dispatch(setPasswordVisibility(passwordVisibility)),
setEmail: (email: string) => dispatch(setEmail(email)),
setNickname: (nickname: string) => dispatch(setNickname(nickname)),
DuplicatedEmail: () => dispatch(DuplicatedEmail()),
setIsEmailDuplicated: (isEmailDuplicated: boolean) =>
dispatch(setIsEmailDuplicated(isEmailDuplicated))
});
export const LoginDialogContainer = connect(
mapStateToProps,
mapDispatchToProps
)(LoginDialog);
export const SET_EMAIL = "SET_EMAIL";
export const SET_IS_EMAIL_DUPLICATED = "SET_IS_EMAIL_DUPLICATED";
import axios from "axios";
import config from "../config";
export interface IEmailAction {
email: string;
type: string;
isEmailDuplicated: boolean;
}
export const setEmail = (email: string) => {
return {
email,
type: SET_EMAIL,
} as IEmailAction;
};
export const setIsEmailDuplicated = (isEmailDuplicated: boolean) => {
return {
isEmailDuplicated,
type: SET_IS_EMAIL_DUPLICATED,
} as IEmailAction;
}
export const DuplicatedEmail = () => (dispatch: any):boolean => {
axios.get(`${config.REACT_APP_SERVER_URL}/users/email`)
.then(res => {
if (res.data.message.length >= 1) {
return dispatch(setIsEmailDuplicated(true));
}
})
.catch(err => {
console.log(err.response)
})
return dispatch(setIsEmailDuplicated(false));
}
import { IEmailAction, SET_EMAIL, SET_IS_EMAIL_DUPLICATED } from "../actions";
export interface IEmailState {
email: string;
isEmailError: boolean;
isEmailDuplicated: boolean;
}
const createEmpty = () => ({
email: "",
isEmailError: false,
isEmailDuplicated: false,
});
const emailRegex = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*#[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
export const emailReducer = (state = createEmpty(), action: IEmailAction) => {
switch (action.type) {
case SET_EMAIL: {
return {
email: action.email,
isEmailError: !validateEmail(action.email),
isEmailDuplicated: false,
} as IEmailState;
}
case SET_IS_EMAIL_DUPLICATED: {
return {
email: state.email,
isEmailError: true,
isEmailDuplicated: action.isEmailDuplicated,
} as IEmailState;
}
default:
return state;
}
};
const validateEmail = (email: string):boolean => {
if (emailRegex.test(email)) {
return true;
}
return false;
}
Please let me know if you need more info about it.
Thanks.
The following line was the problem:
TransitionComponent={props => <Slide direction="up" {...props} />}
By defining this inline it means that the TransitionComponent will look like a new component type with each re-render which then causes that portion of the Dialog to re-mount and therefore redo the transition.
This is easily fixed by defining this outside of your component function as a stable component type (I've called it TransitionComponent below) and then using this for the TransitionComponent Dialog prop:
const TransitionComponent = props => <Slide direction="up" {...props} />;
const LoginDialog: React.SFC<IProps> = props => {
...
return (
<div>
<Dialog
open={props.isOpen}
TransitionComponent={TransitionComponent}
...
};

TypeScript: Either or for functions passed down through several components in React

I'm writing a React Native application using TypeScript.
I have a component EmotionsRater that accepts one of two types: Emotion or Need. It should also either accept a function of type rateNeed or rateEmotion. I combined these types to one called rateBoth using the | operator. And it passes this combined type down to another component called EmotionsRaterItem. The problem is that EmotionsRaterItem then claims:
Cannot invoke an expression whose type lacks a call signature. Type 'rateBoth' has no compatible call signatures.
I provided the boiled down code for all relevant components below.
QuestionsScreen.tsx:
// ... imports
export type rateEmotion = (rating: number, emotion: Emotion) => void;
export type rateNeed = (rating: number, emotion: Need) => void;
export interface Props {
navigation: NavigationScreenProp<any, any>;
}
export interface State {
readonly emotions: Emotion[];
readonly needs: Need[];
}
let EMOTIONS_ARRAY: Emotion[] = // ... some array of emotions
let NEEDS_ARRAY: Need[] = // ... some array of needs
export class QuestionsScreen extends Component<Props, State> {
static navigationOptions = // ... React Navigation Stuff
readonly state = {
emotions: EMOTIONS_ARRAY.slice(),
needs: NEEDS_ARRAY.slice()
};
swiper: any;
componentWillUnmount = () => {
// ... code to reset the emotions
};
toggleEmotion = (emotion: Emotion) => {
// ... unrelated code for the <EmotionsPicker />
};
rateEmotion: rateEmotion = (rating, emotion) => {
this.setState(prevState => ({
...prevState,
emotions: prevState.emotions.map(val => {
if (val.name === emotion.name) {
val.rating = rating;
}
return val;
})
}));
};
rateNeed: rateNeed = (rating, need) => {
this.setState(prevState => ({
...prevState,
need: prevState.emotions.map(val => {
if (val.name === need.name) {
val.rating = rating;
}
return val;
})
}));
};
goToIndex = (targetIndex: number) => {
const currentIndex = this.swiper.state.index;
const offset = targetIndex - currentIndex;
this.swiper.scrollBy(offset);
};
render() {
const { emotions, needs } = this.state;
return (
<SafeAreaView style={styles.container} forceInset={{ bottom: "always" }}>
<Swiper
style={styles.wrapper}
showsButtons={false}
loop={false}
scrollEnabled={false}
showsPagination={false}
ref={component => (this.swiper = component)}
>
<EmotionsPicker
emotions={emotions}
toggleEmotion={this.toggleEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={emotions.filter(emotion => emotion.chosen)}
rateEmotion={this.rateEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={needs}
rateEmotion={this.rateNeed}
goToIndex={this.goToIndex}
tony={true}
/>
</Swiper>
</SafeAreaView>
);
}
}
export default QuestionsScreen;
EmotionsRater.tsx:
// ... imports
export type rateBoth = rateEmotion | rateNeed;
export interface Props {
emotions: Emotion[] | Need[];
rateEmotion: rateBoth;
goToIndex: (targetIndex: number) => void;
tony?: boolean;
}
export interface DefaultProps {
readonly tony: boolean;
}
export class EmotionsRater extends PureComponent<Props & DefaultProps> {
static defaultProps: DefaultProps = {
tony: false
};
keyExtractor = (item: Emotion | Need, index: number): string =>
item.name + index.toString();
renderItem = ({ item }: { item: Emotion | Need }) => (
<EmotionsRaterItem emotion={item} rateEmotion={this.props.rateEmotion} />
);
renderHeader = () => {
const { tony } = this.props;
return (
<ListItem
title={tony ? strings.needsTitle : strings.raterTitle}
titleStyle={styles.title}
bottomDivider={true}
containerStyle={styles.headerContainer}
leftIcon={tony ? badIcon : goodIcon}
rightIcon={tony ? goodIcon : badIcon}
/>
);
};
goBack = () => {
this.props.goToIndex(0);
};
goForth = () => {
this.props.goToIndex(2);
};
render() {
return (
<View style={styles.container}>
<FlatList<Emotion | Need>
style={styles.container}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
data={this.props.emotions}
ListHeaderComponent={this.renderHeader}
stickyHeaderIndices={[0]}
/>
<ButtonFooter
firstButton={{
disabled: false,
onPress: this.goBack,
title: strings.goBack
}}
secondButton={{
disabled: false,
onPress: this.goForth,
title: strings.done
}}
/>
</View>
);
}
}
export default EmotionsRater;
EmotionsRaterItem.tsx:
// ... imports
export interface Props {
emotion: Emotion | Need;
rateEmotion: rateBoth;
}
export interface State {
readonly rating: number;
}
export class EmotionsRaterItem extends PureComponent<Props, State> {
readonly state = { rating: this.props.emotion.rating };
ratingCompleted = (rating: number) => {
this.setState({ rating });
this.props.rateEmotion(rating, this.props.emotion);
// This ^^^^^^^^^^^ throws the error mentioned in the post.
};
render() {
const { emotion } = this.props;
const { rating } = this.state;
const color = getColor(rating);
return (
<ListItem
title={emotion.name}
bottomDivider={true}
rightTitle={String(Math.round(rating * 100))}
rightTitleStyle={{ color: color.hex("rgb") }}
rightContentContainerStyle={styles.rightContentContainer}
subtitle={
<Slider
value={emotion.rating}
thumbTintColor={activeColor}
minimumTrackTintColor={color.hex("rgb")}
maximumTrackTintColor={color.alpha(0.4).hex("rgba")}
step={0.01}
onValueChange={this.ratingCompleted}
/>
}
/>
);
}
}
export default EmotionsRaterItem;
What is going on? Why doesn't TypeScript know that rateBoth is one of two functions and therefore callable?
EDIT:
Thanks to Estus's comment I added the code here instead of gists.
If EmotionsRaterItem has a function of type rateBoth, then that function either requires an Emotion or requires a Need, but the caller does not know which of the type is required. Hence, under current TypeScript semantics, it's impossible to call the function. (You could imagine that maybe passing an argument that is both an Emotion and a Need should work, but TypeScript isn't that smart; see this issue.)
Instead, you could make EmotionsRater and EmotionsRaterItem generic in the type T of the item they are working on (either Emotion or Need). (Of course, generic components are unsound in general, but it looks like the problem won't occur in your scenario.) Semi-complete example:
QuestionsScreen.tsx
// ... imports
import { Component } from "react";
import EmotionsRater from "./EmotionsRater";
import * as React from "react";
export interface Emotion {
emotionBrand: undefined;
name: string;
rating: number;
}
export interface Need {
needBrand: undefined;
name: string;
rating: number;
}
export type rateEmotion = (rating: number, emotion: Emotion) => void;
export type rateNeed = (rating: number, emotion: Need) => void;
export interface Props {
navigation: NavigationScreenProp<any, any>;
}
export interface State {
readonly emotions: Emotion[];
readonly needs: Need[];
}
let EMOTIONS_ARRAY: Emotion[] = []; // ... some array of emotions
let NEEDS_ARRAY: Need[] = []; // ... some array of needs
export class QuestionsScreen extends Component<Props, State> {
static navigationOptions; // ... React Navigation Stuff
readonly state = {
emotions: EMOTIONS_ARRAY.slice(),
needs: NEEDS_ARRAY.slice()
};
swiper: any;
componentWillUnmount = () => {
// ... code to reset the emotions
};
toggleEmotion = (emotion: Emotion) => {
// ... unrelated code for the <EmotionsPicker />
};
rateEmotion: rateEmotion = (rating, emotion) => {
this.setState(prevState => ({
...prevState,
emotions: prevState.emotions.map(val => {
if (val.name === emotion.name) {
val.rating = rating;
}
return val;
})
}));
};
rateNeed: rateNeed = (rating, need) => {
this.setState(prevState => ({
...prevState,
need: prevState.emotions.map(val => {
if (val.name === need.name) {
val.rating = rating;
}
return val;
})
}));
};
goToIndex = (targetIndex: number) => {
const currentIndex = this.swiper.state.index;
const offset = targetIndex - currentIndex;
this.swiper.scrollBy(offset);
};
render() {
const { emotions, needs } = this.state;
return (
<SafeAreaView style={styles.container} forceInset={{ bottom: "always" }}>
<Swiper
style={styles.wrapper}
showsButtons={false}
loop={false}
scrollEnabled={false}
showsPagination={false}
ref={component => (this.swiper = component)}
>
<EmotionsPicker
emotions={emotions}
toggleEmotion={this.toggleEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={emotions.filter(emotion => emotion.chosen)}
rateEmotion={this.rateEmotion}
goToIndex={this.goToIndex}
/>
<EmotionsRater
emotions={needs}
rateEmotion={this.rateNeed}
goToIndex={this.goToIndex}
tony={true}
/>
</Swiper>
</SafeAreaView>
);
}
}
export default QuestionsScreen;
EmotionsRater.tsx
// ... imports
import { PureComponent } from "react";
import * as React from "react";
import { Emotion, Need } from "./QuestionsScreen";
import EmotionsRaterItem from "./EmotionsRaterItem";
export interface Props<T extends Emotion | Need> {
emotions: T[];
rateEmotion: (rating: number, emotion: T) => void;
goToIndex: (targetIndex: number) => void;
tony?: boolean;
}
export interface DefaultProps {
readonly tony: boolean;
}
export class EmotionsRater<T extends Emotion | Need> extends PureComponent<Props<T> & DefaultProps> {
static defaultProps: DefaultProps = {
tony: false
};
keyExtractor = (item: Emotion | Need, index: number): string =>
item.name + index.toString();
renderItem = ({ item }: { item: T }) => (
<EmotionsRaterItem emotion={item} rateEmotion={this.props.rateEmotion} />
);
renderHeader = () => {
const { tony } = this.props;
return (
<ListItem
title={tony ? strings.needsTitle : strings.raterTitle}
titleStyle={styles.title}
bottomDivider={true}
containerStyle={styles.headerContainer}
leftIcon={tony ? badIcon : goodIcon}
rightIcon={tony ? goodIcon : badIcon}
/>
);
};
goBack = () => {
this.props.goToIndex(0);
};
goForth = () => {
this.props.goToIndex(2);
};
render() {
return (
<View style={styles.container}>
<FlatList<T>
style={styles.container}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
data={this.props.emotions}
ListHeaderComponent={this.renderHeader}
stickyHeaderIndices={[0]}
/>
<ButtonFooter
firstButton={{
disabled: false,
onPress: this.goBack,
title: strings.goBack
}}
secondButton={{
disabled: false,
onPress: this.goForth,
title: strings.done
}}
/>
</View>
);
}
}
export default EmotionsRater;
EmotionsRaterItem.tsx
// ... imports
import { PureComponent } from "react";
import * as React from "react";
import { Emotion, Need } from "./QuestionsScreen";
export interface Props<T extends Emotion | Need> {
emotion: T;
rateEmotion: (rating: number, emotion: T) => void;
}
export interface State {
readonly rating: number;
}
export class EmotionsRaterItem<T extends Emotion | Need> extends PureComponent<Props<T>, State> {
readonly state = { rating: this.props.emotion.rating };
ratingCompleted = (rating: number) => {
this.setState({ rating });
this.props.rateEmotion(rating, this.props.emotion);
};
render() {
const { emotion } = this.props;
const { rating } = this.state;
const color = getColor(rating);
return (
<ListItem
title={emotion.name}
bottomDivider={true}
rightTitle={String(Math.round(rating * 100))}
rightTitleStyle={{ color: color.hex("rgb") }}
rightContentContainerStyle={styles.rightContentContainer}
subtitle={
<Slider
value={emotion.rating}
thumbTintColor={activeColor}
minimumTrackTintColor={color.hex("rgb")}
maximumTrackTintColor={color.alpha(0.4).hex("rgba")}
step={0.01}
onValueChange={this.ratingCompleted}
/>
}
/>
);
}
}
export default EmotionsRaterItem;

Error - Client/Server Mismatch: React, Redux, NextJS, Typescript, and Styled Components

I have been trying to work on a Counter example using Next.js for SSR, Typescript, and Styled Components. I have a mostly-working example on Github: https://github.com/dwaynelavon/nextscript-boilerplate
The issue is that I keep getting errors about a Client/Server Mismatch. Warning: Text content did not match. Server: "1" Client: "0". I can't figure out what the issue is. Any suggestions?
// Index.tsx
type CounterDispatchProps = {
addCount: () => any;
startClock: () => any;
};
type CounterStateProps = {
lastUpdate: number;
light: boolean;
};
class Counter extends React.Component<
CounterDispatchProps & CounterStateProps
> {
private timer: number;
constructor(props: CounterDispatchProps & CounterStateProps) {
super(props);
}
static getInitialProps(props: any) {
const store = props['store'];
const isServer = props['isServer'];
store.dispatch(serverRenderClock(isServer));
store.dispatch(addCount());
return { isServer };
}
componentDidMount() {
this.timer = this.props.startClock();
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const props = {
title: 'Index Page!',
linkTo: '/other',
lastUpdate: this.props.lastUpdate,
light: this.props.light
};
return <Page {...props} />;
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
};
};
const mapStateToProps = (state: any) => {
return {
lastUpdate: state['lastUpdate'],
light: state['light']
};
};
export default withRedux(initStore, mapStateToProps,
mapDispatchToProps)(Counter);
Here is the page component:
Interface PageProps {
title: string;
linkTo: string;
lastUpdate: number;
light: boolean;
}
const Page = ({ title, linkTo, lastUpdate, light }: PageProps) => {
const clockProps = {
lastUpdate,
light
};
return (
<div>
<h1>{title}</h1>
<Clock {...clockProps} />
<AddCount />
<nav>
<Link href={linkTo}>
<a>Navigate</a>
</Link>
</nav>
</div>
);
};
export default Page;

Getting the updated state after an action is dispatched in redux

I am using react and redux.
I have a Container component defined as so:
import { connect } from 'react-redux';
import {addTag} from 'actions';
import ExpenseTagsControl from './expense_tags_control'
const mapStateToProps = (state, own_props={selected_tags:[]}) => {
return {
tags_list: state.tags.tags_list
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTag: (tag_name) => {
dispatch(addTag(tag_name))
}
};
};
const AddExpenseTagsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ExpenseTagsControl);
export default AddExpenseTagsContainer;
The container wraps a presentational component which is defined as so:
// expense_tags_control.js
import React, {Component, PropTypes} from 'react';
import ChipInput from 'material-ui-chip-input';
import Chip from 'material-ui/Chip';
import Avatar from 'material-ui/Avatar';
import Tag from 'common/svg_icons/tag';
import AutoComplete from 'material-ui/AutoComplete'
import _ from 'underscore';
class ExpenseTagsControl extends React.Component {
constructor(props) {
super(props);
this.state = {
chips: []
};
};
handleAdd(chip) {
// If the chip does not already exist, add it. the id here will be a dummy value that is not there in the tags_list
if (!(_.contains( _.map(this.props.tags_list, (tag) => tag.id), chip.id))) {
this.props.addTag(chip.name);
}
// This is wrong.
this.setState({
chips: [...this.state.chips, chip]
});
};
handleDelete(chip) {
this.setState({
chips: this.state.chips.filter((c) => c !== deletedChip)
});
};
chipRenderer({ text, value, isFocused, isDisabled, handleClick, handleRequestDelete }, key) {
const style = {
margin: '8px 8px 0 0',
float: 'left',
pointerEvents: isDisabled ? 'none' : undefined
};
return (
<Chip key={key} style={style} onTouchTap={handleClick} onRequestDelete={handleRequestDelete}>
<Avatar size={24} icon={<Tag />} />
{text}
</Chip>
);
};
render() {
return (
<ChipInput
hintText="Tags"
value={this.state.chips}
onRequestAdd={(chip) => this.handleAdd(chip)}
onRequestDelete={(deletedChip) => this.handleDelete(deletedChip)}
fullWidth={true}
dataSourceConfig={{ text: 'name', value: 'id' }}
dataSource={this.props.tags_list}
chipRenderer={this.chipRenderer}
openOnFocus={false}
filter={AutoComplete.fuzzyFilter}
onRequestDelete={console.log("Deleted")}
/>);
};
};
ExpenseTagsControl.PropTypes = {
tags_list: PropTypes.array.isRequired,
addTag: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired
};
export default ExpenseTagsControl;
The presentational component above, maintains a state, which indicates the chips that have been selected.
The ChipInput component allows you to select chips which are objects with an id, and a name, defined from a pre-existing data source. The component also allows you to add a new chip by typing in the name. If the typed in name does not exist in the data source, it is added to the data source.
My Problem
The id of the newly added chip is assigned once the addTag() action is dispatched. How do I get the value of the result of the action that was just dispatched?
I thought about working around this by maintaining the state of the ChipInput in the global state, and manipulate the global state upon dispatching the addTag() action. But that feels like too much overhead.
If what I understand is correct, you might want something like this:
class ExpenseTagsControl extends React.Component {
// ...
/*
* assuming your reducers are working fine and 'addTag'
* has updated global 'state.tags.tags_list'
*/
componentWillReceiveProps(nextProps) {
this.setState({ chips: this.nextProps.tags_list });
}
// ...
}
NB: You might need to optimize calling setState inside componentWillReceiveProps based on some conditions to avoid unnecessary re-render.
From what I understand, the OP's problem is how to dispatch an action to modify the redux store and at the same time update the component's local state.
Edit: added a working example
const initialState = {
tags: ['hello', 'hi', 'howdy']
}
function reducer(state = {}, action) {
switch (action.type) {
case 'ADD_TAG':
return {
...state,
tags: [
...state.tags,
action.payload.tag
]
}
default:
return state;
}
}
const store = Redux.createStore(reducer, initialState);
const addTag = (tag) => ({
type: 'ADD_TAG',
payload: {
tag
}
})
class Chips extends React.Component {
constructor(props) {
super(props);
this.chipToAdd = false;
this.state = {
chips: []
}
this.handleAdd = this.handleAdd.bind(this);
}
componentWillReceiveProps(nextProps) {
console.log(this.chipToAdd);
if (this.chipToAdd) {
this.setState({
chips: [...this.state.chips, this.chipToAdd]
}, (this.chipToAdd = false));
}
}
handleAdd(chip) {
if (this.props.tags.filter(tag => tag === chip).length === 0) {
this.chipToAdd = chip;
this.props.addTag(chip);
} else {
if (this.state.chips.filter(existingChip => existingChip === chip).length === 0) {
this.setState({
chips: [...this.state.chips, chip]
});
}
}
}
render() {
return <div >
< h3 > Tags added in component 's chip state</h3>
<ul>
{this.state.chips.map((chip, index) => <li key={index}>{chip}</li>)}
</ul>
<hr />
<h3>Tags in Redux Store</h3>
{this.props.tags.map(
(tag, index) => <li key={index}>
{tag} <button onClick={() => this.handleAdd(tag)}>Add</button>
</li>
)}
<button onClick={() => this.handleAdd('
new tag - ' + Math.floor((Math.random() * 100) + 1))}>Add a chip with new tag</button>
</div>
}
}
const mapStateToProps = ({ tags = [] }) => ({ tags });
const ConnectedChips = ReactRedux.connect(mapStateToProps, { addTag })(Chips);
class App extends React.Component {
render() {
return <div>
<h1>React/Redux Demo</h1>
<ConnectedChips />
</div>
}
}
const Provider = ReactRedux.Provider;
ReactDOM.render(
<Provider store={store}><App /></Provider>,
document.getElementById('
root ')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/redux#3.6.0/dist/redux.min.js"></script>
<script src="https://unpkg.com/react-redux#4.4.6/dist/react-redux.min.js"></script>
<div id="root"></div>

Resources