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).
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 have a very large and complex React application. It is designed to behave like a desktop application. The interface is a document style interface with tabs, each tab can be one of many different type of editor component (there are currently 14 different editor screens). It is possible to have a very large number of tabs open at once (20-30 tabs). The application was originally written all with React class components, but with newer components (and where significant refactors have been required) I've moved to functional components using hooks. I prefer the concise syntax of functions and that seems to be the recommended direction to take in general, but I've encountered a pattern from the classes that I don't know how to replicate with functions.
Basically, each screen (tab) on the app is an editor of some sort (think Microsoft office, but where you can have a spreadsheet, text document, vector image, Visio diagram, etc all in tabs within the same application... Because each screen is so distinct they manage their own internal state. I don't think Redux or anything like that is a good solution here because the amount of individually owned bits of state are so complex. Each screen needs to be able to save it's current working document to the database, and typically provides a save option. Following standard object oriented design the 'save' function is implemented as a method on the top level component for each editor. However I need to perform a 'save-all' function where I iterate through all of the open tabs and call the save method (using a reference) on each of the tabs. Something like:
openTabs.forEach((tabRef) => tabRef.current.save());
So, If I make this a functional component then I have my save method as a function assigned to a constant inside the function:
const save = () => {...}
But how can I call that from a parent? I think the save for each component should live within that component, not at a higher level. Aside from the fact that would make it very difficult to find and maintain, it also would break my modular loading which only loads the component when needed as the save would have to be at a level above the code-splitting.
The only solution to this problem that I can think of is to have a save prop on the component and a useEffect() to call the save when that save prop is changed - then I'd just need to write a dummy value of anything to that save prop to trigger a save... This seems like a very counter-intuitive and overly complex way to do it.... Or do I simply continue to stick with classes for these components?
Thankyou,
Troy
But how can I call that from a parent? I think the save for each component should live within that component, not at a higher level.
You should ask yourself if the component should be smart vs dumb (https://www.digitalocean.com/community/tutorials/react-smart-dumb-components).
Consider the following:
const Page1 = ({ onSave }) => (...);
const Page2 = ({ onSave }) => (...);
const App = () => {
const handleSavePage1 = (...) => { ... };
const handleSavePage2 = (...) => { ... };
const handleSaveAll = (...) => {
handleSavePage1();
handleSavePage2();
};
return (
<Page1 onSave={handleSavePage1} />
<Page2 onSave={handleSavePage2} />
<Button onClick={handleSaveAll}>Save all</button>
);
};
You've then separated the layout from the functionality, and can compose the application as needed.
I don't think Redux or anything like that is a good solution here because the amount of individually owned bits of state are so complex.
I don't know if for some reason Redux is totally out of the picture or not, but I think it's one of the best options in a project like this.
Where you have a separated reducer for each module, managing the module's state, also each reducer having a "saveTabX" action, all of them available to be dispatched in the Root component.
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.
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!