When I add a new product to the products array using the addQuantityButton function via the child component, the change to the products array isn't recognised in the child Product component. addQuantityButtonfunction in the parent component is correctly adding a new object to the array. This stopped working when I started using Redux and mapStateToProps. What am I doing wrong here?
Path: Parent
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [getNewProduct()]
};
this.addQuantityButton = this.addQuantityButton.bind(this);
}
addQuantityButton(product_id) {
let { products } = this.state;
products = products.map((product) => {
if (product.product_id === product_id) {
product.quantities.push(getNewQuantity());
return product;
}
return product;
});
this.setState({
products
});
}
render() {
const { products } = this.state;
return (
<form>
{products.map((product) => (
<Product
key={product.key}
product_id={product.product_id}
quantities={product.quantities}
addQuantityButton={this.addQuantityButton}
/>
))}
</form>
);
}
}
Path: Product
class Product extends React.Component {
constructor(props) {
super(props);
this.state = {
unitOptions: []
};
}
render() {
return (
<div>
<div>
{this.props.quantities.map((quantity) => (
<p>Example</p>
))}
</div>
<button
type="button"
onClick={() => addQuantityButton(this.props.product_id)}
>
Add quanity
</button>
</div>
);
}
}
Product.propTypes = {
product_id: PropTypes.string,
quantities: PropTypes.array,
addQuantityButton: PropTypes.func
};
const mapStateToProps = (state) => ({
supplierProducts: state.product.products
});
export default connect(mapStateToProps)(Product);
child is listening to redux's state, which is different from parent's state. react component state is one thing, redux state is other. copying redux's state to a component's state is not advisable, you are duplicating state.
I would suggest at Parent's to map only your products to props, then iterate at your form as this.props.products.map(...
while at Children you declare a mapDispatchToProps responsible to increment the quantity. there you declare your addQuantityButton with some refactors. you will use dispatch instead which receives an action. the logic to add product will be implemented at your reducer down the road.
const mapDispatchToProps = (dispatch, ownProps) => ({
addQuantityButton: dispatch(addQuantity(ownProps.product_id))
})
your action is a simple function declared at some actions file, that return an object containing the action TYPE and a payload (you could call the payload something else fwiw):
const addQuantity = product_id => ({
type: 'ADD_QUANTITY',
payload: product_id
})
now, dispatch with a proper action will pass down the object to reducers, and a given reducer that intercepts ADD_QUANTITY will be responsible to increment quantity, and that way return next redux state.
at reducer you implement the logic to update state.
function productsReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_QUANTITY': // suggestion, declare your types as a constant in another file
// also, dont mutate state directly!, you may need to use some deep clone given it's an array of objects
return // logic here with action.payload and state.products
default:
return state
}
}
Related
I am trying to display my state (users) in my react/redux functional component:
const Dumb = ({ users }) => {
console.log('users', users)
return (
<div>
<ul>
{users.map(user => <li>user</li>)}
</ul>
</div>
)
}
const data = state => ({
users: state
})
connect(data, null)(Dumb)
Dumb is used in a container component. The users.map statement has an issue but I thought that the data was injected through the connect statement? the reducer has an initial state with 1 name in it:
const users = (state = ['Jack'], action) => {
switch (action.type) {
case 'RECEIVED_DATA':
return action.data
default:
return state
}
}
CodeSandbox
You aren't using the connected component while rendering and hence the props aren't available in the component
const ConnectedDumb = connect(
data,
null
)(Dumb);
class Container extends React.Component {
render() {
return (
<div>
<ConnectedDumb />
</div>
);
}
}
Working demo
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'm stating to learn react and redux so i think there are many things that i don't know.
I have a problem with missed re-rendering component on store changes.
This is my project structure: https://i.stack.imgur.com/tJJSg.png
And here is my code:
App.js:
class App extends Component {
render() {
return (
<div className="App">
<Nav sortByDate={()=>{this.props.sortBy(SORT_BY_DATE)}} sortByLikes={()=>{this.props.sortBy(SORT_BY_LIKES)}} />
<Items comments={this.props.comments} getList={()=>{this.props.sortBy(GET_LIST)}}/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
comments: state.comments
};
};
const mapDispatchToProps = (dispatch) => {
return {
sortBy: (action) => {
dispatch(sortBy(action));
}
};
};
export default connect (mapStateToProps, mapDispatchToProps) (App);
CommentList.js:
class ListItems extends Component {
constructor(props){
super(props);
this.state = {
comments: props.comments
};
}
componentWillMount() {
this.props.getList();
}
componentWillReceiveProps(nextProps) {
if (this.props.comments !== nextProps.comments) {
this.setState({
comments: nextProps.comments
});
}
}
getComments() {
return (this.state.comments.map(function (object) {
return <Item numLikes={object.num_like} id={object.id} comment={object.comment} date={object.date}
sender={object.sender}/>
}));
}
render() {
return (
<Container>
<Row>
<Col lg={2} md={1} xs={0}/>
<Col lg={8} md={10} xs={12}>
{this.getComments()}
</Col>
<Col lg={2} md={16} xs={0}/>
</Row>
</Container>
);
}
}
export default ListItems;
Reducers.js:
const listReducer = (state = {comments: []}, action) => {
function toDate(dateStr) {
const [day, month, year] = dateStr.split("/")
return new Date(year, month - 1, day)
}
function commentSortedByDate(comments) {
const sorted = comments.sort(function(a, b) {
return toDate(b.date) - toDate(a.date);
})
return sorted;
}
function commentSortedByLikes(comments) {
const sorted = comments.sort(function(a, b) {
return parseInt(b.num_like) - parseInt(a.num_like);
})
return sorted;
}
switch (action.type) {
case SORT_BY_DATE:
console.log("sort by date");
state={
comments: commentSortedByDate(state.comments)
}
break;
case SORT_BY_LIKES:
console.log("sort by likes");
state={
comments: commentSortedByLikes(state.comments)
}
break;
case GET_LIST:
state = {
comments: action.payload
}
break;
}
return state;
};
export default listReducer;
The problem is certainly with this two components.
I have 3 actions:
GET_LIST (in a middleware call a rest service getting the json of comments and update the store).
SORT_BY_DATE (in the reducer sort the array of comments by date and update the store).
SORT_BY_LIKES (same).
The comments in the store are effectively sorted.
First of all the app dispatch automatically the GET_LIST action and it works, pass the props with comments correctly to the CommentList.js component and successfully render the list of CommentItem.
Now the problem:
The click of a button in the Navbar component will dispatch a SORT_BY action that updates the store and finally calls the MapStateToProps function in App, but this time the CommentList stay the same and componentWillReceiveProps is not called.
Why? Can anyone help me?
You are mutating the state (sort function) instead of creating a new Array in your reducer. This prevents the component from re-rendering as it is not notified of a change. To fix it you could make your functions pure:
function commentSortedByDate(comments) {
const copy = [...comments];
copy.sort(function(a, b) {
return toDate(b.date) - toDate(a.date);
})
return copy;
}
function commentSortedByLikes(comments) {
const copy = [...comments];
copy.sort(function(a, b) {
return parseInt(b.num_like) - parseInt(a.num_like);
})
return copy;
}
This way you return a new array instead of old one (with sorted elements).
Consider the following code. It's supposed to work like this; keystrokes are supposed to update the local state and once the button is clicked, the global state should be updated. And the component itself is responsible to show the global state as well.
class App extends React.Component {
constructor(props) {
super(props);
this.state = { name: this.props.appState.name, localName: "" };
this.nameChanged = this.nameChanged.bind(this);
this.sendAction = this.sendAction.bind(this);
}
nameChanged(event) {
this.setState(Object.assign({}, this.state, { localName: event.target.value }));
}
sendAction(event) {
this.props.saveName(this.state.localName);
}
render() {
return (
<div>
<h1>Hello, {this.state.name}!</h1>
<input type="text" value={this.state.localName} onChange={this.nameChanged} />
<input type="button" value="Click me!" onClick={this.sendAction} />
</div>
);
}
}
const AppContainer = ReactRedux.connect(
state => ({ appState: state.appReducer.appState }),
dispatch => Redux.bindActionCreators({
saveName: (name) => ({ type: "SAVE_NAME", name })
}, dispatch)
)(App);
const appReducer = (state = { appState: { name: "World!" } }, action) => {
switch (action.type) {
case "SAVE_NAME":
return Object.assign({}, state, { name: action.name });
default:
return state;
}
};
const combinedReducers = Redux.combineReducers({
appReducer
});
const store = Redux.createStore(combinedReducers);
ReactDOM.render(
<ReactRedux.Provider store={store}>
<AppContainer />
</ReactRedux.Provider>,
document.getElementsByTagName('main')[0]
);
Right now, the local state is updated correctly. But when I click the button, even though the action is created and sent, but I don't see the global state with the new value in the UI. I'm not sure if the global state is not updated or the component is not properly informed.
Change <h1>Hello, {this.state.name}!</h1> to this:
<h1>Hello, {this.props.appState.name}!</h1>
You don't have to set the state locally, Redux is updating the props for you through connect when the store state changes. The other change you'll need to make is in your reducer. Your current state is pretty deeply nested and it's not being returned how you're expecting it to when you dispatch your action. Here's the updated version:
case "SAVE_NAME":
return {
...state,
appState: {
...state.appState,
name: action.name
}
}
Here's a working version in CodePen Link
I have been using react+redux quite while, but could you any one help me the following case, on codepen:
const {createStore } = Redux;
const { Provider, connect } = ReactRedux;
const store = createStore((state={name: 'ron'}, action) => {
switch(action.type) {
case 'changeName': return {name: action.name};
default: return state
}
})
const Person = props => {
const {name, dispatch} = props
console.log(`rendering Person due to name changed to ${name}`)
return (
<div>
<p> My name is {name} </p>
<button onClick={ () => dispatch({type: 'changeName', name: 'ron'}) } > Change to Ron </button>
<button onClick={ () => dispatch({type: 'changeName', name: 'john'}) } > Change to John</button>
</div>
)
}
const App = connect(state=>state)(Person)
ReactDOM.render(
<Provider store={store}><App/></Provider>,
document.getElementById('root')
);
It is simple react app, but I cannot explain:
Initialise redux store with one reducer, and its initValue is {name: 'ron'}
Click Change to ron button, it will dispatch {type: 'changeName', name: 'ron'}
When the reducer get this action, it will generate an brand new state {name: 'ron'}, though the value is same as the original state, but they are different identity and should be the different ones.
The functional component should be re-rendered if the props changed even though the values are the same. So I suppose the render function will be called, and console should output rendering Person due to.... However, it is not happening.
I am wondering why react functional component refuse to render again when the props identity are changed (though the values are the same)
Your connect(state=>state)(Person) I think it's not wrong but it's weird.
According to the documentation https://redux.js.org/docs/basics/UsageWithReact.html you can separate the state and the action dispatcher, commonly naming mapStateToProps and mapDispatchToProps.
So, I propose to you this code:
const mapStateToProps = state => ({
user: state.user
})
const mapDispatchToProps = dispatch => ({
updateName: (name) => dispatch(changeName(name)),
})
class DemoContainer extends Component {
constructor() {
super();
}
render() {
return (
<div>
<p> My name is {this.props.user.name}</p>
<button onClick={ () => this.props.updateName('ron') } > Change to Ron </button>
<button onClick={ () => this.props.updateName('john') } > Change to John</button>
</div>
);
}
}
const Demo = connect(
mapStateToProps,
mapDispatchToProps
)(DemoContainer)
export default Demo
My reducer:
const initialState = { name: 'John'}
const user = (state = initialState, action) => {
switch (action.type) {
case "CHANGE_NAME":
return {
name: action.name
}
default:
return state
}
}
export default user
My action:
export const changeName = ( name ) => ({
type: "CHANGE_NAME",
name,
})
You can check all my code here: https://stackblitz.com/edit/react-tchqrg
I have a class for the component but you can also use a functional component with connect like you do.