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

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.

Related

How can I convert a Class Component which extends another Class component in a Functional Component in ReactJS?

How can I convert a Class Component which extends another Class component in a Functional Component in ReactJS?
input.jsx [Functional Component]
const Input = ({ name, label, error, ...rest }) => {
return (
<div className="mb-3">
<label htmlFor={name} className="form-label">
{label}
</label>
<input
autoFocus
{...rest}
id={name}
name={name}
className="form-control"
/>
{error && <div className="alert alert-danger">{error}</div>}
</div>
)
}
export default Input
form.jsx [Class Component]
import React, { Component } from "react"
import Input from "./input"
import Joi from "joi"
class Form extends Component {
state = {
data: {},
errors: {}
}
validate = () => {
const options = { abortEarly: false }
const schemaJoi = Joi.object(this.schema)
const { error } = schemaJoi.validate(this.state.data, options)
if (!error) return null
const errors = {}
error.details.map(item => (errors[item.path[0]] = item.message))
return errors
}
validateProperty = ({ name, value }) => {
const obj = { [name]: value }
const schema = {
[name]: this.schema[name]
}
const schemaJoi = Joi.object(schema)
const { error } = schemaJoi.validate(obj)
return error ? error.details[0].message : null
}
handleSubmit = e => {
e.preventDefault()
const errors = this.validate()
console.log(errors)
this.setState({ errors: errors || {} })
if (errors) return
this.doSubmit()
}
handleChange = ({ currentTarget: input }) => {
const errors = { ...this.state.errors }
const errorMessage = this.validateProperty(input)
if (errorMessage) errors[input.name] = errorMessage
else delete errors[input.name]
const data = { ...this.state.data }
data[input.name] = input.value
this.setState({ data, errors })
}
renderButton = label => {
return (
<button disabled={this.validate()} className="btn btn-primary">
{label}
</button>
)
}
renderInput = (name, label, type = "text") => {
const { data, errors } = this.state
return (
<Input
name={name}
label={label}
error={errors[name]}
type={type}
value={data[name]}
onChange={this.handleChange}
/>
)
}
}
export default Form
loginForm.jsx [Class Component which extends the other]
import Joi from "joi"
import Form from "./common/form"
class LoginForm extends Form {
state = {
data: { username: "", password: "" },
errors: {}
}
schema = {
username: Joi.string().required().label("Username"),
password: Joi.string().required().label("Password")
}
doSubmit = () => {
console.log("Submitted")
}
render() {
return (
<div>
<h1>Login</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("username", "Username")}
{this.renderInput("password", "Password", "password")}
{this.renderButton("Login")}
</form>
</div>
)
}
}
export default LoginForm
I already know how to convert a simple Class Component to a Stateless Functional Component but what I don't know is how to convert a Class Component which extends another Class Component.
Please, may you explain me how?

Passing props to Parent component

