Redux: Colocating Selectors with Reducers

Dan Abramov
InstructorDan Abramov

Share this video with your friends

Send Tweet
Published 6 years ago
Updated 3 years ago

We will learn how to encapsulate the knowledge about the state shape in the reducer files, so that the components don’t have to rely on it.

[00:00] My map state to props function uses the get visible todos function, and it passes the slice of the state corresponding to the todos. However, if I ever change the state structure, I'll have to remember to update this whole side.

[00:16] Alternatively, I can move the get visible todos function out of my view layer and place it in the file that knows best about the state todos internal structure.

[00:29] The file that determines the internal structure of todos is the file that contains the todos reducer. This is why I'm placing my get visible todos implementation right into the file with reducers, and I'm making it a named export.

[00:47] The convention I follow is simple. The default export is always the reducer function, but any named export starting with get is a function that prepares the data to be displayed by the UI. We usually call these functions selectors because they select something from the current state.

[01:06] In the reducers, the state argument corresponds to the state of this particular reducer. I'll follow the same convention in selectors, where the state argument will correspond to the state of the exported reducer in this file.

[01:21] Going back to my component, I still depend on the state structure because I read the todos from the state. The actual method of reading todos may change in the future.

[01:32] I am opening the file that contains the root reducer, and I add a named selector export there, as well. It is also called get visible todos, and it also accepts the state and the filter, but the state corresponds to the state of the combined reducer.

[01:51] Now I want to call the get visible todos function defined in the todos file alongside the reducer, but I can't use a named import because I have function with exactly the same name in the scope.

[02:04] This is why I'm using the name space import syntax that puts all the exports on an object, called from todos in this case, so I can use from todos.get visible todos to call this function I defined in the other file, and pass the slice of the state corresponding to the todos.

[02:27] Now I can go back to my component, and I can import get visible todos from the root reducer file. It encapsulates all the knowledge about the application state shape, so I can just pass it the whole state of my application, and it will figure out how to select the visible todos according to the logic described in selectors.

[02:51] Let's recap how I co-locate the selectors with the reducers. Inside map state to props, I call get visible todos, and I pass it the whole application state.

[03:03] The fresh value of the state will be passed anytime it changes to the map state to props function. I import get visible todos selector, and notice the curly brace. This is a named import from the file that defines the root reducer.

[03:18] In the reducer files, I started adding new methods with get prefix that are exported as named exports, as opposed to the reducer, which is exported as a default export. It's easy to tell a named export because it doesn't use the default keyword after the export keyword.

[03:38] I want to limit the knowledge about the exact state shape to the files contained in the reducers that manage this state. Todos is one of the reducers from which the root reducer is combined, so we know that its state is available as a state todos under the todos key.

[03:57] However, the state shape of the todos reducer should be encapsulated in the file where it's defined. This is why I'm delegating the further selection to the get visible todos I get from the from todos object that I import as a name space import from the todos file, so I get all the named exports from it.

[04:19] In the todos file, I define my reducer as the default export. But I also define the get visible todos implementation as a named export, and this time, the state refers to the state of just the corresponding reducer, which in this case is an array of todos.

[04:39] If I later want to change it to be something other than an array, I can change the get visible todos implementation. But I won't have to touch my components, because they don't rely on the state shape anymore.

Pavel Dolecek
Pavel Dolecek
~ 6 years ago

Discussion here https://twitter.com/dan_abramov/status/664581975764766721

Kristian
Kristian
~ 6 years ago

In a large application won't this mean there are possibly hundreds of selector functions in the root reducer?

I wonder if there is a way to import selectors from child reducers via an import * and apply the relevant slice of the state tree to them?

Afzal
Afzal
~ 5 years ago

In reducers/index.js why do we not have curly braces here?

export const getVisibleTodos = (state, filter) =>
    fromTodos.getVisibleTodos(state.todos, filter);

If I put curly braces it gives me an error (cannot read map of undefined in todolist ie not getting todos in todolist.js) and doesn't work

// doesn't work
export const getVisibleTodos = (state, filter) => {
    fromTodos.getVisibleTodos(state.todos, filter);
};
Deep
Deep
~ 5 years ago

I think it's a matter of being implicit vs explicit. What happens if you put a return before fromTodos... in your example?

Enoh Barbu
Enoh Barbu
~ 4 years ago

@strajk thanks

J. Matthew
J. Matthew
~ 3 years ago

I wonder if there is a way to import selectors from child reducers via an import * and apply the relevant slice of the state tree to them?

Isn't that exactly what's happening in this lesson?

J. Matthew
J. Matthew
~ 3 years ago

In reducers/index.js why do we not have curly braces here?

export const getVisibleTodos = (state, filter) =>
    fromTodos.getVisibleTodos(state.todos, filter);

As @Deep suggested, this is an example of an implicit return. The ES6 arrow-function syntax allows you to skip the curly braces and the return keyword if the only thing you're doing inside the function is returning something. The => <value>; is equivalent to => { return <value>; }.

If I put curly braces it gives me an error (cannot read map of undefined in todolist ie not getting todos in todolist.js) and doesn't work

// doesn't work
export const getVisibleTodos = (state, filter) => {
    fromTodos.getVisibleTodos(state.todos, filter);
};

As you might guess from the above, the reason it doesn't work is because, if you do use the curly braces, then you also need to use the return keyword to do the actual returning. You're getting an error because the function is expected to return something, and suddenly it's not.

// works
export const getVisibleTodos = (state, filter) => {
    return fromTodos.getVisibleTodos(state.todos, filter);
};