##Redux Egghead Notas##
####Introducción:#### Manejar el estado de la aplicación es una tarea fundalmental, la cual usualmente se hace de forma desordenada. Redux ofrece una manera de manejar ordenadamente el estado (state) de una aplicación en JavaScript que ayudará a la aplicacion a ser más consistente.
Redux es la evolución de Flux, pero que no tiene la complejidad propia de ésta arquitectura presentada por Facebook. Simplifica la forma de trabajar inspirado en el lenguaje Elm.
#####¿Por qué nació Redux?##### Ayuda a pensar dos conceptos que son abstractos para la mente humana: mutabilidad y funciones asíncronas.
####Primer Principio de Redux:#### Todo lo que pueda cabiar de tu aplicación, incluido los datos y las opciones de UI, están contenidas en un sólo objeto llamado árbol de estados (state tree).
####Segundo Principio de Redux:#### El árbol de estados (state tree) sólo se puede leer, la única manera de murtarlo es emitiendo una acción. Las acciones son un objeto en JavaScript que describen los cambios realizados hasta su más mínimo detalle.
El estado (state) es la representación mínima de la información de la aplicación. La acción es la representación mínima de los cambios que se realizan sobre los datos.
La estructura de una acción es libre de ser configurada por quien la crea, el único requisito es que la propiedad type
esté definida. Es recomendable usar palabras (strings) pues estos serán serializables.
La forma de incluir información en una aplicación construida en Redux es a través de acciones, ya sea mediante inputs del usuario o por operaciones del servidor.
####Funciones Puras vs Impuras:#### Las funciones puras devuelven un valor el mismo valor, siempre y cuando los argumentos sean los mismos. Además por su naturaleza éstas no tienen efectos colaterales como llamadas a la base de datos. Por definición nunca incluirán llamadas a la base de datos o el servidor, pues esto no haría predecible la respuesta que se recibe. Otra característica más es que las funciones puras no modifican los valores que se le pasan.
Las funciones impuras quizán hagan llamadas a la base de datos o el servidor. Así que podrían generar efectos colaterales, de esta misma manera podrían sobreescribir los valores pasados a ellas.
Algunas de las funciones que escribirás en Redux deben ser puras, hay que se consciente de esto.
Es probable que hayas escuchado que las UI (interfaces de usuario) son más predecibles cuando el estado de la aplicación está representado por una función pura. Redux complementa esta aproximación con otra idea: las mutaciones de estado están descritos por funciones puras que toman el estado actual y la acción despachada, para devolver el siguiente estado de la aplicación. Es esencial tener en cuenta que ésta no modifique el estado que se la pasa, sino que debe devolver un nuevo objeto.
####Tercer Principio de Redux:#### Para describir las mutaciones de estado se usa una función pura, ésta toma el estado anterior más una acción despachada para devolver el nuevo estado. Ésta es la función reductora (reducer).
####La función Reductora (reducer):#### La función reductora es una función pura que toma el estado anterior y una acción que ha sido despachada, para después retornar el nuevo estado de la aplicación.
La función reductora deberá manejar los siguientes casos:
- Manejar el caso en el que recibe una acción que no espera y retorna el estado actual.
- Si recibe un estado no definido debe devolver el estado inicial.
######Ejemplo de un contador simple (usando ES6)######
const counter = (state = 0, action) => {
switch (action.type) {
case ‘INCREMENT’:
return state + 1;
case ‘DECREMENT’:
return state - 1;
default:
return state;
}
};
######Ejemplo de un contador simple (usando ES5)######
function counter (state , action) {
if (typeof state === ‘undefined’) {
return 0;
}
switch (action.type) {
case ‘INCREMENT’:
return state + 1;
case ‘DECREMENT’:
return state - 1;
default:
return state;
}
}
const
en ES6 está restringido por el scope, de la misma forma que let
. Previene que se re-asigne y se re-declare. Sin embargo, si un objeto es const
sus keys pueden ser modificadas y otras también se pueden agregar.
####Store:#### The store binds together the three principles of Redux. It holds the current application state object, it lets you dispatch actions, and when you create it you specify the reducer that specifies how state is updated with actions.
To create a store do:
const { createStore } = Redux;
// var createStore = Redux.createStore;
const store = createStore(counter);
// var store = createStore(counter);
The store has three important methods: The first, getState, retrieves the current state of the Redux store.
The second and most commonly used store method, dispatch, allows you to dispatch actions that change the state of your application.
The third method, subscribe, lets you register a callback that the store will call any time an action is dispatched so that you can update the UI of your application to reflect the current state.
Typically we extract the logic that updates the UI from the subscribe callback into a render function which you can pass to subscribe as it’s callback but also which you can call initially to show the initial state.
So now for the simple counter we have:
const render () => {
document.body.innerText = store.getState();
};
store.subscribe(render);
render(); // render the initial state
// on click dispatch an increment action
document.addEventListener(‘click’, () => {
store.dispatch({type: ‘INCREMENT’});
});
####Rewrite the createStore method:#### We can rewrite the createStore method in order to better understand it.
const createStore = (reducer) => {
let state; // the state is not a const but rather a local variable that can be reassigned
let listeners = []; // keep track of all the change listeners because the subscribe function can be called many times, each with a unique listener
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action); // generate the new state
listeners.forEach(listener => listener()); // call each listener
};
const subscribe = (listener) => {
listeners.push(listener); // register each listener for each call of subscribe
return () => {
listeners = listeners.filter(l => l != listener);
}; // return a function for removing the listener from the listeners array by filtering it out
};
dispatch({}); // call dispatch in order to generate the initial state
return { getState, dispatch, subscribe }; // return the store object
};
####Actions should not mutate state:#### You can use the expect library to write test assertions and the deepFreeze library to prevent mutations to ensure your actions don’t mutate state as you’re writing them.
Use the concat array method to avoid array mutations:
const addCounter = (list) => {
return list.concat([0]);
};
Or use the ES6 spread operator to do the same more concisely:
const addCounter = (list) => {
return […list, 0]; // spread the values of list and return a new array with 0 appended at the end
};
Use the ES6 spread operator again to write the removeCounter function concisely while preventing mutations on the list.
const removeCounter = (list, index) => {
return [
…list.slice(0, index),
…list.slice(index + 1)
];
};
Now let's write the incrementCounter method:
const incrementCounter = (list, index) => {
return [
…list.slice(0, index),
list[index] + 1,
…list.slice(index + 1)
];
};
Use deepFreeze in your tests to prevent an object being passed into a function from being mutated.
Use Object.assign() from ES6 to assign the properties from an existing object to a new object. In the future we will be able to use the ES7 Object Spread Operator.
const toggleTodo = (todo) => {
return Object.assign({}, todo, {
completed: !todo.completed
});
};
Note that when multiple objects have the same properties, the last one passed to assign wins.
####TodoList reducer:####
const todos = (state = [], action) => {
switch (action.type) {
case ‘ADD_TODO’:
return [
…state,
{
id: action.id,
text: action.text,
completed: false
}
];
case ‘TOGGLE_TODO’:
// map returns a new array
return state.map(todo => {
if (todo.id !== action.id) {
return todo;
}
// use the object spread operator from ES7 to recompose the modified todo
return {
…todo,
completed: !todo.completed
};
});
default:
return state;
}
};
####Compose Reducers:#### You should compose reducers so they act on parts of the state that they are concerned with. Therefore one reducer calls another.
In the todoList example let's create a todo reducer:
const todo = (state, action) => {
switch (action.type) {
case ‘ADD_TODO’:
return {
id: action.id,
text: action.text,
completed: false
};
case ’TOGGLE_TODO’:
if (state.id !== action.id) {
return state;
}
return {
…state,
completed: !state.completed
};
default:
return state;
}
};
Now the todos reducer (our top-most reducer) calls the todo reducer:
const todos = (state = [], action) => {
switch (action.type) {
case ‘ADD_TODO’:
return [
…state,
todo(undefined, action);
];
case ‘TOGGLE_TODO’:
// map returns a new array
return state.map(t => todo(t, action);
default:
return state;
}
};
Now how would we add another property for specifying which todos are visible?
Let's create a reducer for it:
const visibilityFilter = (
state = ‘SHOW_ALL’,
action
) => {
switch (action.type) {
case ‘SET_VISIBILITY_FILTER’:
return action.filter;
default:
return state;
}
};
Let's also create a new top-most reducer that calls the visibilityFilter reducer and the todos reducer with the parts of the state that they manage and combines the result into the new state object.
const todoApp = (state = {}, action) => {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
};
####Reducer Composition with combineReducers:#### combineReducers takes an object which maps the state properties with the reducers managing them in order to generate the top-most reducer for you.
Let’s apply it to our todoList example:
const { combineReducers } = Redux;
const todoApp = combineReducers({
todos: todos,
visibilityFilter: visibilityFilter
});
####Rewrite combineReducers:####
// reducers is an object of reducers
const combineReducers = (reducers) => {
// combineReducers returns a reducer
return (state = {}, action) => {
return Object.keys(reducers).reduce(
// accumulate the state over every reducer key and call the corresponding reducer
(nextState, key) => {
nextState[key] = reducers[key](
state[key],
action
);
return nextState;
},
{} // initial nextState before any of the keys are processed
);
};
};
####Presentation vs Container Components#### Presentational components (aka dumb components) should only be concerned with how things look and not behaviors. Any click handlers or other behaviors should be passed as properties to the component.
Container components (aka smart components) pass data from the store and specify behaviors that, along with the data, are passed down to presentation components. These container components are the only components that interact with Redux.
In the todoList example the todo-app component is the container component that interacts with the Redux store. It specifies the todo click behavior and passes it to the todo-list presentation component which then passes it to each todo presentation component. The component nesting looks like this:
<todo-app>
<todo-list todos=“visibleTodos” onTodoClick=“store.dispatch({…})”>
<todo completed=“” text=“” onClick=“”>
…
</todo-list>
</todo-app>
Remember that components need to have a single root element.
Having a clean separation between presentation and container components allows you to easily swap redux with another library or framework.
The downside is that you have to pass down behaviors and data via props down the component tree to the leaf components even when the intermediate components don’t use those props themselves. This breaks encapsulation because the parent components need to know about the data the child components need.
This can be fixed by having intermediate container components. A nice side-effect of having these intermediate container components is that the top level container components can be refactored as dumb/presentation components and therefore do not subscribe to the state.
The intermediate container components subscribe to the store and re-render themselves anytime the state is changed. They also dispatch their actions and grab the state via store.getState. Then they use the pieces of the state that they care about.
####Injecting the store into components#### Relying on a top-level store variable is not sustainable or testable. You pass into components via props but that adds a lot of boilerplate.
It’s easier to pass it explicitly via an Angular service or implicitly via a provider context in React. Unfortunately the provider context acts like global variables in the component tree.
####Generating Component Containers with connect method#### Container components can be generated with the redux bindings library’s connect method which takes two functions (mapStateToProps and mapDispatchToProps). The first function calculates the props to be injected into the component from the state and the second calculates the callback props to be injected into the component using the dispatch method on the store.
The connect function merges the props in both functions along with any props passed to the container component when it is used. Then you call the output of connect function with the presentation component that the container component is meant to wrap. The merged props get injected as props into this presentation component.
If the connect function is not passed either of the functions then it will not subscribe to the store (ie. it will not re-render itself when the state changes) but it will inject the dispatch method as a prop to the component so it can dispatch actions as needed.
The first function, mapStateToProps, takes the state as it’s first argument and the container component’s props as the second argument. mapDispatchToProps takes the store’s dispatch method as the first argument and the container component’s props as the second argument.
####Extracting Action Creators#### Action Creators are functions that generate action objects. They are typically kept separate from reducers and components in large Redux applications. This allows other components to be able to generate the same actions.
In the todoList example the action creators would be:
let nextTodoId = 0;
const addTodo = (text) => {
return {
type: ‘ADD_TODO’,
id: nextTodoId++,
text
};
};
const setVisibilityFilter = (filter) => {
return {
type: ‘SET_VISIBILITY_FILTER’,
filter
};
};
const toggleTodo = (id) => {
return {
type: ‘TOGGLE_TODO’,
id
};
};