I am really novice to React and I am stuck with this one.
I want to pass data from NewAction component to its parent NewActionSet.
I dont know what i am missing.
I am developing an on-boarding platform with a lot a components and I aim to send all the data entered into all the components to a server.
React parent Component:
import React from 'react'
import './NewActionSet.css'
import axios from 'axios'
import { Container, Segment, Header, Input } from 'semantic-ui-react'
import NewAction from './NewAction'
import 'bootstrap/dist/css/bootstrap.min.css'
class NewActionSet extends React.Component {
constructor (props) {
super(props)
this.state = {
actions: [],
actionType: '',
actionValue: '',
creationStatus: undefined
}
}
handleActions = value => {
this.setState({
actionsList: value
})
console.log(this.state.actionsList)
}
handleSubmit = event => {
event.preventDefault()
console.log(this.state)
axios
.post(
'/assistant/actions/',
{ ...this.state.values },
{ headers: {
xsrfHeaderName: 'X-CSRFToken',
xsrfCookieName: 'csrftoken'
},
withCredentials: true
}
)
.then(response => {
console.log(response)
this.setState({
creationStatus: true
})
})
.catch(error => {
console.log(error)
this.setState({
creationStatus: false
})
})
}
addNewAction = () => {
let { actions } = this.state
this.setState({
actions: [...actions, <NewAction onNewAction={this.handleActionstoParent} />]
})
}
handleActionstoParent = (action2Value, selectedAction) => {
this.setState({
actionType : selectedAction,
actionValue : action2Value
})
// console.log(this.state.actionType, this.state.actiondValue)
}
renderActions () {
return this.state.actions.map((action, index) => {
return (
<NewAction
key={index}
type={this.props.actionType}
content={action.content}
onNewAction={this.handleActionstoParent}
/>
)
})
}
render () {
let index = 0
return (
<Container>
<Header> Action sets </Header>
<Header color='grey' as='h3'>
SET #{index + 1}
</Header>
{this.renderActions()}
<button onClick={() => this.addNewAction()}> New Action </button>
</Container>
)
}
}
export default NewActionSet
React child component
import React from 'react'
import './NewActionSet.css'
import { Header, Dropdown } from 'semantic-ui-react'
import NewSpeechText from './NewSpeechText'
import NewAddPageURL from './NewAddPageURL'
import 'bootstrap/dist/css/bootstrap.min.css'
class NewAction extends React.Component {
constructor (props) {
super(props)
this.state = {
availableActions: [
{ key: 1, text: 'Navigate to page', value: 'Navigate to page' },
{ key: 2, text: 'Play speech', value: 'Play speech' }
],
selectedAction: '',
actionValue: '',
currentElement: ''
}
}
handleActionURL = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
handleActionSpeech = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
// Props to pass data to parent component --> NewActionSet.js
handleActionstoParent = (selected) => {
var action2Value = this.state.actionValue;
console.log(action2Value)
var action2Type = this.state.actionType
this.props.onNewAction(action2Value, action2Type)
console.log(action2Type)
// console.log(this.state.actionValue, this.state.selectedAction)
}
handleChange = (e, { value }) => {
let element
this.setState({
selectedAction: value
})
if (value === 'Navigate to page') {
element = <NewAddPageURL onNewAddPageURL={this.handleActionURL} onChange={this.handleActionstoParent()} />
} else if (value === 'Play speech') {
element = <NewSpeechText onNewSpeechText={this.handleActionSpeech} onChange={this.handleActionstoParent()} />
}
this.setState({
currentElement: element
})
}
render () {
const { value } = this.state
let index = 0
return (
<div className='action'>
<div className='container'>
<Header color='grey' as='h4'>
ACTION #{index + 1}
</Header>
<div className='row'>
<div className='col-md-4'>
<Dropdown
onChange={this.handleChange}
options={this.state.availableActions}
placeholder='Choose an action'
selection
value={value}
/>
</div>
<div className='col-md-4' />
<div className='col-md-4' />
</div>
<div style={{ marginBottom: '20px' }} />
{this.state.currentElement}
</div>
</div>
)
}
}
export default NewAction
Can you please assist?
Thanks a lot
The handleActionstoParent function in NewAction component is the problem.
When you send data from child to parent, actually the data is not updated data.
// Props to pass data to parent component --> NewActionSet.js
handleActionstoParent = (e) => {
this.setState({ [e.target.name]: e.target.value }, () => {
var action2Value = this.state.actionValue;
var action2Type = this.state.actionType;
this.props.onNewAction(action2Value, action2Type);
});
}
You could pass a function to NewAction, in example below we pass handleDataFlow function to our child component and then use it in our child component to pass data higher:
import React from 'react'
import './NewActionSet.css'
import { Header, Dropdown } from 'semantic-ui-react'
import NewSpeechText from './NewSpeechText'
import NewAddPageURL from './NewAddPageURL'
import 'bootstrap/dist/css/bootstrap.min.css'
class NewAction extends React.Component {
constructor (props) {
super(props)
this.state = {
availableActions: [
{ key: 1, text: 'Navigate to page', value: 'Navigate to page' },
{ key: 2, text: 'Play speech', value: 'Play speech' }
],
selectedAction: '',
actionValue: '',
currentElement: ''
}
}
handleActionURL = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
handleActionSpeech = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
// Props to pass data to parent component --> NewActionSet.js
handleActionstoParent = (selected) => {
var action2Value = this.state.actionValue;
console.log(action2Value)
var action2Type = this.state.actionType
this.props.onNewAction(action2Value, action2Type)
console.log(action2Type)
// console.log(this.state.actionValue, this.state.selectedAction)
}
handleChange = (e, { value }) => {
let element
this.setState({
selectedAction: value
})
this.props.handleDataFlow(value)
if (value === 'Navigate to page') {
element = <NewAddPageURL onNewAddPageURL={this.handleActionURL} onChange={this.handleActionstoParent()} />
} else if (value === 'Play speech') {
element = <NewSpeechText onNewSpeechText={this.handleActionSpeech} onChange={this.handleActionstoParent()} />
}
this.setState({
currentElement: element
})
}
render () {
const { value } = this.state
let index = 0
return (
<div className='action'>
<div className='container'>
<Header color='grey' as='h4'>
ACTION #{index + 1}
</Header>
<div className='row'>
<div className='col-md-4'>
<Dropdown
onChange={this.handleChange}
options={this.state.availableActions}
placeholder='Choose an action'
selection
value={value}
/>
</div>
<div className='col-md-4' />
<div className='col-md-4' />
</div>
<div style={{ marginBottom: '20px' }} />
{this.state.currentElement}
</div>
</div>
)
}
}
export default NewAction
Data flow in React is unidirectional. Data has one, and only one, way to be transferred: from parent to child.
To update parent state from child you have to send action (in props).
<NewAction updateParentState={this.doSmth} />
...
const doSmth = params => { this.setState({ ... })
and in NewAction you can call it in specific case
let parentUpdateState = ....
this.props.updateParentState(parentUpdateState);

Cannot access updated state from mapStateToProps, undefined

I'm new to redux and am having trouble accessing state from mapStateToProps. I'm trying to create a 'folder' when the user enters the folder name and submits. I've managed to update the state with an array of folder names but can't manage to access the folder array and use this to create my Folder components.
Here is a container component that is supposed to give access to 'folders' in my Folders component:
import { connect } from 'react-redux';
import Folders from './Folders';
const mapStateToProps = state => {
return {
folders: state.folders
}
}
const Cabinet = connect(
mapStateToProps
)(Folders);
export default Cabinet;
Here is the component im trying to access the state from:
import React from 'react';
import Folder from './Folder';
import AddFolderButton from './AddFolderButton';
const Folders = ({ folders }) => (
<div className="Folders">
<h2>Folders</h2>
<div className="Drawer">
{console.log(folders)}
<AddFolderButton />
</div>
</div>
)
export default Folders;
'folders' is always undefined when I update data in the store.
I'm not quite sure what I'm doing wrong, I've been working through the basics tutorial in the Redux docs but think I may be fundamentally misunderstanding something.
Here's the code I used to update the store:
Reducer
import { combineReducers } from 'redux';
const initialState = {
folders: []
}
function handleFolders(state = initialState, action) {
switch(action.type) {
case 'CREATE_FOLDER':
return {
...state, folders: [
...state.folders,
{
name: action.name
}
]
}
default:
return state;
}
}
let rootReducer = combineReducers({
handleFolders
})
export default rootReducer;
The button to 'create' a folder:
class AddFolderButton extends React.Component {
constructor() {
super();
this.state = {
isClicked: false,
};
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
this.textInput = null;
}
handleClick() {
if(!this.state.isClicked) {
document.addEventListener('click', this.handleOutsideClick, false);
} else {
document.removeEventListener('click', this.handleOutsideClick, false);
}
this.setState(prevState => ({
isClicked: !prevState.isClicked
}));
}
handleOutsideClick(e) {
if(this.node.contains(e.target)) {
return;
}
this.handleClick();
}
render() {
return(
<div className="new-folder-input" ref={ node => {this.node = node;}}>
{!this.state.isClicked && (
<button className="add-folder-btn" onClick={this.handleClick} >
<FontAwesomeIcon icon={faPlus} size="4x"/>
</button>
)}
{this.state.isClicked && (
<div className="folder-input">
<form onSubmit={e => {
e.preventDefault()
this.props.createFolder(this.textInput.value)
this.handleClick();
}}
>
<input ref={node => this.textInput = node}
type="text"
value={this.state.value}
autoFocus
placeholder="Enter folder name" />
<button type="submit">Add Folder</button>
</form>
</div>
)}
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
createFolder: name => dispatch(createFolder(name))
})
export default connect(
null,
mapDispatchToProps
)(AddFolderButton);
Try changing your mapStateToProps method to this:
const mapStateToProps = ({handleFolders: {folders}}) => ({
folders
});
When your're combining your reducers you must access them first using it's name. Your reducer is called handleFolders and it has property folders

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>

React re-renders whole app after rendering a component

I use react and redux in my web app. It's the simple app which has 4 components, one reducer and 3 actions. After I add a new entry to list, react renders component of list (the listItem), then re-renders the whole app. What is the cause of re-rendering whole app after rendering one component?
Updated:
App container:
class App extends Component {
static propTypes = {
groups: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
render() {
return (<div>
<Header addGroup={this.props.actions.addGroup} />
<List groups={this.props.groups} />
</div>
);
}
}
function mapStateToProps(state) {
return { groups: state.groups };
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(AppActions, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Reduser:
export default function groupDiseases(state = initialState, action){
switch (action.type) {
case ADD_GROUP:
return [
{
id: '',
name: action.name
},
...state
];
case DELETE_GROUP:
return state.filter(group =>
group.id !== action.id
);
case EDIT_GROUP:
return state.map(group => (group.id === action.id ? { id: action.id, name: action.name } : group));
default:
return state;
}
}
Components:
export default class Add extends Component {
static propTypes = {
addGroup: PropTypes.func.isRequired
}
componentDidMount() {
this.textInput.focus();
}
handleAdd = () => {
const name = this.textInput.value.trim();
if (name.length !== 0) {
this.props.addGroup(name);
this.textInput.value = '';
}
}
render() {
return (
<form className="add_form">
<input
type="text"
className="add__name"
defaultValue=""
ref={(input) => this.textInput = input}
placeholder="Name" />
<button
className="add__btn"
ref="add_button"
onClick={this.handleAdd}>
Add
</button>
</form>
);
}
}
export default class ListGroups extends Component {
static propTypes = {
groups: PropTypes.array.isRequired
};
render() {
let data = this.props.groups;
let groupTemplate = <div> Группы отсутствуют. </div>;
if (data.length) {
groupTemplate = data.map((item, index) => {
return (
<div key={index}>
<Item item={item} />
</div>
);
});
}
return (
<div className="groups">
{groupTemplate}
<strong
className={'group__count ' + (data.length > 0 ? '' : 'none')}>
Всего групп: {data.length}
</strong>
</div>
);
}
}
It's likely due to the fact that you are letting the <form> continue its default behavior, which is to submit to a targeted action. Take a look at the w3c spec for buttons:
http://w3c.github.io/html-reference/button.html
Specifically, a button with no type attribute will default to submit.
So your button is telling the form to submit, with the target being the current page since none is provided. In your handleAdd method, you can do something like:
handleAdd = (event) => {
event.preventDefault(); // prevent default form submission behavior
const name = this.textInput.value.trim();
if (name.length !== 0) {
this.props.addGroup(name);
this.textInput.value = '';
}
}
Or you can modify your button to have type="button".

Resources