I'm building an application using React/Redux, I have an array of products which are loaded to Redux state asynchronously and from which I would then like to pull individual products. However, the reducer I have written to do this isn't working as it is registering state as null. This is confusing me as calling getState() in the thunk action creator prior to returning the action and triggering the reducer is logging the correct state with the array of products.
Is this an error in my code or simply part of how redux state updates?
ACTION CREATOR: getSingleProduct
export const getSingleProduct = productName => (dispatch, getState) => {
const action = { type: 'GET_SINGLE_PRODUCT', productName };
if (!getState().products.length) return dispatch(getAllProducts())
.then(() => {
console.log('STATE IN ACTION CREATOR THEN BLOCK', getState());
return dispatch(action);
})
.catch(console.log);
else return action;
}
REDUCER: currentProduct
const currentProduct = (state = null, action) => {
switch (action.type) {
case 'GET_SINGLE_PRODUCT':
console.log('STATE IN REDUCER', state);
return state.products.filter(prod => prod.name.toLowerCase() === action.productName)[0];
break;
default:
return state;
}
}
Console Log Output
STATE IN ACTION CREATOR THEN BLOCK
{ basket: Array(0), products: Array(6), currentProduct: null }
STATE IN REDUCER
null
State is null cause you defined it as null on first function call.
console.log state after action complete and you see value run.
It is wrong to return modified state. Should return new state.
const currentProduct = (state = null, action) => {
switch (action.type) {
case 'GET_SINGLE_PRODUCT':
console.log('STATE IN REDUCER', state);
const products = state.products.slice().filter(prod => prod.name.toLowerCase() === action.productName)[0];
return { ...state, products }
break;
default:
return state;
}
}
Reducer state was in fact up to date, the issue was a misunderstanding of how reducer state works. I was trying to utilise a dependent state which is not available from the state argument of the reducer. The resolution for me was to pass this information from a dependent state in on the action object.
action
export const getSingleProduct = (productName, products = []) => (dispatch, getState) => {
let action = {
type: 'GET_SINGLE_PRODUCT',
productName,
products: getState().products
}
if (!action.products.length) dispatch(getAllProducts())
.then(() => {
action = Object.assign({}, action, { products: getState().products });
dispatch(action);
})
.catch(console.log);
else return action;
}
reducer
const currentProduct = (state = {}, action) => {
switch (action.type) {
case 'GET_SINGLE_PRODUCT':
const currentProduct = action.products.filter(prod => prod.name.toLowerCase() === action.productName)[0];
return Object.assign({}, state, currentProduct);
default:
return state;
}
}
Related
why state is returned empty
i had tried many solutions from this website but non of them worked
const initialState = [];
export default function(state = initialState, actions) {
const { type, payload } = actions;
switch (type) {
case DASHBOARD:
return [...state,payload]
default:
return state;
}
}
when this state is mapped to props , payload become undefined.
here when i console log action.payload it is defined but could not be passed in return statement.
May I know for which case it failing?
For Dashboard case,it will be "DASHBOARD" until and unless it is not constant.
And for your default case,you need to change your initial state like this:
const initialState = {payload:"your initial payload",your_other_properties:"here"};
export default function(state = initialState, actions) {
const { type, payload } = actions;
switch (type) {
case "DASHBOARD":
return [...state,payload]
default:
return state;
}
}
I have a use case like this:
eventListReducer: will get a list of events based on date range
eventDetailReducer: will get the event details based on one event id
I know how to do the two above, my question:
When my page loads initially, I will get a list of events based on default date range and load the first event details, I can certainly create an
EventListAndDetailReducer to duplicate eventListReducer and eventDetailReducer. Is there any better way I can reuse the logic?
What I want to achieve is to have another action, that will first call getEvents and update the eventLists state, and then grab the first event and call setEvent and update the eventDetail state.
This is my eventDetailReducer:
const initialState = {
eventDetails: "",
}
const eventReducer = (state = initialState, action) => {
switch (action.type) {
case "SET_EVENT":
state = {
...state,
eventDetails: action.payload
};
break;
}
return state;
}
export default eventReducer;
This is my eventsReducer:
const initialState = {
eventsList: [],
}
//getEventsReducer
const getEventsReducer = (state = initialState, action) => {
switch (action.type) {
case "GET_EVENTS":
state = {
...state,
eventList: ["Joe", "Tom", "Marry"] //assuming this from some other endpoint
};
break;
}
return state;
}
export default getEventsReducer;
What about using EventListAndDetailReducer?
const initialState = {
eventsList: [],
eventDetails: ""
}
export function eventListAndDetailReducer(state, action) {
switch(action.type) {
case GET_EVENTS:
return {...state, eventList: eventsReducer(state.eventsList, action)}
case "SET_EVENT":
return {...state, eventDetails: eventDetailsReducer(state.eventDetails, action)}
default:
return state
}
}
and then somewhen start using combineReducers?
Why not just have the eventDetails reducer also update on the GET_EVENTS action?
const eventReducer = (state = initialState, action) => {
switch (action.type) {
case "SET_EVENT":
state = {
...state,
eventDetails: action.payload
};
break;
case "GET_EVENTS":
state = {
...state,
eventDetails: action.payload[0] // assuming payload is an array
};
break;
}
return state;
}
Remember, all reducers receive all actions, so it does not need to be a 1-1 mapping.
What I understand from you question is that you want another action to do both actions sequentially and be dependent on each. I assume you have some middle ware such as redux-thunk that allows actions to be more than plaIn functions!
export function combinedAction() {
return (dispatch, getState) => {
// Write fetch() request to get events list from anywhere.
// Following should be within .then() if you're using fetch.
// Here events are just hardcoded in reducer!
dispatch(return { type: GET_EVENTS, payload: events }).then( () => {
let event = getState().eventsList[0]
dispatch(return { type: SET_EVENT, payload: event })
})
};
}
This will fire up GET_EVENTS action first and it'll set events array in state.eventsList. Then next action just uses this state information to dispatch next action SET_EVENT. Refer here to learn about chaining actions. How to chain async actions?
I have an events database maintained in Firestore. I use AngularFire5 to keep the events synced to my app.
The events have many 'sessions', that can be updated my multiple administrators and users(ratings, questions etc).
I'm using ngrx store, reducers and effects to maintain the sync. I was able to figure out the single session update/delete/add.
The issue is that the initial load of sessions returns a session[]. And beyond that, the snapshot changes could return 'added', 'modified' and 'removed' session. How can I use a single action+reducer function to add when session(s) is not existing, but update or remove when session(s) existing. (I'm not calling store.dispatch within my firebase service, I'm using the effects to manage this)
session.effect.ts
#Effect()
getSession: Observable<Action> = this.actions.ofType(SessionActions.ADD_SESSION)
.map((action: SessionActions.AddSession) => action.payload)
.switchMap(payload => this.db.getSessions(payload)
.map((sessions:Session[]) => new SessionActions.AddSessionSuccess(sessions))
);
session.reducer.ts
export function SessionReducer(state: Session[], action: Action) {
// console.log('SessionsReducer :: Action Type: ' + action.type + ', Action Payload: ' + JSON.stringify(action.payload));
switch(action.type) {
case SessionActions.ADD_SESSION_SUCCESS:
return [ ...state, action.payload ];
case SessionActions.CREATE_SESSION_SUCCESS:
return [ ...state, action.payload ];
case SessionActions.UPDATE_SESSION_SUCCESS:
return state.map(session => {
return session.id === action.payload.id ? Object.assign({}, action.payload) : session;
})
case SessionActions.ADD_SESSION:
case SessionActions.DELETE_SESSION_FAILURE:
return state;
default:
return state;
}
}
session.actions.ts
//Used to search the sessions by event id, think of it as GetSessions
export class AddSession implements Action {
readonly type = ADD_SESSION;
constructor(public payload: string) {}
}
export class AddSessionSuccess implements Action {
readonly type = ADD_SESSION_SUCCESS;
constructor(public payload: Session[]) {}
}
In my store I have a state with this shape: {posts: [{...},{...}]}, but when I use mapStateToProps() in Home.js, the state returns {posts: []}, with an empty array (where there used to be an array in the store's state).
Am I using mapStateToProps() incorrectly or does the problem stem from other parts of the Redux cycle?
API fetch I'm using, temporarily located in actions.js
// api
const API = "http://localhost:3001"
let token = localStorage.token
if (!token) {
token = localStorage.token = Math.random().toString(36).substr(-8)
}
const headers = {
'Accept': 'application/json',
'Authorization': token
}
// gets all posts
const getAllPosts = token => (
fetch(`${API}/posts`, { method: 'GET', headers })
);
Action and action creators, using thunk middleware:
// actions.js
export const REQUEST_POSTS = 'REQUEST_POSTS';
function requestPosts (posts) {
return {
type: REQUEST_POSTS,
posts
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
function receivePosts (posts) {
return {
type: RECEIVE_POSTS,
posts,
receivedAt: Date.now()
}
}
// thunk middleware action creator, intervenes in the above function
export function fetchPosts (posts) {
return function (dispatch) {
dispatch(requestPosts(posts))
return getAllPosts()
.then(
res => res.json(),
error => console.log('An error occured.', error)
)
.then(posts =>
dispatch(receivePosts(posts))
)
}
}
Reducer:
// rootReducer.js
function posts (state = [], action) {
const { posts } = action
switch(action.type) {
case RECEIVE_POSTS :
return posts;
default :
return state;
}
}
Root component that temporarily contains the Redux store:
// index.js (contains store)
const store = createStore(
rootReducer,
composeEnhancers(
applyMiddleware(
logger, // logs actions
thunk // lets us dispatch() functions
)
)
)
store
.dispatch(fetchPosts())
.then(() => console.log('On store dispatch: ', store.getState())) // returns expected
ReactDOM.render(
<BrowserRouter>
<Provider store={store}>
<Quoted />
</Provider>
</BrowserRouter>, document.getElementById('root'));
registerServiceWorker();
Main component:
// Home.js
function mapStateToProps(state) {
return {
posts: state
}
}
export default connect(mapStateToProps)(Home)
In Home.js component, console.log('Props', this.props) returns {posts: []}, where I expect {posts: [{...},{...}]}.
*** EDIT:
After adding a console.log() in the action before dispatch and in the reducer, here is the console output:
Console output link (not high enough rep to embed yet)
The redux store should be an object, but seems like it's getting initialized as an array in the root reducer. You can try the following:
const initialState = {
posts: []
}
function posts (state = initialState, action) {
switch(action.type) {
case RECEIVE_POSTS :
return Object.assign({}, state, {posts: action.posts})
default :
return state;
}
}
Then in your mapStateToProps function:
function mapStateToProps(state) {
return {
posts: state.posts
}
}
I have the following two #ngrx/store reducers:
import {ActionReducer, Action} from '#ngrx/store';
import {UserAccount} from '../shared/models/useraccount.model';
export const SET_CURRENT_USER_ACCOUNT = 'SET_CURRENT_USER_ACCOUNT';
export const UPDATE_CURRENT_USER_ACCOUNT_FIRST_NAME = 'UPDATE_CURRENT_USER_ACCOUNT_FIRST_NAME';
export const currentUserAccountReducer: ActionReducer<UserAccount> = (state: UserAccount, action: Action) => {
console.log('currentUserAccountReducer:', state, action);
switch (action.type) {
case SET_CURRENT_USER_ACCOUNT: {
return action.payload;
}
case UPDATE_CURRENT_USER_ACCOUNT_FIRST_NAME: {
state.firstName = action.payload;
return state;
}
}
};
export const SET_AUTHENTICATED = 'SET_AUTHENTICATED';
export const SET_UNAUTHENTICATED = 'SET_UNAUTHENTICATED';
export const authenticatedReducer: ActionReducer<boolean> = (state: boolean, action: Action) => {
console.log('authenticatedReducer:', state, action);
switch (action.type) {
case SET_AUTHENTICATED: {
return true;
}
case SET_UNAUTHENTICATED: {
return false;
}
}
};
However, for some reason when I issue a dispatch for the 1st reducer (i.e. currentUserAccountReducer) then it changes the state for the 2rd reducer (i.e. authenticatedReducer)...
Here is the dispatch causing this issue:
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
Here is how I initialize the store in the imports section:
StoreModule.provideStore(
{
currentUserAccount: currentUserAccountReducer,
authenticated: authenticatedReducer
})
Can someone please provide advice?
edit: The issue is that authenticated ends up undefined!!
The switch statements in your reducers do not contain default cases. You need to add default cases that return the state, as the reducers will be called for all actions - the store has no way of knowing which reducer should be called for a particular action type, so each dispatched action is passed to every reducer.