Reputation: 3310
After watching the new egghead course by Dan Abramov, I have question regarding the selectors that was mentioned.
The purpose of the selectors is to hide the details of the state tree from the components, so that it is easy to manage code later if tree changes.
If I understand it correctly, that means, the selectors called inside mapStateToProps
should only be the ones that live in the top-level reducer. Because the state
that is passed to mapStateToProps
is the whole application state tree. If this is true, as the application grows, I can imagine it would become very difficult to manage the top level selectors.
Have I miss understood the concept here? or is this a valid concern?
Edit: trying to make my question clearer.
Say my whole state start with
{ byIds, listByFilter }
and I have
export const getIsFetching = (state, filter) =>
fromList.getIsFetching(state.listByFilter[filter]);
in my top level reducer reducers/index.js
, and components would simply use getIsFetching
passing the whole state to is, which is totally fine because it is the top level.
However, later on, I decided my whole app is going to contain a todo app and an counter app. So it make sense to put the current top level reducers into reducers/todo.js
, and create a new top level reducers reducers/index.js
like this:
combineReducers({
todo: todoReducer,
counter: counterReducer
})
at the point my state would be like
{
todo: {
byIds,
listByFilter
},
counter: {
// counter stuff
}
}
components can no longer use the getIsFetching
from reducers/todo.js
, because the state in getIsFetching
is now actually dealing with state.todo
. So i have to in the top level reducer reducers/index.js
export another selector like this:
export const getIsFetching = (state, filter) =>
fromTodo.getIsFetching(state.todo);
only at this point, the component is able to use getIsFetching
without worring about the state shape.
However, this raises my concern which is all the selectors directly used by components must live in the top-level reducer.
Update 2: essentially we are exporting selectors from the deepest level all the way up to the top-level reducers, while all the exports in the intermediate reducers are not using them, but they are there because the reducer knows the shape of the state at that level.
It is very much like passing props
from parent all the way down to children, while the intermediate component aren't using props
. We avoided this by context
, or connect
.
apologize for the poor English.
Upvotes: 3
Views: 3037
Reputation: 4799
I also came across this issue (and also had a hard time explaining it...). My solution for compartmentalization this follows from how redux-forms
handles it.
Essentially the problem boils down to one issue - where is the reducer bound to? In redux-forms
they assume you set it at form
(though you can change this) in the global state.
Because you've assumed this, you can now write your module's selectors to accept the globalState
and return a selector as follows: (globalState) => globalState.form.someInnerAttribute
or whatever you want.
To make it even more extensible you can create an internal variable to track where the state is bound to in the global state tree and also an internal function that's like getStateFromGlobalState = (globalState) => globalState[boundLocation]
and uses that to get the inner state tree. Then you can change this variable programatically if you decide to bind your state to a different spot in the global state tree.
This way when you export your module's selectors and use them in mapStateToProps, they can accept the global state. If you make any changes to where the where the reducer is bound, then you only have to change that one internal function.
IMO, this is better than rewriting every nested selector in the top level. That is hard to scale/maintain and requires a lot of boilerplate code. This keeps the reducer/selector module contained to itself. The only thing it needs to know is where the reducer is bound to.
By the way - you can do this for some deeply nested states where you wouldn't necessarily be referring about this from globalState
but rather some upper level node on the state tree. Though if you have a super nested state it may make more sense to write the selector from a upper state's POV.
Upvotes: 1
Reputation: 8436
So while mapStateToProps
does take the entire state tree, it's up to you to return what you'd like from that state in order to render your component.
For instance, we can see he calls getVisibleTodos
and passes in state
(and params
from the router), and gets back a list of filtered todos
:
components/VisibleTodoList.js
const mapStateToProps = (state, { params }) => ({
todos: getVisibleTodos(state, params.filter || 'all'),
});
And by following the call, we can see that the store is utilizing combineReducers
(albeit with a single reducer), as such, this necessitates that he pass the applicable portion of the state tree to the todos
reducer, which is, of course, state.todos
.
reducer/index.js
import { combineReducers } from 'redux';
import todos, * as fromTodos from './todos';
const todoApp = combineReducers({
todos,
});
export default todoApp;
export const getVisibleTodos = (state, filter) =>
fromTodos.getVisibleTodos(state.todos, filter);
And while getVisibleTodos
returns a list of todos
, which by is a direct subset of the top-level state.todos
(and equally named as such), I believe that's just for simplicity of the demonstration:
We could easily write another perhaps another component where there's a mapStateToProps
similar to:
components/NotTopLevel.js
const mapStateToProps = (state, { params }) => ({
todoText: getSingleTodoText(state, params.todoId),
});
In this case, the getSingleTodoText
still accepts full state
(and an id
from params
), however it would only return the text of todo
, not even the full object, or a list of top-level todos
. So again, it's really up to you to decide what you want to pull out of the store and stuff into your components when rendering.
Upvotes: 1