Should Actions in Redux always be unique? - redux

In this example I'm using an action named ADD_TODO
import { createStore, combineReducers } from 'redux';
function todos(state, action) {
state = state || [];
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.text ]);
default:
return state;
}
}
function counter(state, action){
state = state || 0;
switch (action.type){
case 'INCREMENT':
return state+1;
case 'DECREMENT':
return state-1;
case 'ADD_TODO':
return state+100;
default:
return state;
}
}
var app = combineReducers({
todos: todos,
counter: counter
});
var store = createStore(app);
store.dispatch({ type: 'ADD_TODO': text: 'buy eggs' });
This cause both the "todos" and "counter" reducers to trigger.
Should I make all reducers have unique actions unless I actually intended it?
How can we implement this with multiple reducers that almost do the same thing? Multiple counters for example can have "INCREMENT" and a "DECREMENT" actions.
Should name spacing actions solve it?
eg: "POINT_INCREMENT", "POINT_DECREMENT".

There's nothing inherently wrong with having different reducers respond to the same action -- for example, if you refresh the entire state at once. But yeah, if you have two counters that correspond to different things, you probably want to come up with a naming scheme to differentiate. But I would think the action names probably should have some noun to indicate what they apply to.

This cause both the "todos" and "counter" reducers to trigger. Should I make all reducers have unique actions unless I actually intended it?
Yes, probably they should have different unique actions.
From your example it becomes not really clear what you actually intend.
Should the counter count the amount of todo's ?
In that case it can actually be sensible that a "ADD_ITEM" action would both update the counter and also add a todo item. In that case please refer to the answer of acjay
How can we implement this with multiple reducers that almost do the same thing? Multiple counters for example can have "INCREMENT" and a "DECREMENT" actions.
When displaying a list of counters in the same app, each counter can be assigned a unique identifier (id).
An action should pass along the id of the counter.
export const toggleTodo = id => ({
type: 'INCREMENT',
id
})
The reducer should then check by id which counter to update.
See this example of a todo list in the official redux docs.
https://redux.js.org/basics/example
Redux actions are in a way globals. There are different strategies to workaround this problem on a larger scale.
https://kickstarter.engineering/namespacing-actions-for-redux-d9b55a88b1b1

Related

How to work with Redux reducers and server API

What is a better way to create reducers with handleActions in redux-actions:
1. Create reducers for each CRUD operations (like add data, delete data) and combine it. How set initialState in this case?
2. Set actions in one reducer (fetchDeleteDataRequest, fetchDeleteDataSuccess, fetchAddDataRequest, fetchAddDataSuccess by example)?
You can have sperate reducers and or common reducers to add or delete data that is up to you. If you are considering separate actions for add and delete you will need to keep the data consistent. But having a common function to deal with the CRUD operations is ideal since this will reduce the amount of code that you have to use but you will need a way to distinguish them as well (maybe bypassing some variable ADD or DELETE). let's consider and list of items that you will be adding or deleting. This list can be an empty array in the beginning (initialState) and pass it as props to your component.
Actions
export const addDeleteItem = data => dispatch => {
// you can perform REST calls here
dispatch({
type: ADD_REMOVE_DATA,
payload: data
});
};
Reducers
let initialState = {
items:[]
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_REMOVE_DATA:
if(action.payload.event === 'ADD'){
return {...state,items:[...state.items,action.payload.item]}
}else if(action.payload.event === 'DELETE'){
return {...state,items:state.items.filter(item=>item.id !== action.payload.item.id)}
}else if (action.payload.event === 'UPDATE'){
//handle your update code
}
}
}
This is just an example you can follow something like this to avoid code duplication.

How do I define a reducer at the top level?

I have two reducer files that handle the two areas of state in my module...
export interface MyState {
people: PeopleState;
pets: PetState;
}
export const reducers: ActionReducerMap<MyState> = {
people: peopleReducer,
pets: petsReducer
};
This works fine. However, I have some actions that need to update both areas of the state. I'd like to handle this by having a reducer file that deals with things at the MyState level and can update people and pets. I'd like to keep both the existing reducers that handle this at the lower levels.
I can't see how to register the top level reducer. The way the code is now, any reducer added to the ActionReducerMap must be added as a property inside MyState rather than handling MyState as a whole.
Anyone got any ideas?
Thanks
Nick
I guess that the only solution here is to use metaReducer, check this article: https://netbasal.com/implementing-a-meta-reducer-in-ngrx-store-4379d7e1020a
Description: metaReducer is a kind of reducer that stays above other reducers. It should have it's own actions, can have it's own effects. You can use it in such way:
export function metaReducer( reducer ) {
return function reduce( state: ParentState, action: MetaReducerAction ) {
switch(action.type)
case PARENT_ACTION:
return {
...state,
people: peopleReducer(state.people, new DoSthWithPeople()),
pets: petsReducer(state.pets, new DoSthWithPets())
}
default:
return reducer(state, action);
}
}
Where:
interface ParentState {
pets: PetsState,
people: PeopleState
}
type MetaReducerAction = ParentAction <-- this has type PARENT_ACTION
So the workflow is straight forward. In a place that you want a action to update both people and pets states, you need to dispatch PARENT_ACTION, then the actions DoSthWith... are going to be triggered on both slices of state. If you dispatch different action (the action of type that is not handled by the metaReducer, so sth different to PARENT_ACTION) then it will allow other reducers to handle the action (check whats in default section).
The last part is configuration, it should look like this:
StoreModule.forFeature(compose(metaReducer, combineReducers)(reducers))
Where reducers is just:
const reducers = {
pets: petsReducer,
people: peopleReducer
}
Edit: formatting

