trigger mapDispatchToProps but it does not change props [duplicate] - reactjs

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

Related

redux useSelector: component is not reactive to state updates

I am setting up a very basic react typescript and redux app.
I used useSelector() to retrieve state then use it in my component.
however when I dipatch to the store adding a new article the ui doesn't change, I checked redux dev tools and the store is updated, I read that useSelector automatically subscribes to store so I'm not sure why I'm having this problem.
my App component code:
function App() {
const dispatch: Dispatch<any> = useDispatch();
const articles: readonly IArticle[] = useSelector(
(state: ArticleState) => state.articles
);
const saveArticle = React.useCallback(
(article: IArticle) => dispatch(addArticle(article)),
[dispatch]
);
return (
<div className="App">
<header className="App-header">
<h1>My Articles</h1>
<AddArticle saveArticle={saveArticle} />
<ul>
{articles.map((article: IArticle) => (
<li>{article.title}</li>
))}
</ul>
</header>
</div>
);
}
export default App;
the addArticle ActionCreator
export function addArticle(article: IArticle) {
const action: ArticleAction = {
type: actionTypes.ADD_ARTICLE,
article,
};
return simulateHttpRequest(action);
}
The Reducer
const reducer = (
state: ArticleState = initialState,
action: ArticleAction
): ArticleState => {
switch (action.type) {
case actionTypes.ADD_ARTICLE:
const newState = {
...state,
};
const newArticle: IArticle = {
id: Math.random(), // not really unique
title: action.article.title,
body: action.article.body,
};
newState.articles.push(newArticle);
return newState;
case actionTypes.REMOVE_ARTICLE:
const newArticles = state.articles.filter(
(article) => article.id !== action.article.id
);
return {
...state,
articles: newArticles,
};
default:
return state;
}
};
export default reducer;
here's a screenshot I see that data is actually updating in the store
The line newState.articles.push(newArticle); is mutating the existing articles array. Your selector is then trying to read state.articles. Since it's the same reference as before, React-Redux assumes nothing has changed, and will not re-render:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
Please switch over to using Redux Toolkit for your store setup and reducer logic. Not only will it let you simplify your reducers by writing this kind of "mutating" logic and letting it create updates immutably, it effectively makes accidental mutations like this impossible.

Is it possible to submit form in React without redirect?

