I'm a beginner to Redux and having trouble.
Somehow after every action, my whole store is completely cleared and only current action is executed. I'm going to describe the situation with screenshot of action log.
Here is my action:
function writeProduct(id) {
return {
type: 'WRITE_PRODUCT',
id
}
}
Here is my reducer:
case 'WRITE_PRODUCT':
console.log("WRITE PRODUCT ACTION: ", action);
console.log("state: ",state);
return {
productID: action.id
};
And here is the consoled log during the dispatch of writeProduct action.
https://cdn1.imggmi.com/uploads/2018/5/9/c4cfc7debab662dfe241889d86254cd1-full.png
What I do wrong? Why after every dispatch previous store is overwritten?
You're overwriting the previous state in your reducer.
You should include all the previous state and only change what the reducer should.
Eg.
In your reducer:
return {
...state,
productID: action.id
};
Notice the ...state part, which includes all the previous state in the result.
I'd suggest you review the redux docs on immutable updates
Also, your action object isn't FSA compliant, should probably read about that too.
hmmm i don't know why the whole store is wiped out but
case 'WRITE_PRODUCT':
console.log("WRITE PRODUCT ACTION: ", action);
console.log("state: ",state);
return {
productID: action.id
};
should be
case 'WRITE_PRODUCT':
console.log("WRITE PRODUCT ACTION: ", action);
console.log("state: ",state);
return {
...state,
productID: action.id
};
```
state is passed to your reducer function
Related
I'm quite new to coding and I'm currently practicing the useReducer() hook in React to manage some state in a simple todo app.
I'm having trouble when trying to implement the TOGGLE_TODO action. I've done it before using arrays, but as I'll likely be working with a lot of objects, I'm trying to figure out why I can't get this right. I'd say I'm learning by failing, but all I'm learning is how to switch the computer off and walk away!
Each time I toggle, I'm passing the state with the spread operator, I've tried it throughout all of the item, I've logged out the key and action.payload to make sure I'm getting a match (it works when I do a simple alert with matching).
I'm aware that the toggle isn't a toggle yet, I was just trying to simply get complete to be true.
I've tried a multitude of things to return state, I've added return to the beginning of the statement, and I"ve encountered some weird bugs along the way. As mentioned, this is quite simple state for now, but it will be more complex in another project I'm working on, so useState get's quite messy.
Any help on what I'm doing wrong here would be highly appreciated.
const initialAppState = {
isOpen: true,
todos: {}
};
export const ACTIONS = {
TOGGLE_MODAL: "toggle-modal",
ADD_TODO: "add-todo",
TOGGLE_TODO: "toggle-todo"
};
const reducer = (state, action) => {
// switch statement for actions
switch (action.type) {
case ACTIONS.TOGGLE_MODAL:
return { ...state, isOpen: !state.isOpen };
case ACTIONS.ADD_TODO:
return {
...state,
todos: {
...state.todos,
// Object is created with Unix code as the key
[Date.now()]: {
todo: action.payload.todo,
complete: false
}
}
};
case ACTIONS.TOGGLE_TODO:
// Comparing the key and the action payload. If they match, it should set complete to 'true'. This will be updated to a toggle when working.
Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return {
...state,
todos: { ...state.todos, [key]: { complete: true } }
};
}
return state;
});
default:
throw new Error("Nope. not working");
}
};
In the render, I pass the key as an id so it can get returned with the payload.
Here is the dispatch function from the component...
const Todo = ({ id, value, dispatch }) => {
return (
<div className="todo">
<h1>{`Todo: ${value.todo}`}</h1>
<p>Done? {`${value.complete}`}</p>
<button
onClick={() =>
dispatch({
type: ACTIONS.TOGGLE_TODO,
payload: id
})
}
>
Mark as Done
</button>
</div>
);
};
and the render is using Object.entries which all works just fine. There were times when I'd get an error, or the initial todo would disappear, so I knew that state wasn't being updated correctly.
Here is the code on CodeSandbox too. I'll update here if I get it working, but I've been stuck here a couple of days. :-(
You were almost there, good idea to index your items with Date.now()!
Only a few issues in the TOGGLE_TODO case:
your reducer should always return a state, your return statement should be at the end of the case, but you put it with the map's function
your reducer should compute a new state, not mutate the current state. So you have to create a new todo object with the complete property.
Here is how it goes:
case ACTIONS.TOGGLE_TODO:
const newTodos = Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return { ...state.todos[key], complete: true } // create a new todo item
}
else {
return state.todos[key]; // keep the existing item
}
});
return {...state, todos: newTodos};
Basically, in our case, we need to either get an alerts list that shows the first few items (mounting it first time in the DOM) or show the initial list + the next list (clicking a load more button).
Hence we needed to do this condition in our GET_ALERTS action:
case "GET_ALERTS":
if (action.initialList) {
newState.list = [...newState.list, action.res.data.list];
} else {
newState.list = newState.list.concat(
action.res.data.list
);
}
And when we call the action reducer in our Alerts component, we need to indicate whether initialList is true or false.
E.g.
componentDidMount() {
this.props.getAlerts(pageNum, true);
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum, true);
}
Anyway in such a case, is it fine to use conditional statement in the reducer?
I am against this idea. The reducer has a single responsibility: update Redux state according to the action.
Here are three ways to slove this:
easy way - initialize your list in Redux state to empty list
if you set the list in state to empty list ([]) then it's much simpler.
You can basically just change your reducer to this:
case "GET_ALERTS":
return {...state, list: [...state.list, action.res.data.list]
This will make sure that even if you have get initial list or more items to add to the list, they will be appended. No need to add any logic - which is awesome IMHO.
redux-thunk and separating type into two different types
create two actions: GET_INIT_ALERTS and GET_MORE_ALERTS.
switch(action.type) {
case "GET_INIT_ALERTS":
return {...state, list: action.res.data.list }
case "GET_MORE_ALERTS":
return {...state, list: [...state.list, ...action.res.data.list]}
case "CHECK_READ_ALERTS":
return {...state, read: [...state.read, ...action.res.data.list]}
}
In the component I will have:
componentDidMount() {
this.props.getInitAlerts();
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum);
}
In alerts action with the help of redux-thunk:
export const getAlerts = (pageNum : number) => (dispatch) => {
return apiAction(`/alerts/${pageNum}`, 'GET').then(res => dispatch({type: "GET_MORE_ALERTS", res});
}
export const getInitAlerts = () => (dispatch) => {
return apiAction('/alerts/1', 'GET').then(res => dispatch({type: "GET_INIT_ALERTS", res});
}
I guess you update pageNum after readMore or componentDidMount. Of course you can save that state in Redux and map it back to props and just increment it when calling the getAlerts action.
write your own middleware
Another way to do this is to write an ad-hoc/feature middleware to concat new data to a list.
const concatLists = store => next => action => {
let newAction = action
if (action.type.includes("GET") && action.initialList) {
newAction = {...action, concatList: action.res.data.list}
} else if (action.type.includes("GET") {
newAction = {...action, concatList: [...state[action.key].list, action.res.data.list]}
}
return next(newAction);
}
And change your reducer to simply push concatList to the state:
case "GET_ALERTS":
return {...state, list: action.concatList}
In addition, you will have to change your action to include key (in this case the key will be set to alert (or the name of the key where you store the alert state in redux) and initialList to determine whether to concat or not.
BTW, it's a good practice to put these two under the meta key.
{
type: "GET_ALERT",
meta: {
initialList: true,
key: "alert",
},
res: {...}
}
I hope this helps.
I would suggest you to have following set of actions:
ALERTS/INIT - loads initial list
ALERTS/LOAD_MORE - loads next page and then increments pageNo, so next call will know how many pages are loaded
ALERTS/MARK_ALL_AS_READ - does server call and reinitializes list
The store structure
{
list: [],
currentPage: 0
}
And component code should not track pageNum
componentDidMount() {
this.props.initAlerts();
}
markAllAsRead() {
this.props.markAllAsRead();
}
readMore() {
this.props.loadMore();
}
Using React-redux here and having a bit of an issue, that some of you might help with.
The user can create 'Jobs' (posts) and also remove them. Adding them is no issue and the reducer returns what is expected. However, once I delete a job from the (firebase) database I trigger a new fetch for the current jobs, but the reducer still returns the old jobs. Am I missing something?
Before deleting, this is how the jobs objects looks like:
activeJobs= {
-KrkPPy4ibSraKG-O49S: {
title: 'Help',
location: 'etc,
...
},
-KrkPPy4ibSraKG-O49S: {
title: 'Help',
location: 'etc,
...
} and so on
}
When I delete them all I get this {} back from the server. Expected.
What is not expected is that my reducer still returns the old jobs and my components do not re-render.
I dispatch an action after fetching the jobs:
firebase.database().ref(`/jobs/activeJobs/${currentUser.uid}`)
.on('value', snapshot => {
console.log('new activeJobs ===', snapshot.val());
dispatch({
type: FETCH_JOBS_SUCCESS,
payload: snapshot.val()
});
});
snapshot.val() does contain the new updated jobs.
Then here is the reducer that handles the action:
switch (action.type) {
case FETCH_JOBS_SUCCESS:
// ...state contains the OLD jobs and action.payload contains {}. Why is is not overriding it the old jobs?
return { ...state, ...action.payload };
default:
return state;
}
Why is my reducer failing?
The { ...state, ...action.payload } syntax actually mean : build a new object by taking every prop of state and adding every props of action.payload. In your case, you just get a new object that is similar to state, since ...action.payload is an empty object.
Change your action to
return Object.assign({}, state, {activeJobs : action.payload});
The shape of my Redux state looks like this:
{
user: {
id: 123,
items: [1, 2]
},
items: {
1: {
...
},
2: {
...
}
}
}
Using combineReducers I have 2 sets of reducers. Each act on one of the root keys of the state. i.e. one manages the user key and the other the items key.
If I want to add an item I can call 2 reducers, the first will add a new object to the items and the second will add the id to the user.items array.
This has a bad code smell. I feel that there should be a way to atomically reduce the state of both objects at the same time. i.e. in addition to the sub-reducers have a root reducer that acts on the root object. Is this possible?
I think what you're doing is actually correct!
When dispatching an action, starting from the root-reducer, every "sub-reducer" will be called, passing the corresponding "sub-state" and action to the next layer of sub-reducers. You might think that this is not a good pattern since every "sub-reducer" gets called and propagates all the way down to every single leaf node of the state tree, but this is actually not the case!
If the action is defined in the switch case, the "sub-reducer" will only change the "sub-state" part it owns, and maybe passes the action to the next layer, but if the action isn't defined in the "sub-reducer", it will do nothing and return the current "sub-state", which stops the propagation.
Let's see an example with a more complex state tree!
Say you use redux-simple-router, and I extended your case to be more complex (having data of multiple users), then your state tree might look something like this:
{
currentUser: {
loggedIn: true,
id: 123,
},
entities: {
users: {
123: {
id: 123,
items: [1, 2]
},
456: {
id: 456,
items: [...]
}
},
items: {
1: {
...
},
2: {
...
}
}
},
routing: {
changeId: 3,
path: "/",
state: undefined,
replace:false
}
}
As you can see already, there are nested layers in the state tree, and to deal with this we use reducer composition, and the concept is to use combineReducer() for every layer in the state tree.
So your reducer should look something like this:
(To illustrate the layer by layer concept, this is outside-in, so the order is backwards)
first layer:
import { routeReducer } from 'redux-simple-router'
function currentUserReducer(state = {}, action) {
switch (action.type) {...}
}
const rootReducer = combineReducers({
currentUser: currentUserReducer,
entities: entitiesReducer, // from the second layer
routing: routeReducer // from 'redux-simple-router'
})
second layer (the entities part):
function usersReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
case TYPE_TWO:
case TYPE_TREE:
return Object.assign({}, state, {
// you can think of this as passing it to the "third layer"
[action.userId]: itemsInUserReducer(state[action.userId], action)
})
case TYPE_FOUR:
return ...
default:
return state
}
}
function itemsReducer(...) {...}
const entitiesReducer = combineReducers({
users: usersReducer,
items: itemsReducer
})
third layer (entities.users.items):
/**
* Note: only ADD_ITEM, TYPE_TWO, TYPE_TREE will be called here,
* no other types will propagate to this reducer
*/
function itemsInUserReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
return Object.assign({}, state, {
items: state.items.concat([action.itemId])
// or items: [...state.items, action.itemId]
})
case TYPE_TWO:
return DO_SOMETHING
case TYPE_TREE:
return DO_SOMETHING_ELSE
default:
state:
}
}
when an action dispatches
redux will call every sub-reducer from the rootReducer,
passing:
currentUser: {...} sub-state and the whole action to currentUserReducer
entities: {users: {...}, items: {...}} and action to entitiesReducer
routing: {...} and action to routeReducer
and...
entitiesReducer will pass users: {...} and action to usersReducer,
and items: {...} and action to itemsReducer
why is this good?
So you mentioned is there a way to have the root reducer handling different parts of the state, instead of passing them to separate sub-reducers. But if you don't use reducer composition and write a huge reducer to handle every part of the state, or you simply nest you state into a deeply nested tree, then as your app gets more complicated (say every user has a [friends] array, or items can have [tags], etc), it will be insanely complicated if not impossible to figure out every case.
Furthermore, splitting reducers makes your app extremely flexible, you just have to add any case TYPE_NAME to a reducer to react to that action (as long as your parent reducer passes it down).
For example if you want to track if the user visits some route, just add the case UPDATE_PATH to your reducer switch!
Bear with me here as this question pertains to my first test app using either React, Redux or react-redux. Docs have gotten me far and I have a mock banking app that mostly works. My state object looks roughly like this:
{
activePageId: "checking",
accounts: [
checking: {
balance: 123,
transactions: [
{date, amount, description, balance}
]
}
]
}
I have just two actions:
1. CHANGE_HASH (as in url hash). This action always works as expected and all the reducer does is update the state.activePageId (yes, I'm cloning the state object and not modifying it). After the action, I can see the state has changed in the Redux store and I can see that React has updated.
function changeHash(id) {
return {
type: "CHANGE_HASH",
id: id
}
}
2. ADD_TRANSACTION (form submission). This action never updates React, but it always updates the Redux store. The reducer for this action is updating state.accounts[0].balance and it's adding a transaction object to the array state.accounts[0].transactions. I don't receive any errors, React just doesn't update. HOWEVER, if I dispatch a CHANGE_HASH action React will catch up and display all of the ADD_TRANSACTION state updates properly.
function addTransaction(transaction, balance, account) {
return {
type: "ADD_TRANSACTION",
payload: {
transaction: transaction,
balance: balance,
account: account
}
}
}
My reducer...
function bankApp(state, action) {
switch(action.type) {
case "CHANGE_HASH":
return Object.assign({}, state, {
activePageId: action.id
});
case "ADD_TRANSACTION":
// get a ref to the account
for (var i = 0; i < state.accounts.length; i++) {
if (state.accounts[i].name == action.payload.account) {
var accountIndex = i;
break;
}
}
// is something wrong?
if (accountIndex == undefined) {
console.error("could not determine account for transaction");
return state;
}
// clone the state
var newState = Object.assign({}, state);
// add the new transaction
newState.accounts[accountIndex].transactions.unshift(action.payload.transaction);
// update account balance
newState.accounts[accountIndex].balance = action.payload.balance;
return newState;
default:
return state;
}
My mapStateToProps
function select(state) {
return state;
}
What am I missing here? I'm under the impression that React is supposed to update as the Redux storeis updated.
Github repo:
Deployment bank demo
p.s. I lied about not having any errors. I do have a number of warnings
""Warning: Each child in an array or iterator should have a unique "key" prop..."
I'm already giving them a key prop set to it's index. I doubt that has anything to do with my issue though.
The problem is in this piece of code:
// clone the state
var newState = Object.assign({}, state);
// add the new transaction
newState.accounts[accountIndex].transactions.unshift(action.payload.transaction);
// update account balance
newState.accounts[accountIndex].balance = action.payload.balance;
Cloning the state object doesn't mean you can mutate the objects it is referring to. I suggest you to read more about immutability because this isn't how it works.
This problem and solution to it are described in detail in Redux “Troubleshooting” docs so I suggest you to read them.
https://redux.js.org/troubleshooting
I also suggest you to take a look at Shopping Card example in Flux Comparison for Redux because it shows how to update nested objects without mutating them in a similar way to what you are asking.
https://github.com/voronianski/flux-comparison/tree/master/redux