The common pattern to declare actions in Redux is to assign constant to a string.
const ACTION = 'ACTION';
But in a real world, this approach makes the actions part of your app bloated. It’s hard to add and write such low-level stuff. I saw and wrote by myself a lot of that ridiculous constants.jss that contains only strings bounded to consts. Well, it makes me feel old school and COBOLish, but that’s the only benefits of this approach to me.
I believe, it’s not a good pattern for real life applications.
What is your way to declare and reuse actions?
And why the Redux lib was designed so?
What specifically isn't a good pattern? Constants? Strings?
Nothing mandates action types must be strings–that's a convention. Types can be symbols, objects, or anything else that can be used to make decisions in the code.
FSAs are explicitly intended to be human-friendly, and humans are good at reading text.
Check out the Reducing Boilerplate section, e.g., any of the action creator libraries.
You can use the redux-ducks proposal to make your actions/reducers more modular and maintainable. No more constants.js files full of action types.
https://github.com/erikras/ducks-modular-redux
As for reusing actions, you could use redux thunk/saga to create chains of actions. this way you can reuse actions multiple times.
Well Personally even I was really irritated by declaring a constants file. It was bloating my project and the bundle size kept on increasing.
I would say if you have a reducer doing same work for different actions you should definitely take a look at higher order reducers. The link is from redux docs itself.
I will try to explain this with the help of following code.
Consider you have constants file in which I do
export default RECEIVE_DICT_2 = 'RECEIVE_DICT_2'
export default RECEIVE_DICT_1 = 'RECEIVE_DICT_1'
Consider you have a reducers
First Reducer
const state = {
dict : {},
}
function firstReducer(state = state,action){
switch(action.type){
case 'RECEIVE_DICT_1':
return Object.assign({},state,{
dict : action.dict
})
}
Second Reducer
const state = {
dict : {},
}
function secondReducer(state = state,action){
switch(action.type){
case 'RECEIVE_DICT_2':
return Object.assign({},state,{
dict : action.dict
})
}
Now if you see both the reducers do exactly same the thing but while dispatching actions you need to specify each keys since we specified this in constants.
So you actions would be like
firstAction(dict){
return {
action:'RECEIVE_DICT_1',
dict
}
}
secondAction(dict){
return {
action:'RECEIVE_DICT_2',
dict
}
}
Clearly this is too much code bloat. So here we can use higher order reducers and just throw away that constants file.
Now your reducer would something like this
export default function parentReducer(code){
return function commons(state = state,action){
switch(action.type){
case `RECEIVE_DICT_${code}`:
return Object.assign({},state,{
dict : action.dict
})
}
}
}
I hope you get the picture. You can read more about this in the link I shared above.
Related
I'm using Vue 3 with Pinia and want to use the composition API inside my Pinia stores. Given the following example store
export const useMyStore = defineStore("my", () => {
const currentState = ref(0);
return { currentState };
}
);
I can directly modify the value of currentState. I'm not sure if this is the way to go or if I should prevent this and provide some get and set functions like
export const useMyStore = defineStore("my", () => {
const currentState = ref(false);
const readonlyCurrentState = computed((): Readonly<boolean> => currentState.value);
function changeState(newState: boolean) {
currentState.value = newState;
}
return { readonlyCurrentState, changeState };
}
);
The options API solves by problem by providing getters and actions, so I thought if I should achieve the same here.
This might be an opinion based question but I would like to know if I should encapsulate the state currentState to prevent corruption by accident.
I can directly modify the value of currentState
Yes, this is intentional. As described in comparison with Vuex
mutations no longer exist. They were very often perceived as extremely verbose. They initially brought devtools integration but that is no longer an issue.
This is more technical reason but you can read more from author himself (and people who doesn't like this new "freedom") in this discussion: Add an option to disallow direct state modification from component
Generally the code in your question demonstrates the point perfectly - lot of unnecessary code just to change simple boolean value. Of course there are cases where data is more complex, you want to enforce some constraints etc. where unrestricted direct mutation can be considered dangerous. I think the pattern in your question is perfectly viable and you can find more ideas (and experimental "strict mode" support commit waiting for feedback) in the linked discussion
Personally I think a lot of the safety checks can be done with linting and especially TypeScript definitions. You get the additional safety during development without paying the price at runtime (in terms of additional code executing and being bundled&served). Great thing about Pinia is that it gives us the choice of how to approach the problem...
I'm trying to learn react with redux, I've read in the main documentation of redux, we should store actions types into one js files and then export all constants and use them into type, for example it's simple mode:
export function itemsAction(items) {
return {
type: 'ITEMS',
data: items
}
}
But apparently I should do in this format:
import { types } from './Types';
export function itemsAction(items) {
return {
type: types.ITEMS,
data: items
}
}
But what's the main benefit of this trick? actions are fixed and never changes, why I should set the action types from variables and import all types ?
Thanks
Because it's good to know you're using an object property rather than a raw string, since raw strings are prone to typos. Take your example:
export function itemsAction(items) {
return {
type: 'ITEMS',
data: items
}
}
Now let's say I have a reducer to handle that action that also uses a raw string:
const todos = (state = [], action) => {
switch (action.type) {
case 'ITESM':
return state.map(item => item.something = "potato")
default:
return state
}
}
In the reducer, I misspelled the action type for its case. But due to how Javascript and Redux work, it won't get noticed as an error in your IDE linting, nor will it produce any errors when you execute the code. You'll dispatch the action, and it will silently fail. Nothing will happen, no action, no errors, nothing. This can be a pain to debug.
In addition, what if you want to change the name of an action. It's good to just go change the value in the object, rather than do a big find and replace across your whole project, hoping you don't overwrite something you didn't mean to in the process.
Plus it's just nice to have all your action types in one place, so you can go through and see what you have available, rather than looking through all your action files. Once your project grows, you'll be glad you did it.
Also, on Typescript it's especially useful, since if you try to use an action you haven't defined it won't even build, so you get some build time safety.
This may be a duplicate of this question.
My answer though
Action types are going to be referred to several times in your code. For example, in your reducer, you may also refer to your action type:
import {
types
} from './actions'
...
function exampleReducer(state, action) {
switch (action.type) {
case types.ITEMS:
// do something here
default:
return state
}
}
Then you don't want to write type: 'ITEMS' again there. You want to write types.ITEMS to avoid repeating yourself. It's always a good practice to refer to constant variables rather than writing them over again in plain strings.
Types get especially hard to remember when you have multiples of them and usages in multiple files. So it's really good for you to have action types as constants.
It's a good habit to see your action types in one file sorted out so that you can always have a look over it and change them as needed (as opposed to changing 'ITEMS' to 'ITEM' in your 4 files if you were to change the name of the action type).
ETC
You usually do something like
import {
ITEMS,
...more action types
} from './actions'
and refer to it directly (which is more concise):
export function itemsAction(items) {
return {
type: ITEMS,
data: items
}
}
It's beneficial to keep actions in a separate file because it reduces the code duplication. Your actions names will be required in actions, reducers (possibly multiple reducers) and epics (if you use redux-observable). Always copy-pasting action name string is error prone, you can easily end up with a typo.
If you put action names in a separate file, you can be more confident about using them in the system and refactoring later since you have a single point of truth.
If you decide to use TypeScript, try something like typesafe-actions. TypeScript eliminates the need for having action names in a separate constants file since action name can become a real type. This opens up a lot of things like having intellisense when writing actions in reducers and epics
In this Redux: Colocating Selectors with Reducers Egghead tutorial, Dan Abramov suggests using selectors that accept the full state tree, rather than slices of state, to encapsulate knowledge of the state away from components. He argues this makes it easier to change the state structure as components have no knowledge of it, which I completely agree with.
However, the approach he suggests is that for each selector corresponding to a particular slice of state, we define it again alongside the root reducer so it can accept the full state. Surely this implementation overhead undermines what he is trying to achieve... simplifying the process of changing the state structure in the future.
In a large application with many reducers, each with many selectors, won't we inevitably run into naming collisions if we're defining all our selectors in the root reducer file? What's wrong with importing a selector directly from its related reducer and passing in global state instead of the corresponding slice of state? e.g.
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, todo(undefined, action)];
case 'TOGGLE_TODO':
return state.map(t => todo(t, action));
default:
return state;
}
};
export default todos;
export const getVisibleTodos = (globalState, filter) => {
switch (filter) {
case 'all':
return globalState.todos;
case 'completed':
return globalState.todos.filter(t => t.completed);
case 'active':
return globalState.todos.filter(t => !t.completed);
default:
throw new Error(`Unknown filter: ${filter}.`);
}
};
Is there any disadvantage to doing it this way?
Having made this mistake myself (not with Redux, but with a similar in-house Flux framework), the problem is that your suggested approach couples the selectors to the location of the associated reducer's state in the overall state tree. This causes a problem in a few cases:
You want to have the reducer in multiple locations in the state tree (e.g. because the related component appears in multiple parts of the screen, or is used by multiple independent screens of your application).
You want to reuse the reducer in another application, and the state structure of this application is different from your original application.
It also adds an implicit dependency on your root reducer to each module's selectors (since they have to know what key they are under, which is really the responsibility of the root reducer).
If a selector needs state from multiple different reducers, the problem can be magnified. Ideally, the module should just export a pure function that transforms the state slice to the required value, and it's up to the application's root module files to wire it up.
One good trick is to have a file that only exports selectors, all taking the state slice. That way they can be handled in a batch:
// in file rootselectors.js
import * as todoSelectors from 'todos/selectors';
//...
// something like this:
export const todo = shiftSelectors(state => state.todos, todoSelectors);
(shiftSelectors has a simple implementation - I suspect the reselect library already has a suitable function).
This also gives you name-spacing - the todo selectors are all available under the 'todo' export. Now, if you have two todo lists, you can easily export todo1 and todo2, and even provide access to dynamic ones by exporting a memoized function to create them for a particular index or id, say. (e.g. if you can display an arbitrary set of todo lists at a time). E.g.
export const todo = memoize(id => shiftSelectors(state => state.todos[id], todoSelectors));
// but be careful if there are lot of ids!
Sometimes selectors need state from multiple parts of the application. Again, avoid wiring up except in the root. In your module, you'll have:
export function selectSomeState(todos, user) {...}
and then your root selectors file can import that, and re-export the version that wires up 'todos' and 'user' to the appropriate parts of the state tree.
So, for a small, throwaway application, it's probably not very useful and just adds boilerplate (particularly in JavaScript, which isn't the most concise functional language). For a large application suite using many shared components, it's going enable a lot of reuse, and it keeps responsibilities clear. It also keeps the module-level selectors simpler, since they don't have to get down to the appropriate level first. Also, if you add FlowType or TypeScript, you avoid the really bad problem of all your sub-modules having to depend on your root state type (basically, the implicit dependency I mentioned becomes explicit).
I'm building a mobile app with react-native and redux, I'm organizing my project structure by features in this way:
Component1/
---Component1Actions.js
---Component1Reducer.js
---...
Component2/
---Component2Actions.js
---Component2Reducer.js
---...
In my opinion this kind of project structure is amazing for many reasons, first of all for its great scalability.
Only problem I have come across so far is when 2 different components have to dispatch the same actions (such as a simple text change in a textbox).
It wouldn't make sense to rewrite the exact same action in 2 different files and I also know that importing an action from one component to another component is really a bad practice.
I thought about keeping these kind of "shareable" actions in a global module and then import them in the various components but I'm not sure if it is a good practice.
I would like to know the best way to handle this kind of situations.
Thanks in advance.
You can handle the same "ACTION_TYPE" in multiple reducers.
... actions are by design global. They are not meant to be tied to a particular reducer (Dan Abramov)
You could handle an "LOGOUT" action in all your reducers, which would just return the initial state.. and set the application to defaults
for example..
const postReducer = (state = initial, action) => {
swtich(action.type) {
...
case "LOGOUT":
return initial
default:
return state
}
}
Define it in common reducers.js, actions.js.
In compose pass it to REACT component connect(mapStateToProps, {commonAction, ...actions})
Redux documentations says I should make actions and action creators, like this:
function addTodo(filter) {
return {
type: SET_VISIBILITY_FILTER,
filter
}
}
Then write reducers, like this:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
}
}
I then invoke the action using dispatch:
store.dispatch(addTodo("Ask question on stackoverflow"));
It seems there's a one-one correspondence between actions and reducers; the sole purpose of the action is to select a reducer and provide input data for that reducer.
Why don't we skip the middle man and identify actions with reducers and action creators with functions producing reducers? Then dispatch would take a single argument, a reducer/action of type State => State:
// Action/reducer. (Parametrised state transformer, really.)
const addTodo = text => state => {
return Object.assign({}, state, {
visibilityFilter: action.filter
});
}
// Dispatch takes as a argument the action/reducer
store.dispatch(addTodo("Ask question on stackoverflow"));
You'd lose the ability to serialise actions, but otherwise, it seems you'd get rid of boilerplate action creators and express more clearly the connection between actions and reducers. If you're in Typescript, you also get typechecking of the data in actions, which is difficult to express otherwise.
So what reasons for having actions as data am I missing?
The main purpose of action in Redux is to reduce the state.
Reduce method will be called on array of actions (thats why it called a reducer). Example:
import reducer from './reducer';
const actions = [
{type: 'INIT'},
{type: 'SOME_ACTION', params: {...}},
{type: 'RECEIVE_DATA', data: [...]},
{type: 'SOME_ANOTHER_ACTION', params: {...}},
{type: 'RECEIVE_DATA', data: [...]},
...
];
const finalState = actions.reduce(reducer, undefined);
Action creators is a function that can create actions. It is not necessary that action creator will create only one action.
Actually, if your reducer is able to receive functions instead of objects - your actions will be functions and it will do the main purpose, but you can loose some benefits of Redux functional abilities.
In that case the reducer will be implemented like this:
function reducer(state, action) {
return action(state);
}
The reasons why you may create actions as {type: 'ACTION_NAME'} format:
Redux DevTools expects this format.
You need to store sequence of actions.
Reducer makes state transformations on worker.
Everybody in Redux ecosystem use this format. It's a kind of convention.
Hot reloading abilities (your stored functions will not be reloaded).
You need to send actions as is on server.
Debugging benefits - to see the stack of actions with actions names.
Writing unit tests for reducer: assert.equal(finalState, expectedState).
More declarative code - action name and parameters are about "what to do" and not about "how to do" (but addTodo('Ask question') is declarative too).
Note about coupling between action creators and state changes
Just compare two notations:
First:
function someActionCreator() {
return {
type: "ADD_TODO",
text: "Ask question on stackoverflow"
}; // returns object
}
Second:
function someActionCreator() {
return addTodo("Ask question on stackoverflow"); // returns function
}
"In both cases we see that code is declarative and action creator is decoupled from state change. You can still reuse addTodo or dispatch two addTodo's or use middleware or dispatch compose(addTodo('One'), addTodo('Two')). The main difference is that we created Object and Function and place in code where state changes.
There is NOT a one-to-one mapping between actions and reducers. Per Dan Abramov's comments at https://github.com/Pitzcarraldo/reduxible/issues/8 :
It reinforces a very common misconception about Redux: namely that action creators and reducers are one-to-one mapping.
This is only true in trivial examples, but it is extremely limiting in real applications. Beginners exposed to this pattern couple reducers and action creators, and fail to realize they're meant to be many-to-many and decoupled.
Many reducers may handle one action. One reducer may handle many actions. Putting them together negates many benefits of how Flux and Redux application scale. This leads to code bloat and unnecessary coupling. You lose the flexibility of reacting to the same action from different places, and your action creators start to act like “setters”, coupled to a specific state shape, thus coupling the components to it as well.
As for actions and the "type" parameter, the other answers are right. That's deliberately how Redux was designed, and that was intended to give the benefits of serialization for debugging purposes.
Good question.
Separating actions from state changes is really a Flux pattern, rather than a specifically Redux thing. (Though I will answer the question with reference to Redux.) It's an example of loose coupling.
In a simple app, tight coupling between actions and state changes might be fine. But in a larger app, this could be a headache. For instance, your addTodo action might trigger changes in several parts of the state. Splitting actions from state changes - the latter performed in reducers - allows you to write smaller functions, which are easier to reason about and more testable.
Additionally, decoupling your actions and state changes allows your reducer logic to be more reusable. e.g. Action X might trigger state changes A and B, whilst action Y only triggers state change A.
Furthermore, this decoupling gives rise to a Redux feature called middleware. Middleware listens to action dispatches. It doesn't change the state of the app, but it can read the current state and the next state, as well as the action information. Middleware is useful for functionality extraneous from the core application logic, e.g. logging and tracking (dev tools were mentioned in a previous answer).
[UPDATE] But why have actions as objects?
If it were simply a matter of functions calling other functions, that decoupling would be much less explicit. Indeed it may even get lost; since most actions only enact one state change, developers might tire of a single function calling a single function and do away with the separation entirely.
The other thing is the Flux data flow model. One-way data flow is very important to the Flux/React paradigm. A typical Redux/React goes something like this: Store state -> Higher order React components -> Lower order React components -> DOM. The action is the aberration in the model; it's the backwards arrow transmitting data from the view to the store. It makes sense to make the action something as loud and emphatic as possible. Not a mere function call, but a dispatched object. It's as if your app were announcing Hey! Something important happened here!