When I submit form I get direct to black page or it gets redirected something like this
?name="..."&type="..."
but I want to stay in same component and update the component with out getting redirected according to the state change.
Route Code:
import Home from './pages/Home';
import Products from './pages/products';
import { Route } from 'react-router-dom';
function App() {
return (
<div className="App">
<h1>App Page</h1>
<Route path="/">
<Home />
<Products />
</Route>
</div>
);
}
export default App;
Home Component:
import { connect } from 'react-redux';
import { addProduct } from '../actions/shop.Action';
const Home = ({ addProduct }) => {
let name;
let type;
const nameHandler = (e) => {
name = e.target.value;
};
const typeHandler = (e) => {
type = e.target.value;
};
const submitHandler = (e) => {
e.preventDefault();
addProduct({
productName: name,
productType: type,
});
};
return (
<form onSubmit={submitHandler}>
<h1>Add Product</h1>
<label>Product Name</label>
<input type="text" name="pname" onChange={nameHandler} />
<label>Product Type</label>
<input type="text" name="ptype" onChange={typeHandler} />
<button type="submit">Submit</button>
</form>
);
};
const mapDispatchToProps = (dispatch) => ({
addProduct: (product) => dispatch(addProduct(product)),
});
export default connect(null, mapDispatchToProps)(Home);
Product Component:
import {connect} from 'react-redux'
const Products = ({products}) =>{
console.log(products)
return(
<div>
{products.length?products:<p>No Products</p>}
</div>
)
}
const mapStateToProps = (state) =>({
products: state.shop
})
export default connect(mapStateToProps)(Products);
Reducer or INITIAL STATE:
import { addProductType } from '../actionTypes/shop.actionTypes';
const INIT = [];
const Shop = (state = INIT, action) => {
switch (action.type) {
case addProductType:
const products = [...state]
products.push(action.payload)
return [...state,...products]
default:
return state
}
};
export default Shop;
Here is the image
video link : https://streamable.com/nauewl
There were a couple of issues with your code. Here is the fixed version:
https://codesandbox.io/s/recursing-meitner-80qo0?file=/src/pages/Home.jsx
Your reducer was incorrect. You were using an object but expecting an array in your Products UI. Also, the initial state should have been an array.
Note: You can use an object if you want but isn't really necessary unless you are planning to have a huge list and add crud functionality to your application. Also, an object doesn't have a lenght property. You would've used Object.keys in that case.
import { addProductType } from "../actionTypes/shop.actionTypes";
const INIT = [];
const Shop = (state = INIT, action) => {
switch (action.type) {
case addProductType:
return [...state, action.payload];
default:
return state;
}
};
export default Shop;
The initial state should've been an array and upon every addition of an item, the previous state has to be merged with the currently added item and return an array.
In your product UI, you were not iterating through the products. You need map to do that. (You can use other methods but this is the most recommended way, also, you cannot use something like forEach because it doesn't return anything).
Correction:
import { connect } from "react-redux";
const Products = ({ products }) => {
return (
<div>
{products.length > 0
? products.map((product, index) => (
<div key={index}>
<span>{product.productName}</span>
<span>{product.productType}</span>
</div>
))
: "No products"}
</div>
);
};
const mapStateToProps = (state) => ({
products: state.shop
});
export default connect(mapStateToProps)(Products);
Note: For the key prop, it is always suggested to have a unique identifier for each list item. Index will work in this case since the current scope of the application doesn't deal with list manipulation.
And lastly, in your Home component, you were using local variables instead of state variables to capture input values. Which is fine, but, doesn't allow you to keep track of values and manipulate them for any other use cases like validation.

Child component doesn't update when parent updates

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
}
}

Component in Component, React redux

Is it possible to call component in component (Like inception)
Example
Content.jsx
class Content extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.dispatch(fetchNav(this.props.match.params.tab));
}
componentDidUpdate(prevProps) {
if(this.props.match.params.tab != prevProps.match.params.tab) {
this.props.dispatch(fetchNav(this.props.match.params.tab));
}
}
render() {
const {nav, match} = this.props;
let redirect = null;
return (
<div>
<ul>
{nav.some((item, key) => {
if (this.props.location.pathname != (match.url + item.path)) {
redirect = <Redirect to={(match.url + item.path)} />;
}
return true;
})}
{redirect}
{nav.map((item, key) => {
return <li key={key}>
<Link to={match.url + item.path}>{item.name}</Link>
</li>;
})}
<Switch>
{nav.map((item, key) => {
return <Route key={key} path={`${match.url}/list/:tab`} component={Content} />;
})}
</Switch>
</ul>
</div>
);
}
}
const mapStateToProps = (state, props) => {
const {fetchNav} = state;
const {
lastUpdated,
isFetching,
nav: nav
} = fetchNav[props.match.params.tab] || {
isFetching: true,
nav: []
};
return {
nav,
isFetching,
lastUpdated
}
};
export default connect(mapStateToProps)(withStyles(appStyle)(Content));
Actually when i do this, if my route match and call same "Content" component, it says : "this.props.dispatch is not a function"
Do you think i need to create a ContentContainer that manage connect() and pass a method via props to manage change ?
Many thanks for you answers
Regards,
Thomas.
You are clearly not mapping dispatch to your props in your connect.
See the docs for redux' connect method:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
You can map the dispatch function to your props like like this:
const mapDispatchToProps = (dispatch) => ({ dispatch });
connect(mapStateToProps, mapDispatchToProps)...
Do you think i need to create a ContentContainer that manage connect() and pass a method via props to manage change ?
You don't need a container. The separation of code between a container and a (view-) component is just a pattern and not required.
As a sidenote: I would recommend to use compose to combine your HOCs, see this or this.

Why render is not triggered even the props changed

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.

Resources