Reusability of reducers at different levels of app state structure

For example we have reducer photos, which handles array of photos via actions ADD_PHOTO and REMOVE_PHOTO. And if we have arrays users and posts, they both have field for array of photos.
So, in order to avoid code duplicates I'm going to do the following:
Create reducer user = combineReducers(..., photos, ...)
Create actionCreator updateUser
const updateUser = (id, subAction) => ({
type: UPDATE_USER,
payload: {
id,
subAction
}
})
Create reducer users (Here I'm using Immutable.js)
function users(state = List(), action) {
switch (action.type) {
//...
case UPDATE_USER:
const { id, subAction } = action.payload
const index = state.findIndex(user => user.id == id)
return state.updateIn(
[index, 'photos'],
state => photos(state, subAction)
)
break
//...
default:
return state
}
}
And then I'm going to use all of it like this:
dispatch(updateUser(id, addPhoto(url)))
Is this a correct solution of my problem?
Why not simply dispatch both in the place where this is initiated by the user?
dispatch(updateUser(id));
dispatch(addPhoto(url));
I haven't come across this pattern you're applying before. It seems a bit unusual that one reducer is responsible for reducing state of another. Creates a dependency between them that doesn't feel very pure. I'm not even sure one reducer should be able to/can see the state of another.
So no idea about "correct", but I'd say it's not ideal to do it your way. I'd try dispatching both sequentially or maybe in a sort of meta-action that takes care of nested updates and dispatches actions to multiple reducers.

"Thread safety" in Redux?

Let's pretend I have a long-running function working on computing my new state.
Meanwhile another action comes in and changes the state while the first one did not finish and is working on stuff.
If I am imagining things correctly there is no actions queue and the state might be resolved in some unpredictable manner.
Should I be worried about this at all?
I don't mean real threads, just a concept for the lack of better wording. Actions are asynchronous and state keys are being accessed by reference.
I was concerned about the same thing so I just did some digging. It looks like two threads concurrently calling dispatch() (if it were possible) could raise an exception. But it shouldn't be possible and that error message points to a particular, different cause. The "actions queue" is in the browser's own event loop. That event loop runs async/interaction callbacks (from which we call dispatch()) one-at-a-time.
That's the responsibility of your own action creators and your own reducers, and heavily related to how you structure your actions and reducers conceptually. The Redux FAQ question on structuring "business logic" is very relevant here:Redux FAQ
Thunk action creators have access to getState, so it's very common to have a thunk check the current state and only dispatch under certain conditions, such as this example:
// An example of conditional dispatching based on state
const MAX_TODOS = 5;
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState();
if(state.todos.length < MAX_TODOS) {
dispatch({type : "ADD_TODO", text : todoText});
}
}
}
Your reducer can also have sanity checks as well:
function todosReducer(state, action) {
switch(action.type) {
case "ADD_TODO": {
if(state.todos.length >= state.maxTodos) {
return state;
}
return {
...state,
todos : state.todos.concat(action.newTodo)
}
}
default : return state;
}
}
Personally, I don't like to have my reducers just blindly merge in whatever data's in the action, unless it's very small (like, say, the name of the currently selected tab or something). I prefer to have a reasonable amount of logic in my action creator to set up the action, a minimal-ish amount of data included in the action itself, and a sufficiently smart reducer to do the work based on that action.

How to share state between 2 combine reducers?

I have 2 reducers that I use and combine them. In the first reducer, I have something that gets all the initial data (which is also relevant for the second reducer).
How do I use the data in the state that I initialize/set from the first reducer to the second one?
function reducer1(state = initialState, action = '') {
switch (action.type) {
case constants.INITIAL_DATA:
returnstate.set('data', fromJS(document.data));
....
Then I combine both of those reducers and I want to access "data" from both of them (or pass the data as initialState to the second reducer).
A reducer should return a plain object and have no side effects. If you want the same state available in 2 different action.type cases, then they either need to be in the same function, or a parent function needs to pass the state to different reducer functions, like so:
function parentReducer(state = {}, action = '') {
switch (action.type) {
case CASE_ONE:
return childReducer1(state, action)
case CASE_TWO:
return childReducer2(state, action)
default:
return state
}
}
But this brings up an important design point: each (top-level) reducer represents a distinct branch of the Redux state tree, and you can probably always design your data store in a way that different parts of the tree don't need to be shared. In Redux (check out the DevTools), you have a single object, and the top-level keys of this object are the names of your top-level reducer functions.
Basically, if you perceive a need to set a different state in a reducer, so other reducers can use that, it evidence of a need to rethink the store's design.

Resources