I have question about passing componentDidMount function from parent to deep laying child.
I have a list of items, which are selected by items status. After I change one of the item's status, I need to re-render parent to get new data. Tricky part for me is that, that I can't find way, how to pass componentDidMount function or action to fetch my list's data again.
My parent class:
class Page extends React.Component {
componentDidMount() {
this.props.onCompMount();
}
render() {
const { error, loading, list } = this.props;
const pageListProps = {
loading,
error,
list,
};
return (
<article>
<div>
<PageList {...pageListProps} />
</div>
</article>
);
}
}
My 1st child:
function PageList({ loading, error, list }) {
if (loading) {
return <List component={LoadingIndicator} />;
}
if (error !== false) {
const ErrorComponent = () => (
<ListItem item="Something went wrong, please try again!" />
);
return <List component={ErrorComponent} />;
}
if (list !== false) {
return <List items={list} component={PageItem} />;
}
return null;
}
2nd child:
export class PageItem extends React.PureComponent {
constructor() {
super();
this.state = {
modalIsOpen: false,
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
openModal() {
this.setState({ modalIsOpen: true });
}
closeModal() {
this.setState({ modalIsOpen: false });
}
render() {
const { item } = this.props;
// Put together the content of the repository
const content = (
<Wrapper>
<h3>{item.title}</h3>
<button onClick={this.openModal}>Decline</button>
<Modal
isOpen={this.state.modalIsOpen}
onRequestClose={this.closeModal}
style={customStyles}
contentLabel="Preview"
>
<Form close={this.closeModal} />
</Modal>
</Wrapper>
);
And my last child where I want after submit to re-render parent container:
export class Form extends React.Component {
render() {
return (
<article>
<form
onSubmit={e => {
e.preventDefault();
this.props.submit();
this.props.close();
//Somehow re-render parent
}}
>
<div className="row" style={{ textAlign: 'start' }}>
Do you really want to change status?
<div className="col-md-12 buttonContainer">
<ButtonA
label="Submit"
style={{ width: '50%' }}
primary
type="submit"
/>
</div>
</div>
</form>
</article>
);
}
}
What I have tried is to reload page with window.location.reload(); and it works. But I think it is bad practice with React. Maybe someone could advise me how to make it better?
EDIT: I am adding parent reducer and 4th child reducer.
Parent reducer:
const initialState = fromJS({
loading: false,
error: false,
listData: {
list: false,
},
});
function pageReducer(state = initialState, action) {
switch (action.type) {
case FETCH_LIST_BEGIN:
return state
.set('loading', true)
.set('error', false)
.setIn(['listData', 'list'], false);
case FETCH_LIST_SUCCESS:
return state
.setIn(['listData', 'list'], action.list)
.set('loading', false);
case FETCH_LIST_FAILURE:
return state.set('error', action.error).set('loading', false);
default:
return state;
}
}
export default pageReducer;
4th child reducer:
const initialState = fromJS({});
function formReducer(state = initialState, action) {
switch (action.type) {
case SUBMIT:
return state;
default:
return state;
}
}
export default formReducer;
We use Redux or React's new Context API to avoid prop drilling issue in react.
In your use you can dispatch action from parent component and connect relevant reducer to your 4th level child. So when your reducer updates global state (store), your connected component will re-render and take updated state as in props.
Just pass this.props.onCompMount() as props to child components and then call it in child component every time it gets updated.
Related
I would like to understand the behavior of react component constructor. Let suppose I have three components - PageComponent, ListComponent, ItemComponent. My pseudo-code structure is:
PageComponent (get data from redux, fetch data)
ListComponent (obtains data as props, in loop (map) renders list of ItemComponents)
ItemComponent (obtains item data as props, renders item, manipulate data)
Logic:
- when data in ItemComponent changes, changes are stored in REDUX and this change caused list re-rendering.
Use-case 1:
- PageComponent renders ListComponent and ListComponent renders list of ItemComponets
- when REDUX listItem data chages, PageComponent is updated, ListComponent is updated and ItemComponent CONSTRUCTOR is called (its local state is reset)
Use-case 2:
- PageComponent renders only LIST (using map loop) of ItemComponents.
- when REDUX listItem data chages, PageComponent is updated ItemComponent CONSTRUCTOR is NOT called (component is "only" updated) (and its local state is NOT reset)
Why there is a different behavior in these examples?
Source code:
PageComponent:
import React from 'react'
...
class UsersPage extends React.Component {
constructor(props) {
super(props)
props.actions.getUsers();
}
render() {
const {users} = this.props
return (
<Main>
{/* // NO ITEM CONSTRUCTOR IS CALLED
users.data.items.map((item, index) => {
return <ListItemComponent
data={item}
itemMethods={{
getItem: (data) => this.props.actions.getUser(data),
onEdit: (data) => this.props.actions.updateUser(data),
onDelete: (data) => this.props.actions.deleteUser(data),
validation: (data) => validateInput(this.props.strings, data)
}}
key={index}
/>
})*/
}
{ // ITEM CONSTRUCTOR IS CALLED
<ListComponent
loading={users.isFetching}
data={users.data}
methods={{
getItem: (data) => this.props.actions.getUser(data),
onEdit: (data) => this.props.actions.updateUser(data),
onDelete: (data) => this.props.actions.deleteUser(data),
validation: (data) => validateInput(this.props.strings, data)
}}
/>}
</Main>
);
}
}
UsersPage.propTypes = {
users: PropTypes.object.isRequired,
strings: PropTypes.object.isRequired,
}
function mapStateToProps(state) {
return {
users: state.users,
strings: state.strings.data || {},
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getUsers,
getUser,
addUser,
updateUser,
deleteUser,
}, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withAlert(UsersPage));
ListComponent:
import React from 'react'
...
class ListComponent extends React.Component {
getList() {
return <div className="list-outer">
<Row>
{
items.map((item, index) => {
return <ListItemComponent
data={item}
itemMethods={methods}
key={index}
/>
})
}
</Row>
</div>
}
render() {
const {loading} = this.props
return (
<div className="list-wrapper">
{
loading ? <Spinner visible={true}/>
:
this.getList()
}
</div>
)
}
}
ListComponent.propTypes = {
loading: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
methods: PropTypes.object.isRequired,
}
export default ListComponent
ListItemComponent:
import React from 'react'
...
class ListItemComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
editMode: false,
}
}
toggleEditMode(){
const editMode = this.state.editMode
this.setState({editMode: !editMode})
}
onEdit(id) {
const itemMethods = this.props.itemMethods
this.toggleEditMode()
itemMethods.getItem({id: id})
}
onDelete(item) {
//...
}
getFields(rowData, index) {
return <div key={index}>
{
rowData.map((itm, idx) => {
return <div key={idx}>{itm.label}: {itm.value}</div>
})
}
</div>
}
render() {
const editMode = this.state.editMode
const {data, itemMethods, strings} = this.props
return (
editMode ?
<Form
id={data.id}
onSubmit={(data) => itemMethods.onEdit(data)}
validation={(data) => itemMethods.validation(data)}
onCloseForm={() => this.toggleEditMode()}
/>
:
<Col xs={12}>
<div>
<div
{this.getFields(data)}
</div>
<div className="controls">
<button
className="btn btn-theme inverse danger"
onClick={() => this.onDelete(data)}
>{strings.delete}</button>
<button
onClick={() => this.onEdit(data.id)}
className="btn btn-theme" type="button"
>
{strings.edit}
</button>
</div>
</div>
</Col>
)
}
}
ListItemComponent .propTypes = {
strings: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
itemMethods: PropTypes.object.isRequired,
}
function mapStateToProps(state) {
return {
strings: state.strings.data || {}
};
}
export default connect(
mapStateToProps,
null,
)(ListItemComponent )
Ensure each ItemComponent has a key prop set. When React renders your list of items, it needs to know how to identify each element and React leaves it up to you to do this. If you omit the key prop, React will destroy and re-create your list upon each re-render, which means calling the component constructor.
If you provide the exact code you're using, we can better point out where your issue is coming from.
You can read more about lists and keys here.
SOLVED
It was cause by ListComponent and the loading prop that was placed as condion in render function. When item was edited, prop loading was set to true, spinner became visible AND it was the only element in ListComponent and therefore the list items were unmounted
I am trying to understand how to pass a changed state from child component to its parent in ReactJS? so far the following code changes the child state but not the parents state, any clue what I am doing wrong?
I am using redux to get product array from mongodb.
Product array example:
[
{
“_id”: “2331”,
“department”: “Shoes”,
“category”: “Shoes/Women/Pumps”,
“name”: “Calvin Klein”,
“title”: “Evening Platform Pumps”,
“description”: “Perfect for a casual night out or a formal event.”,
“style”: “Designer”,
"colors": ["red","yellow","red","black"]
},
{
“_id”: “30671”,
“department”: “Shoes”,
“category”: “Shoes/Women/Pumps”,
“name”: “zara”,
“title”: “Evening Platform Pumps”,
“description”: “Perfect for a casual night out or a formal event.”,
“style”: “Designer”,
"colors": ["red","yellow","red","black"]
}
]
Parent Component
import React, { Component } from 'react'
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
};
}
componentDidMount() {
this.props.getProducts();
}
componentDidUpdate(prevProps, prevState) {
if (this.props.product.products !== prevState.products) {
this.setState({ products: this.props.product.products });
}
}
onUpdateProducts = (e) => {
const newProducts = this.state.products;
this.props.updateProductName(newProducts);
};
render() {
const { products } = this.state;
if (isEmpty(products)) {
productContent = (
<div>
<p className="lead text-muted">Error Empty Products </p>
</div>
);
} else {
const productArr = products.map((product) => (
<Child key={product._id} product={product} />
));
productContent = (
<div>
{productArr}
</div>
);
}
return (
<div className="container">
{productContent}
<div className="row">
<div className="col-md-12">
<button className="btn " onClick={this.onUpdateProducts}>
Submit
</button>
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state) => ({
product: state.product
});
export default connect(mapStateToProps, {
getProducts,updateProductName
})(Parent);
Child Component
import React, { Component } from 'react'
export default class Child extends Component {
constructor(props) {
super(props);
this.state = {
product: this.props.product,
};
}
componentDidUpdate(prevProps, prevState) {
if (this.props.product !== prevProps.product) {
this.setState({
product: this.props.product
});
}
}
onChangeProductName = (e) => {
const newProduct = Object.assign({}, this.state.product, {
name: e.target.value
});
this.setState({ product: newProduct }, function() {
console.log('onChangeProductName: ', this.state.product);
});
};
render() {
const { product } = this.state;
return (
<div>
<TextInput
placeholder="Product Name"
name="prd_name"
value={product.name}
onChange={this.onChangeProductName}
/>
</div>
)
}
}
There are two ways for a child component to update the parent component:
Without using Redux, you can pass a function as a prop of the child component, so the child component can call this function to update the parent component.
Store the data in the Redux store. The child component dispatches an action which updates the Redux state, where the parent component gets data.
A simple example would explain the concept of passing the changed state from child to the parent.
Component A:
export default class A extends Component{
//This is a callback
handleStateChange = (value) ={
console.log("value", value);//you get the value here when state changes in B(Child) component
}
render(){
return(
<div>
<B handleStateChange={this.handleStateChange} />
</div>
)
}
}
Component B:
export Class B extends Component{
constructor(props){
super(props);
this.state={
value: "01"
}
}
handleButton = () => {
const value = "02";
this.setState({
value: "02"
});
this.props.handleStateChange(value);
}
render(){
return(
<div>
<button onClick={this.handleButton} />
</div>
)
}
}
Or you can directly pass the state if you call this.props.handleStateChange(this.state.value); this in render directly on any event handler if you want to pass updated state
As #Ying zuo mentioned you need to use redux to get the changed state value of child component in parent component.
When state changes in child component, you make a redux action call by passing the value as param and set that in the state in reducer and get the state in your parent component
Hope that explains the concept.
You have to pass the child a function.
In the child component you are setting state to be equal to props value, and then you are updating state. This has no connection to parent class - you also shouldn't modify props just as an aside.
The solution is to pass a function from the parent to child. This function will update the parent state, and because you are passing the parent state to the child, it will also be updated.
So in your parent class you could do something like:
onChangeProductName = (value, i) => {
const new_product_array = [...this.state.products];
new_product_array[i].name = value;
this.setState({ products: new_product_array});
};
You would need to pass this to the child
const productArr = products.map((product, i) => (
<Child
key={product._id}
product={product} onChangeName={this.onChangeProductName.bind(this)}
index={i} />
));
And then call it in the child
<TextInput
placeholder="Product Name"
name="prd_name"
value={product.name}
onChange={() => this.props.onChangeName(product, this.props.index)}
/>
The child component then doesn't need all the state tracking.
I would like to ask if how to dispatch or catch the data in mapStateToProps if data that I want to get is in a nested state and the identifier would be the this.props.group that is passed in FilmList via the Parent Component.
// Parent Component
<Row>
<FilmList group="upcoming" groupTitle="Upcoming Movies" />
<FilmList group="top_rated" groupTitle="Top Rated Movies" />
</Row>
// Child Component
class FilmList extends React.Component {
constructor(props){
super(props);
}
componentDidMount(){
this.props.getMovieByGroup(this.props.group);
}
renderFilmItem(){
if(this.props.data){
var film = this.props.data.upcoming.slice(0,6).map((item) => {
return <FilmItem key={item.id} film={item} />
});
return film;
}
}
render(){
console.log('new');
return(
<div className={styles.filmContainer}>
<h1>{ this.props.groupTitle }</h1>
{ this.renderFilmItem() }
</div>
);
}
}
function mapStateToProps(state){
return {
data: state.film.data.upcoming
}
}
This is what my state looks like:
This is my reducer:
const INITIAL_STATE = {
data: {},
error: {},
};
function processData(initialData, data) {
let updated = initialData;
updated[data.group] = data.results;
return updated;
}
export default (state = INITIAL_STATE, action) => {
switch(action.type) {
case GET_FILM_SUCCESS: {
return Object.assign({}, state.data[action.data.group], {
data: processData(state.data,action.data)
});
}
case GET_FILM_FAILURE: {
return { ...state, error: action.data }
}
}
return state;
}
Currently in my mapStateToProps I only access state.film.data.upcoming what I want to achieve is like state.film.data.{this.props.group} somewhere along that code so it will re render the component when "top_rated" or "upcoming" data state change.
So if state.file.data.upcoming is working fine, then you should be able to use state.file.data in mapStateToProps then do state.file.data[this.props.group] in your 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".
I try to use React-Redux to develope a Login Page and I encounter with some problems.
I want to send the TextFields about account and password to server by AJAX, but when I click the login button, this login button can not get the value of TextFields.
I do not want to use "form tag" and I try to do my best to solve this problem by myself, but I still suspect my solution, so I want to find some advise about this problem.
My solution is the following
TextFields will trigger an action which will set Login info into the next state.
The Login Button will do send these info to server only.
The following is the part of codes.
TextField:
class BaseInputComponent extends React.Component {
constructor(props) {
super(props);
this._handleChange = this._handleChange.bind(this);
}
_handleChange(e) {
this.props.onChange({infoType: this.props.dataType, data: e.target.value})
}
render() {
var style = {
marginBottom: 30
};
return (
<div style={style}>
<TextField
hintText={ this.props.hintText }
floatingLabelText={ this.props.floatingLabelText }
type={ this.props.type }
onChange={ this._handleChange }
value={this.props.textField}
/>
</div>
);
}
}
A Login Page
class UserPaperComponent extends React.Component {
constructor(props) {
super(props);
}
handleClickBtn() {
this.props.actions.userLogin()
}
render() {
var style = {
padding: 20,
textAlign: 'center',
};
return (
<Paper style={style} zDepth={5}>
<UserPaperTitleComponent text={this.props.userPaperText}/>
<Divider />
<BaseInputComponent dataType="userAccount" textField={this.props.userInfo.userAccount} onChange={ this.props.actions.setUserInfo } floatingLabelText="Account"/>
<BaseInputComponent dataType="userPwd" textField={this.props.userInfo.userPwd} onChange={ this.props.actions.setUserInfo } floatingLabelText="Password" type="password"/>
<Divider />
<br />
<RaisedButton label={this.props.userPaperText} onMouseUp={ this.handleClickBtn } />
</Paper>
);
}
}
In the reduce
const initialState = {
userAccount: '',
userPwd: '',
userHadLogin: false
};
module.exports = function(state = initialState, action) {
/* Keep the reducer clean - do not mutate the original state. */
//let nextState = Object.assign({}, state);
switch(action.type) {
case 'SET_USER_INFO': {
console.log(action.paras)
let nextState = Object.assign({}, state);
nextState[action.paras.infoType] = action.paras.data;
return nextState;
} break;
case 'USER_LOGIN': {
// Modify next state depending on the action and return it
let nextState = Object.assign({}, state);
nextState.userHadLogin = true;
return nextState;
} break;
case 'USER_LOGOUT': {
// Modify next state depending on the action and return it
let nextState = Object.assign({}, initialState);
return nextState;
} break;
default: {
/* Return original state if no actions were consumed. */
return state;
}
}
}
try binding binding the handler with "this"
<RaisedButton label={this.props.userPaperText} onMouseUp={ this.handleClickBtn.bind(this) } />