I'm a little confused on passing an object to the redux store. I have successfully created the store and can add items from the initial state. The function also fires when called
Action:
import { GET_ITEM } from './OrderTypes'
export const getItem = (payload) => {
return {
type: GET_ITEM,
payload: { payload }
}
}
Reducer:
import { GET_ITEM } from './OrderTypes'
const initialState = {
orderList: [],
}
const orderReducer = (state = initialState, action) => {
switch (action.type) {
case GET_ITEM: return {
...state,
orderList: [...state.orderList, action.payload]
}
default: return state
}
}
export default orderReducer
Component:
class TestComponentextends Component {
pushItem = () => {
this.props.getItem({
payload: 'test object'
})
}
render() {
return (
<input type='button' value='test btn' onClick={this.pushItem} />
)
}
}
const mapStateToProps = state => {
return {
orderList: state.orderList
}
}
const mapDispatchToProps = dispatch => {
return {
getItem: () => dispatch(getItem())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TestComponent)
What happens: An empty object is added to the orderList array.
What I want to happen: Store the object in pushItem in the orderList array.
Your mapDispatchToProps doesn't pass the arguments to the action creator (see mapDispatchToProps function arguments - 2nd example):
const mapDispatchToProps = dispatch => ({
getItem: (...args) => dispatch(getItem(...args))
})
Even easier is to let react-redux handle the mapping by using mapDispatchToProps as an object:
const mapDispatchToProps = {
getItem
}
Related
I am facing an issue in my code base so I have made a sample code to demonstrate the issue.
link for the codesandbox code
App.js
import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux';
import { handleDataInit, handlePageChange, handleDataAdded } from './appDataAction';
import First from './First';
import Second from './Second';
import { reduxStore } from "./store";
class App extends Component {
handleChange = (pageNumber, pageTitle) => {
let data = {
val1: "val1",
val2: "val2",
val3: "val3"
}
this.props.handleDataAdded(data);
console.log("app Data", this.props.appData);
console.log('app data in redux store ', reduxStore.getState().appData);
this.props.handlePageChange({ pageNumber, pageTitle });
}
render() {
return (
<div>
<button onClick={() => this.handleChange(1, "first_page")}>1</button>
<button onClick={() => this.handleChange(2, "second_page")}>2</button>
{
this.props.appData.pageNumber === 1 ?
<First />
:
<Second />
}
</div>
);
}
}
const mapStateToProps = (state) => {
console.log('map state to props state value is ', state);
return ({
appData: state && state.appData
})
}
const mapDispatchToProps = (dispatch) => {
return ({
handleDataInit: (data) => dispatch(handleDataInit(data)),
handlePageChange: (newPage) => dispatch(handlePageChange(newPage)),
handleDataAdded: (data) => dispatch(handleDataAdded(data))
})
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
screenshot for the two console.log
browser console log:
appDataAction.js
export const handleDataInit = (data) => {
return ({
type: "data_init",
payload: data
});
}
export const handlePageChange = (newPage) => {
return ({
type: "page_change",
payload: newPage
});
}
export const handleDataAdded = (data) => {
return ({
type: "data_added",
payload: data
});
}
appDataReducer.js
const initialState = {
pageNumber: 1,
pageTitle: "first_page",
}
export const appDataReducer = (state = initialState, action) => {
switch (action.type) {
case "data_init":
if (Object.keys(state).length > 2) {
return state
}
else {
let newState = Object.assign({}, state, action.payload);
// console.log("new state in init ", newState);
return newState;
}
case "page_change":
// console.log('action.payload', action.payload);
let newState2 = {
...state,
pageNumber: action.payload.pageNumber,
pageTitle: action.payload.pageTitle
}
// console.log('new state is ', newState2);
return newState2;
case "data_added":
let newState3 = Object.assign({}, state, action.payload);
// console.log("new state in data added ", newState3);
return newState3;
default:
return state;
}
}
From react-redux documentation
The first argument to a mapStateToProps function is the entire Redux store state (the same value returned by a call to store.getState()).
can somebody explain why there is difference in the two console's.
I have debugged and found out that after return from reducer mapStateToProps is called and it gets the updated value of state
then why is this.props.appData is not up to date in the handleChange function.
I believe it could be something related to dirty state but if it is proper for getState() in the function it should be for this.props.appData too.
The state Selectedlan don't update on the click.. I think it is an error of reducer. Specifically when I the function handleGreeceLan is called the action should be called and the reducer but I think only the action is called
The file that I make the call
import { changeLanGR, changeLanEN } from '../actions/toolbarActions';
const handleGreeceLan = () => {
changeLanGR();
};
const handleUKLan = () => {
changeLanEN();
};
<MenuItem onClick={handleUKLan}>
...
</MenuItem>
<MenuItem onClick={handleGreeceLan}>
...
</MenuItem>
...
const mapStateToProps = state => {
return { Selectedlan: state.Selectedlan };
};
export default connect(
mapStateToProps,
{ changeLanGR, changeLanEN }
)(Language);
Reducer
import { combineReducers } from 'redux';
import { LAN_GR, LAN_EN } from '../actions/types';
const selectLanguage = (state = LAN_GR, action) => {
if (action.type === LAN_GR) {
return { ...state, Selectedlan: LAN_GR};
}
else if (action.type === LAN_EN) {
return { ...state, Selectedlan: LAN_EN};
}
return state;
};
export default combineReducers({
Selectedlan: selectLanguage
});
Action
import { LAN_GR, LAN_EN } from './types';
export const changeLanGR = () => {
return {
type: LAN_GR
};
};
export const changeLanEN = () => {
return {
type: LAN_EN
};
};
You are mapping the dispatch function to props, meaning you need to call those functions mapped from props:
const Language = ({
className,
changeLanGR,
changeLanEN,
...rest
}) => {
const handleGreeceLan = () => {
changeLanGR();
};
const handleUKLan = () => {
changeLanEN();
};
Following problem: I've tried to write a generic typescript reducer the last few hours, and I feel like it's working fairly well already, but there's just one problem - They way I wired it with my store seems to have problems. It seems like the store does not properly update, as a component I tried to hook up with the data from the reducer does not receive new props.
This is the generic reducer. It's not fully complete yet, but the add functionality should work at least.
// Framework
import * as Redux from "redux";
// Functionality
import { CouldBeArray } from "data/commonTypes";
import { ensureArray } from "helper/arrayUtils";
type ReducerParams<T> = {
actionIdentifier: string;
key: keyof T;
}
export type ReducerState<T> = {
data: Array<T>;
}
type ReducerAction<T> = Redux.Action & {
payload: CouldBeArray<T>;
}
type Reducer<T> = {
add: (data: T) => ReducerAction<T>;
update: (data: T) => ReducerAction<T>;
delete: (data: T) => ReducerAction<T>;
replace: (data: T) => ReducerAction<T>;
reducer: Redux.Reducer<ReducerState<T>, ReducerAction<T>>;
}
export const createReducer = <T>(params: ReducerParams<T>): Reducer<T> => {
const ADD_IDENTIFIER = `${params.actionIdentifier}_ADD`;
const UPDATE_IDENTIFIER = `${params.actionIdentifier}_UPDATE`;
const DELETE_IDENTIFIER = `${params.actionIdentifier}_DELETE`;
const REPLACE_IDENTIFIER = `${params.actionIdentifier}_REPLACE`;
const initialState: ReducerState<T> = {
data: []
};
const reducer = (state = initialState, action: ReducerAction<T>): ReducerState<T> => {
switch (action.type) {
case ADD_IDENTIFIER:
const newState = { ...state };
const newData = [ ...newState.data ];
const payloadAsArray = ensureArray(action.payload);
payloadAsArray.forEach(x => newData.push(x));
newState.data = newData;
return newState;
case UPDATE_IDENTIFIER:
return {
...state,
};
case DELETE_IDENTIFIER:
return {
...state,
};
case REPLACE_IDENTIFIER:
return {
...state,
};
default:
return initialState;
}
}
const addAction = (data: T): ReducerAction<T> => {
return {
type: ADD_IDENTIFIER,
payload: data,
}
};
const updateAction = (data: T): ReducerAction<T> => {
return {
type: UPDATE_IDENTIFIER,
payload: data,
}
};
const deleteAction = (data: T): ReducerAction<T> => {
return {
type: DELETE_IDENTIFIER,
payload: data,
}
};
const replaceAction = (data: T): ReducerAction<T> => {
return {
type: REPLACE_IDENTIFIER,
payload: data,
}
};
return {
add: addAction,
update: updateAction,
delete: deleteAction,
replace: replaceAction,
reducer: reducer,
}
}
Next off, my store:
// Framework
import * as redux from "redux";
// Functionality
import { ReducerState } from "modules/common/Reducer/CrudReducer";
import { reducer as friendsReducer } from "modules/Friends/Reducer/FriendsReducer";
import { Friend } from "modules/Friends/types";
export type ReduxStore = {
friendsReducer: ReducerState<Friend>;
}
export const store: ReduxStore = redux.createStore(
redux.combineReducers({
friendsReducer: friendsReducer.reducer,
})
);
export default store;
and last but not least, the consuming component:
type Props = {
friends: Array<Friend>
}
export const FriendsList: React.FC<Props> = ({ friends }) => {
return (
<Flex className={"FriendsList"}>
Friends
</Flex>
);
}
const mapStateToProps = (store: ReduxStore): Props => {
return {
friends: store.friendsReducer.data,
};
}
export default connect(mapStateToProps)(FriendsList);
The problem usually unfolds in the following order:
Data is properly fetched from network
Update the store via store.dispatch(friendsReducer.add(payload))
With the debugger, I did step through the genericreducer and saw that the new state properly contains the new data.
This is where the problem occurs - The freshly generated state by the reducer is not transferred to my Friendslist component. It will only receive props once, while the data in there is still empty.
Where did I go wrong?
EDIT: By demand, the code for the friendsReducer:
import { createReducer } from "modules/common/Reducer/CrudReducer";
import { Friend } from "modules/friends/types";
export const reducer = createReducer<Friend>({
actionIdentifier: "FRIENDS",
key: "id"
});
export default reducer;
and for the dispatch:
const friendsResponse = await friendsCommunication.getFriends();
if (friendsResponse.success){
this.dispatch(friendsReducer.add(friendsResponse.payload));
}
...
protected dispatch(dispatchAction: Action){
store.dispatch(dispatchAction);
}
Found the problem - My generic reducer returned the following as default:
default:
return initialState;
while it should return state.
Otherwise it just did reset the state of all iterated reducers for every action.
Summary
In order to learn Redux, I am incorporating some state, actions, reducers, and trying to see how they are used in React Components.
I have set up a test object...
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
...and aim to:
increment the navigationCount by 1 when visiting pages
add or subtract from someNumber
push() & pop() elements from someList.
Versions
Currently using gatsby ^2.5.0, react ^16.8.6, and react-redux ^6.0.1.
Code
actions & reducers
import { combineReducers } from 'redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from './actionTypes.js';
// state
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
// action creators returning actions
export const pageIncrementer = navigationCount => {
return {
type: PAGE_INCREMENT,
navigationCount,
};
};
export const numberAdder = numberToAdd => {
return {
type: NUMBER_INCREASE,
numberToAdd,
};
};
export const numberMinuser = numberToMinus => {
return {
type: NUMBER_DECREASE,
numberToMinus,
};
};
export const listPusher = itemToAdd => {
return {
type: LIST_PUSH,
itemToAdd,
}
};
export const listPopper = () => {
return {
type: LIST_POP,
}
};
// reducers
const pageIncrementReducer = (state = initialState, action) => {
switch (action.type) {
case PAGE_INCREMENT:
return Object.assign({}, ...state, {
navigationCount: action.navigationCount+1
});
default:
return state.navigationCount;
}
};
const numberChanger = (state = initialState, action) => {
switch (action.type) {
case NUMBER_INCREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber+action.numberToAdd,
});
case NUMBER_DECREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber-action.numberToMinus,
});
default:
return state.someNumber;
};
};
const listChanger = (state = initialState, action) => {
switch (action.type) {
case LIST_POP:
return Object.assign({}, ...state, {
someList: state.someList.pop(),
});
case LIST_PUSH:
return Object.assign({}, ...state, {
someList: state.someList.push(action.itemToAdd),
});
default:
return state.someList;
}
}
// store
const rootReducer = combineReducers({
pageIncrementReducer,
numberChanger,
listChanger,
});
export default rootReducer;
React Component
import React from 'react';
import Layout from '../components/common/Layout.jsx';
import LandingBanner from '../components/landing/LandingBanner.jsx';
import LandingNavgrid from '../components/landing/LandingNavgrid.jsx';
import LandingApp from '../components/landing/LandingApp.jsx';
import { connect } from 'react-redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from '../state/actionTypes';
class LandingPage extends React.Component {
constructor(props){
super(props);
this.state = {
appliedNum: 2000,
};
}
componentDidMount(){
// this.props.pageIncrement(); // => numberChanger returned undefined
// this.props.numberIncrease(4444); // => pageIncrementReducer returned undefined
// this.props.numberDecrease(4444); // => pageIncrementReducer returned undefined
// this.props.listPush(4444); // => pageIncrementReducer returned undefined
this.props.listPop();
}
render(){
return (
<Layout>
<LandingBanner/>
<LandingNavgrid/>
<LandingApp/>
</Layout>
)
}
}
const filterNumbers = (list=[]) => {
console.log('filterNumbers list: ', list);
return list.filter(listElement => !!Number(listElement));
};
const mapStateToProps = (state, ownProps) => {
return {
someNumber: state.someNumber,
someList: filterNumbers(state.someList),
navigationCount: state.navigationCount,
};
};
const mapDispatchToProps = (dispatch) => {
return {
pageIncrement: () => dispatch({ type: PAGE_INCREMENT }),
numberIncrease: () => dispatch({ type: NUMBER_INCREASE }),
numberDecrease: () => dispatch({ type: NUMBER_DECREASE }),
listPush: () => dispatch({ type: LIST_PUSH }),
listPop: () => dispatch({ type: LIST_POP }),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LandingPage);
Errors
redux.js:449 Uncaught Error: Given action "LIST_POP", reducer
"pageIncrementReducer" returned undefined. To ignore an action, you
must explicitly return the previous state. If you want this reducer to
hold no value, you can return null instead of undefined.
first of all, you always need to return state on the default switch case.
default:
return state;
I've been trying to retrieve the new state from my vitaminReducer() reducer function, and connect it through mapStateToProps. But when I console.log the state, I get back "the state is {vitamin: undefined}".
This is the Vitamins component where I'm calling mapStateToProps()
(Vitamins.js)
componentDidMount() {
this.props.fetchVitamins();
}
function mapStateToProps(state) {
return {
vitamin: state,
}
};
console.log('the state is', mapStateToProps());
export default connect(mapStateToProps, { fetchVitamins })(Vitamins);
(reducers.js)
function vitaminReducer(state = [], action) {
switch(action.type) {
case FETCH_VITAMINS_SUCCESS:
return [
...state,
action.payload.vitamins
];
default:
return state;
}
}
const reducers = combineReducers({
vitamin: vitaminReducer,
});
I have the data coming through an Express server. I've console logged "vitamins" here and I get the data back, so I know that's not the issue.
(actions.js)
export function fetchVitamins() {
return dispatch => {
return fetch("/users")
.then(handleErrors)
.then(res => res.json())
.then(micros => {
dispatch(fetchVitaminsSuccess(micros));
const vitamins = micros.vitamins;
}
)};
};
export const FETCH_VITAMINS_SUCCESS = 'FETCH_VITAMINS_SUCCESS';
export const fetchVitaminsSuccess = vitamins => ({
type: FETCH_VITAMINS_SUCCESS,
payload: vitamins
});
If I do: "return { vitamin: state.vitamin, }" instead of "return { vitamin: state, }", I get back "TypeError: Cannot read property 'vitamin' of undefined". But that's what I called vitaminReducer in my combineReducers() function at the bottom of reducers.js, so I thought that was the right way to do it.
Thank you everyone for your input! I was able to get it working.
I ditched the mapStateToProps() and instead did this
(Vitamins.js)
componentDidMount() {
this.props.fetchVitamins();
}
renderData() {
const { vitamins } = this.props.vitamins;
return vitamins.map((micro, index) => {
return (
<option value={micro.value} key={index}>{micro.name}</option>
)
})
}
export default connect(
state => ({
vitamins: state.vitamins
}),
{
fetchVitamins
},
)(Vitamins);
I set the dispatch action inside of the fetchVitamins() function
(actions.js)
export function fetchVitamins() {
return dispatch => {
return fetch("/users")
.then(handleErrors)
.then(res => res.json())
.then(micros => {
dispatch({
type: "RECEIVE_VITAMINS",
payload: micros.vitamins
});
}
)};
};
export const RECEIVE_VITAMINS = 'RECEIVE_VITAMINS';
In reducers I set the initialState to the vitamins array, and passed the new state of micros.vitamins from my RECEIVE_VITAMINS action
(reducers.js)
const initialState = {
vitamins: [],
}
function vitaminReducer(state = initialState, action) {
switch(action.type) {
case RECEIVE_VITAMINS:
return {
...state,
vitamins: action.payload
};
default:
return state;
}
}
const reducers = combineReducers({
vitamins: vitaminReducer,
});
Thanks everyone for your help! Let me know if you have any other suggestions :D