I have built a fairly large react-redux application. In one component I have added an undo feature. Tracing the state all the way through it is definitely being updated and not mutated. It even re-renders the component and all child components. However on the page the component position is not modified until I click the component to either move it again or highlight it.
I have definitely verified that this is not a case of mutated state and have stepped through all the redux code to ensure that the shallow equality fails and I have added breakpoints on the main component and the child component which should be moved.
I will add code if you want to see it but my question is why a re-rendered component in React would not re-render in the updated position on the screen, even though the top and left coordinates have definitely changed?
Edit adding code
//layout.js
const mapLayoutDispatchToProps = (dispatch) => {
//add action creators here - by reference?
return {
Layout_Set_Current_Site: (siteId) => { dispatch(Layout_Set_Current_Site(siteId)) },
Layout_Get_Sites: () => { dispatch(Layout_Get_Sites()) },
Layout_Get_Map_Background: (siteId, callback) => { dispatch(Layout_Get_Map_Background(siteId, callback)) },
Layout_Get_UserImages: (deskId) => { dispatch(Layout_Get_UserImages(deskId)) },
Layout_Create_Desk: (type, siteId, height, width) => { dispatch(Layout_Create_Desk(type, siteId, height, width)) },
Layout_Restore_All: () => { dispatch(Layout_Restore_All()) },
Layout_Undo_Change: (callback) => { dispatch(Layout_Undo_Change(callback)) },
Layout_Redo_Change: () => { dispatch(Layout_Redo_Change()) },
Layout_Fetch_Desks: (siteId) => { dispatch(Layout_Fetch_Desks(siteId)) },
Layout_Get_Desk_Types: () => { dispatch(Layout_Get_Desk_Types()) },
Layout_Set_Current_Desk: (desk) => { dispatch(Layout_Set_Current_Desk(desk)) }
};
}
getDesks = () => {
console.log("Layout.getDesks");
// const d = this.props.layout_moveData.desks.present;
const desks = clone(this.props.layout_moveData.present);
return desks;
}
handleKeyPress = (e) => {
console.log("Layout.handleKeyPress");
if (this.state.edit) {
switch (e.code) {
case 'KeyZ':
if (e.ctrlKey) {
this.props.Layout_Undo_Change();
e.cancelBubble = true;
// this.forceUpdate();
}
break;
case 'KeyY':
if (e.ctrlKey) {
//this.props.Layout_Redo_Change();
UndoMove.redo();
e.cancelBubble = true;
}
break;
default:
break;
}
}
}
buildDesks = () => {
const desks = this.getDesks();
let ret = desks.map((desk, index) =>
<Desk
key={index}
desks={desks}
index={index}
deskTypes={this.props.layout.deskTypes}
scale={this.getScale()}
editable={this.state.edit}
/>
);
return ret;
}
render=()=>{
return (
<div>
<Row>
<Col sm={1}>
{this.showAdmin()}
</Col>
<Col sm={10}>
{this.state.details}
</Col>
</Row>
<Row>
<Col sm={1}>
<select onChange={(e) => this.changeMap(e.target)}>
{this.buildMapOptions()}
</select>
</Col>
<Col sm={10} id="Layout_Map_Container">
{this.buildMap()}
{this.buildDesks()}
</Col>
</Row >
{this.showStatus()}
</div>
);
}
}
//desks.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Draggable from '../../Elements/Draggable';
import {
Layout_Clear_Desk,
Layout_Delete_Desk,
Layout_Update_Desk_Data,
Layout_Set_Current_Desk
} from '../../../redux/Creators/Layout_Creator';
import '../../../shared/styles/layout.css';
const clone = require('rfdc')();
const mapDesksStateToProps = (state) => {
return {
layout: state.layout,
layout_moveData: state.layout_moveData
}
}
const mapDesksDispatchToProps = (dispatch) => {
return {
Layout_Clear_Desk: (deskId) => { dispatch(Layout_Clear_Desk(deskId)) },
Layout_Delete_Desk: (deskId) => { dispatch(Layout_Delete_Desk(deskId)) },
Layout_Update_Desk_Data: (desk, deskId) => { dispatch(Layout_Update_Desk_Data(desk, deskId)) },
Layout_Set_Current_Desk: (deskId) => { dispatch(Layout_Set_Current_Desk(deskId)) }
}
}
class Desk extends Component {
constructor(props) {
super(props);
this.state = {
desk: clone(props.desks[props.index]),
desks: clone(props.desks)
}
}
rightClick = (e, deskId) => {
if (this.props.editable) {
const desk = this.state.desk;
let rotation = parseInt(desk.rotation);
rotation += 90;
if (rotation >= 360) rotation -= 360;
desk.rotation = rotation;
this.props.Layout_Set_Current_Desk(desk);
this.props.Layout_Update_Desk_Data(desk);
}
}
updateProperties = (data) => {
let string = `Top: ${data.top}, Left:${data.left}`;
// data = this.state.details + ', ' + data
this.setState({ details: string });
}
mouseUp = (e, deskId, data) => {
console.log("Layout.mouseUp");
const desks = clone(this.state.desks);
// let desk = ;
if (data.dragged && this.props.editable) {
// this.clickDesk(e, deskId);
const scale = this.props.scale;
const newX = parseInt(data.left / scale);
const newY = parseInt(data.top / scale);
desks[deskId].x = newX + ""; //convert to strings
desks[deskId].y = newY + "";
console.log(this.state.desks);
console.log(desks);
this.props.Layout_Update_Desk_Data(desks, deskId);
}
else {
this.clickDesk(e, deskId);
}
}
clickDesk = (e, deskId) => {
if (deskId !== null && deskId !== undefined && deskId !== false) {
this.props.Layout_Set_Current_Desk(this.state.desk);
}
else {
this.props.Layout_Set_Current_Desk(null);
}
}
render() {
let deskImg = null;
// const desk = this.props.desks[this.props.index];
let desk = clone(this.state.desk);
try {
let dImg = this.props.deskTypes.find(
d => parseInt(d.deskType) === parseInt(desk.deskType)
);
deskImg = dImg.deskImage;
}
catch (ex) {
console.log(ex);
}
const userName = desk.UserLogon !== (null || '') ? desk.UserLogon : "Unassigned";
const top = Math.trunc(parseInt(parseInt(desk.y) * this.props.scale));
const left = Math.trunc(parseInt(parseInt(desk.x) * this.props.scale));
let imgStyle = {
width: `${parseInt(parseInt(desk.width) * this.props.scale)}px`,
height: `${parseInt((parseInt(desk.height) * this.props.scale))}px`,
position: 'absolute'
}
if (this.props.layout.currentDesk && desk.id === this.props.layout.currentDesk.id) {
imgStyle.border = '2px solid cyan';
}
const url = `data:image/jpeg;base64,${deskImg}`;
try {
//
return (
<Draggable key={desk.id}
index={this.props.index}
enabled={this.props.editable}
left={left}
top={top}
onMove={this.updateProperties}
onStop={this.mouseUp}
onRightClick={this.rightClick}
>
<div style={{
position: 'relative',
transform: `rotate(${parseInt(desk.rotation)}deg)`
}}
className='deskImg'>
<img style={imgStyle} alt={userName} src={url} />
</div>
</Draggable>
);
}
catch (ex) {
console.log(ex);
return null;
}
}//buildDesks
}
export default connect(mapDesksStateToProps, mapDesksDispatchToProps)(Desk);
//layout_creators.js
export const Layout_Undo_Change = () => (dispatch, getState) => {
const state = getState();
const desks = clone(state.layout_moveData);
console.log("1", state.layout_moveData.present)
//if no past to undo to
if (desks.past.length === 0) return;
const previous = clone(desks.past[desks.past.length - 1]);
desks.past.shift();
const undoPast = clone(desks.past);
// const undoPast = clone(desks.past).slice(0, desks.past.length - 1);
const undoFuture = [clone(desks.present), ...clone(desks.future)]
const undoDesks = { past: undoPast, present: previous, future: undoFuture };
dispatch({ type: ActionTypes.LAYOUT_UNDO_MOVES, payload: clone(undoDesks) });
console.log("2", state.layout_moveData.present)
let init = fetchInit();
init.method = "POST";
const deskData = { mode: 'UPDATEMANY', data: previous };
init.body = JSON.stringify(deskData);
let myReq = new Request(`/dataAPI/Layout/`, init);
fetch(myReq)
.then((response) => {
if (response.ok) {
return response;
}
else {
var error = new Error("Error " + response.statusText);
error.response = response;
throw error;
}
}, (error) => {
var err = new Error(error.message);
throw err;
})
.catch(err => {
return dispatch({
type: ActionTypes.LAYOUT_FAILED,
payload: err.message
});
});
}
//layout_reducer.js
import * as ActionTypes from '../ActionTypes';
export const layout = (state = {
isLoading: true,
isLoadingMap: false,
isLoadingDesks: false,
desksLoaded: false,
mapLoaded: false,
currentMap: null,
currentDesk: null,
maps: [],
deskTypes: [],
editMode: false,
errMess: null
}, action) => {
switch (action.type) {
case ActionTypes.LAYOUT_SITES_LOADING:
return { ...state, isLoading: true };
case ActionTypes.LAYOUT_DESKS_LOADING:
return { ...state, isLoadingDesks: true, desksLoaded: false };
case ActionTypes.LAYOUT_MAP_LOADING:
return {
...state, isLoadingMap: true, desks: [],
currentDesk: null, editMode: false, desksLoaded: false
};
case ActionTypes.LAYOUT_MAP_LOADED:
return { ...state, isLoadingMap: false, mapLoaded: true, maps: action.payload };
case ActionTypes.LAYOUT_MAPS_LOADED:
return { ...state, maps: action.payload, isLoading: false };
case ActionTypes.LAYOUT_DESKTYPES_LOADED:
return { ...state, deskTypes: action.payload };
case ActionTypes.LAYOUT_SET_CURRENT_DESK:
return { ...state, currentDesk: action.payload };
case ActionTypes.LAYOUT_SET_EDITMODE:
return { ...state, editMode: action.payload };
case ActionTypes.LAYOUT_DESK_DELETED:
return { ...state, currentDesk: null }
case ActionTypes.LAYOUT_DESKS_LOADED:
return { ...state, currentDesk: null, isLoadingDesks: false, desksLoaded: true }
case ActionTypes.LAYOUT_SET_ACTIVE_MAP:
return { ...state, currentMap: action.payload, desksLoaded: false };
case ActionTypes.LAYOUT_FAILED:
return {
...state, isLoadingMap: false, isLoadingDesks: false, desksLoaded: false,
errMess: action.payload, pageUsageData: []
};
case ActionTypes.LAYOUT_RESTORE_ALL:
return {
...state,
isLoading: true, isLoadingMap: false, mapLoaded: false, currentMap: null,
maps: [], desks: [], deskTypes: [], editMode: false,
errMess: null, desksLoaded: false
}
default:
return state;
}
}
export const layout_moveData = (state = {
past: [],
present: null,
future: []
}, action) => {
switch (action.type) {
case ActionTypes.LAYOUT_DESKS_LOADING:
return { ...state, present: [], past: [], future: [] };
case ActionTypes.LAYOUT_DESKS_LOADED:
return { ...state, present: action.payload, past: [], future: [] };
case ActionTypes.LAYOUT_DESK_DELETED:
return { ...state, present: action.payload.present, past: action.payload.past, future: action.payload.future };
case ActionTypes.LAYOUT_RESTORE_ALL:
return { ...state, present: [], past: [], future: [] };
case ActionTypes.LAYOUT_SET_MOVES:
return { ...state, present: action.payload.present, past: action.payload.past, future: action.payload.future };
case ActionTypes.LAYOUT_UNDO_MOVES:
return { ...state, present: action.payload.present, past: action.payload.past, future: action.payload.future };
case ActionTypes.LAYOUT_REDO_MOVES:
return { ...state, present: action.payload.present, past: action.payload.past, future: action.payload.future };
default:
return state
}
}
I have extrapolated all items in the page to separate components as in React redux state change does not cause update even after deep copy of all state data. This allowed for better stack handling.
Related
When I try and destructure my data using useSelector, I get the error backgroundImage undefined.
I am setting my initial state in my reducer. How can I stop the error occurring?
component:
export const SampleScreen = (props) => {
const { navigation } = props;
const {
rescueMe: {
rescueMeSplashScreen: {
backgroundImage, // error undefined
buttonText,
informationText,
title: pageTitle,
},
},
} = useSelector((state) => state);
return (
<>
</>
);
};
Reducer:
const RescueMeReducer = (
state = {
rescueMeSplashScreen: {
backgroundImage: "",
buttonText: "",
informationText: "",
title: "",
},
lastKnownPosition: undefined,
error: undefined,
loading: false,
},
action
) => {
switch (action.type) {
...
case RESCUE_ME_CONTENT: {
return {
...state,
error: undefined,
rescueMeSplashScreen: undefined,
loading: true,
};
}
case RESCUE_ME_CONTENT_SUCCESS: {
return {
...state,
rescueMeSplashScreen: action.payload,
loading: false,
};
}
case RESCUE_ME_CONTENT_FAILED: {
return {
...state,
rescueMeSplashScreen: undefined,
error: {
errorMessage: action.payload,
},
loading: false,
};
}
default:
return state;
}
};
export default RescueMeReducer;
I am using Redux for state management, I have faced an issue in reducer function
here is the image of my console, You can see the Product Action is providing my data but the reducer is not passing on my function
here is my code of ProductAction:
export const getProductsbyFind = (myvariable) =>async (dispatch)=>{
try {
console.log(myvariable)
dispatch({type: ALL_PRODUCTS_REQUEST_BY_ID})
const{ data } = await axios.get(`/api/v1/product/${myvariable}`)
console.log(data)
dispatch({
type: ALL_PRODUCTS_SUCCESS_BY_ID,
payload: data
})
} catch (error) {
dispatch({
type:ALL_PRODUCTS_FAIL,
payload: error.response.data.message
})
}
}
here is the code of Reducer:
export const productReducersById = (state = { products: [] }, action) => {
switch (action.type) {
case ALL_PRODUCTS_REQUEST_BY_ID:
return {
loading: true,
products: []
}
case ALL_PRODUCTS_SUCCESS_BY_ID:
return {
loading: false,
products: action.payload.products,
productsCount: action.payload.productsCount
}
case UPDATE_QUANTITY_BY_ID:
const { index, quantity } = action.payload;
const prods = state.products.map((p, i) => {
if (i !== index)
return p;
return {
...p,
quantity
}
});
return {
loading: true,
products: prods
}
case ALL_PRODUCTS_FAIL_BY_ID:
return {
loading: false,
error: action.payload
}
case CLEAR_ERRORS_BY_ID:
return {
...state,
error: null
}
default:
return state
}
}
here is the code of my page where I want to get my data:
const { loading, products, error, productCount } = useSelector(state => state.products);
console.log(products)
useEffect(() => {
dispatch(getProductsbyFind(myvariable));
}, [dispatch])
You have a typo in your reducer:
case ALL_PRODUCTS_SUCCESS_BY_ID:
return {
loading: false,
- products: action.payload.products,
+ products: action.payload.product,
productsCount: action.payload.productsCount
}
(Also, productsCount does not exist in your payload, so that will become undefined.)
Store changes not immediately visible to component due to this error message not showing in component whenever request get failed. From reducer, state update take some time to return the update value to component. Hence, component always return as empty msg which is default value present in reducer
Api.js
export const createCategory = async (category, authtoken) => {
return await axios.post(
`${process.env.REACT_APP_API}/category/create`,
category,
{
headers: {
authtoken,
},
}
);
};
category.saga.js
export function* createCategoryAsync({ payload: { name, token } }) {
try {
yield delay(1000);
const response = yield call(createCategory, { name }, token);
yield delay(1000);
console.log("===response", response);
if (response.status === 200 && response.status < 300) {
yield put(createCategorySuccess(response.data.name));
}
console.log("===response", response);
} catch (error) {
yield put(createCategoryFail(error.response.data));
}
}
category.reducer.js
import CategoryActionTypes from "./category.types";
const INITIAL_STATE = {
categoryName: "",
categories: [],
error: false,
errorMsg: "",
};
const categoryReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case CategoryActionTypes.LOAD_CATEGORY_START:
return {
...state,
loading: true,
};
case CategoryActionTypes.LOAD_CATEGORY_SUCCESS:
return {
...state,
loading: false,
categories: action.payload,
};
case CategoryActionTypes.SET_CATEGORY_EMPTY:
return {
...state,
categoryName: "",
};
case CategoryActionTypes.CREATE_CATEGORY_START:
return {
...state,
loading: true,
};
case CategoryActionTypes.CREATE_CATEGORY_SUCCESS:
return {
...state,
loading: false,
categoryName: action.payload,
};
case CategoryActionTypes.LOAD_CATEGORY_FAIL:
case CategoryActionTypes.CREATE_CATEGORY_FAIL:
return {
...state,
loading: false,
error: true,
errorMsg: action.payload,
};
default:
return state;
}
};
export default categoryReducer;
Component.js
const Component = () => {
useEffect(() => {
loadCateories();
}, []);
const { categories, loading, categoryName, error, errorMsg } = useSelector(
(state) => ({
...state.category,
})
);
const loadCateories = () => {
dispatch(loadCategoryStart());
};
console.log("==errorMsg", errorMsg);
const {
user: { token },
} = useSelector((state) => ({ ...state }));
const handleSubmit = (e) => {
e.preventDefault();
// setLoading(true);
// dispatch(setCategoryEmpty());
dispatch(createCategoryStart({ name, token }));
if (categoryName) {
toast.success(`${name} is created`);
setName("");
loadCateories();
} else {
toast.error(errorMsg && errorMsg);
setName("");
}
};
}
I have a component which when using CTRL+Z should trigger an undo action. Tracing the code through it is obvious that the state is updated correctly and that the arrays in it are not being mutated. However the component is not rerendered until I click on it, which causes a highlight to occur. At this point the component jumps to its previous location. I have attempted using forceUpdate() after the undo action is dispatched but that did not succeed either.
My reducer is a single line returning state and the new object as the action.payload. My action creator reads the original data, clones everything (some of them multiple times in a 'swing wildly' attempt to solve this) and then dispatches the undo action and data to the reducer.
Stepping through the code and comparing values shows me that everything seems correct so I cannot see where the issue is.
Here is my action creator:
export const Layout_Undo_Change = (callback) => (dispatch, getState) => {
const state = getState();
const desks = state.layout_moveData.desks;
//if no past to undo to
if (desks.past.length === 0) return;
const previous = clone(desks.past)[desks.past.length - 1];
const undoPast = clone(desks.past.slice(0, desks.past.length - 1));
const undoFuture = clone([desks.present, ...clone(desks.future)])
const undoDesks = { past: undoPast, future: undoFuture, present: previous };
dispatch({ type: ActionTypes.LAYOUT_UNDO_MOVES, payload: undoDesks });
// callback();
}
and here is the reducer:
export const layout_moveData = (state = {
desks: {
past: [],
present: null,
future: []
}
}, action) => {
switch (action.type) {
case ActionTypes.LAYOUT_DESKS_LOADING:
return { ...state, desks: { present: [], past: [], future: [] } };
case ActionTypes.LAYOUT_DESKS_LOADED:
return { ...state, desks: { present: action.payload, past: [], future: [] } };
case ActionTypes.LAYOUT_DESK_DELETED:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_RESTORE_ALL:
return { ...state, desks: { present: [], past: [], future: [] } };
case ActionTypes.LAYOUT_SET_MOVES:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_UNDO_MOVES:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_REDO_MOVES:
return { ...state, desks: action.payload };
default:
return state
}
}
and finally here is the calling line from the component:
handleKeyPress = (e) => {
console.log("Layout.handleKeyPress");
if (this.state.edit) {
switch (e.code) {
case 'KeyZ':
if (e.ctrlKey) {
this.props.Layout_Undo_Change(this.forceUpdate);
e.cancelBubble = true;
this.forceUpdate();
}
break;
case 'KeyY':
if (e.ctrlKey) {
//this.props.Layout_Redo_Change();
UndoMove.redo();
e.cancelBubble = true;
}
break;
default:
break;
}
}
}
Edit - adding mapState code
mapDispatchToProps code:
const mapDispatchToProps = (dispatch) => {
//add action creators here - by reference?
return {
Layout_Set_Current_Site: (siteId) => { dispatch(Layout_Set_Current_Site(siteId)) },
Layout_Get_Sites: () => { dispatch(Layout_Get_Sites()) },
Layout_Get_Map_Background: (siteId, callback) => { dispatch(Layout_Get_Map_Background(siteId, callback)) },
Layout_Get_Desk_Types: () => { dispatch(Layout_Get_Desk_Types()) },
Layout_Fetch_Desks: (siteId) => { dispatch(Layout_Fetch_Desks(siteId)) },
Layout_Undo_Change: (callback) => { dispatch(Layout_Undo_Change(callback)) },
Layout_Redo_Change: () => { dispatch(Layout_Redo_Change()) },
Layout_Clear_Desk: (deskId) => { dispatch(Layout_Clear_Desk(deskId)) },
Layout_Delete_Desk: (deskId) => { dispatch(Layout_Delete_Desk(deskId)) },
Layout_Update_Desk_Data: (desk, deskId) => { dispatch(Layout_Update_Desk_Data(desk, deskId)) },
Layout_Get_UserImages: (deskId) => { dispatch(Layout_Get_UserImages(deskId)) },
Layout_Create_Desk: (type, siteId, height, width) => { dispatch(Layout_Create_Desk(type, siteId, height, width)) },
Layout_Restore_All: () => { dispatch(Layout_Restore_All()) },
Layout_Set_Current_Desk: (deskId) => { dispatch(Layout_Set_Current_Desk(deskId)) }
};
}
mapStateToProps code:
const mapStateToProps = (state) => {
return {
layout: state.layout,
layout_moveData: state.layout_moveData,
roles: state.siteMap.siteMapData.userRoles
}
}
Any help to point me in the correct direction would be awesome.
Extrapolated all components and items to separate classes to better handle individual state changes rather than dealing with everything from top down
Would you mind helping me to be clear about mapDispatchToProps.
I have a example code like this:
// ----------------------- not use mapDispatchToProps -----------------------------
//var onSubmit = (event) => {
// event.preventDefault()
// var email = event.target.elements[0].value
// var password = event.target.elements[1].value
// // const path = `/repos/${userName}/${repo}`
// store.dispatch(action.requestLogin({username:email,password:password}))
// // store.dispatch(action.receiveLogin({user{username:email,password:password,objectId:1,sessionToken:"asdfg"}}))
// }
// ----------------------- use mapDispatchToProps -----------------------------
const mapDispatchToProps = (dispatch) => {
return {
onSubmit: (event) => {
event.preventDefault()
var email = event.target.elements[0].value
var password = event.target.elements[1].value
dispatch(action.requestLogin({username:email,password:password}))
}
}
}
const mapStateToProps = state => ({
// onSubmit: onSubmit,
error: state.login.error
});
var LoginPage = ({ onSubmit,error }) => {
return (
`<div className="row">
<div className="col-md-12">
<LoginFormComponent className="account-form text-center" title="Log in to Portal" error={error !== null ? error : ""} onSubmit={onSubmit}/>
</div>
</div>`
)
}
export default connect(mapStateToProps,mapDispatchToProps)(LoginPage)
//-----------------------------and this is the reducer -------------------------------------
export default function login(state = {
logedAt: null,
isLogging: false,
error: null,
data: {},
}, action) {
switch (action.type) {
case types.LOGIN_REQUEST:
return update(state, {
isLogging: { $set: true },
error: { $set: null }
});
case types.LOGIN_SUCCESS:
return update(state, {
data: { $set: action.body },
isLogging: { $set: false },
logedAt: { $set: action.logedAt },
});
case types.LOGIN_FAILURE:
return update(state, {
logedAt: { $set: null },
error: { $set: action.error },
});
default:
return state;
}
}
//-----------------------------and the middleware -------------------------------------
export function login({dispatch, getState}){
return next => action => {
return callLogin().then(
response => dispatch(Object.assign({},{
body: response,
logedAt: Date.now(),
type: LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true,
// callLogin: callLogin,
})),
error => dispatch(Object.assign({} ,{
error: error.response.text,
type: LOGIN_FAILURE,
isFetching: false,
isAuthenticated: false,
// callLogin: callLogin,
}))
);
}
}
When I don't use the mapDispatchToProps, I just can dispatch the action for type:LOGIN_REQUEST but not the LOGIN_SUCCESS,LOGIN_FAILURE, when use mapDispatchToProps, it work. could you explain for me
Thanks a lot.