As I finish my first React apps I've decided to learn Redux. The one thing I learned working on those small projects (you can find them here in my github) is that managing the state of the app is not easy. Redux should fix that.
For this I'll use Dan Amabrov's course.
Redux creates a single source of truth. The whole state of the application is stored in a plan Javascript object, it is inmmutable, meaning it cannot be changed unless through a very specific method.
The state is read-only. The only way to change it is to dispatch an action.
Actions are a plan Javascript object that represent the action dispached to change our state object. The most simple action is the one that only contains the 'type', which is a string that identifies the kind of action that is beign used.
More complex actions can be created for bigger applications. This actions have a type plus other information that is required to create the new state.
"Any data that gets into the state of the application gets there through actions". This is the only way to change the state tree.
Reducers are pure functions accept state and actions as arguments and return a new state. Any state mutations should be done through reducers.
IMPORTANT: pure functions only use their arguments and don't modify the original arguments given, they return new ones.
const counter = (state = 0, action) => {
switch (Action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
What is going on here? In this basic reducer function we are decreasing or increasing the state by one. Three things are important to notice:
- The state is initialized in the first line with an ES6 default argument syntax.
- We handle the INCREMENT action type correctly as well as DECREMENT
- We have an option for when an action is not defined correctly: the default. This only returns the current state.
Creating a store.
const { createStore } = Redux;
//or
import { createStore } from 'redux'
The store binds together the 3 core principles of redux. It stores the state tree. After that we have to create the reducer function.
const { createStore } = Redux;
const store = createStore(counter);
Counter is the function we created previously. It is a reducer. The store has 3 important methods.
Retrieves the current state of the store (0 in this case).
const { createStore } = Redux;
const store = createStore(counter);
console.log(store.getState());
The most used one. It dispatches actions to change the state of our application. It takes in the action object.
const { createStore } = Redux;
const store = createStore(counter);
console.log(store.getState());
store.dispatch({ type: 'INCREMENT'});
It lets you register a callback that the Redux store is going to call everytime an action has been dispatched so that you can update the UI with the new state.
const { createStore } = Redux;
const store = createStore(counter);
console.log(store.getState());
const render = () => {
document.body.innerText = store.getState();
}
store.suscribe(render);
render();
document.addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT' });
});
So what is going on here? We created an event listener. Everytime we click it is going to dispatch an action of type increment, then the state changes. Before that we suscribed the render method, meaning that everytime that we execute the action the render method is going to happen as a callback. In this method Dan renders the state to the browser.
suscribe => bind action to click => action => new state => callback(render)
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = redicer(state, action);
listeners.forEach(listener => listener());
};
const suscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch([]);
return { getState, dispatch, suscribe };
};
IMPORTANT: this function is already provided by redux, this is a recreation to see how it works under the hood.
In here the only argument it needs is the reducer. Has the three main methods (getstate, dispatch, suscribe). We have an array with all the listeners. We see in the dispatch method that everytime it gets call it loops through the listeners array and calls each one. That way we can trigger all of them at every action. The suscribe method has a way to unsuscribe too. The dummy action at the end is to populate is the initial state.
As previously explained, the state should be immutable. In order to keep with this principle we need to make sure we use the right methods. For example: concat()
slice()
and the spread operator ...
.
Dan uses deepfreeze and expect libraries to test for this.
Example
wrong
const addCounter = (list) => {
list.push(0);
return list;
}
right
const addCounter = (list) => {
return list.concat([0]);
}
also right
const addCounter = (list) => {
return [...list, 0];
}
For more information on the spread operator see this.
const todos = (state = [], action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo => {
if (todo.id !== action.id) {
return todo;
}
return {
...todo,
completed: !todo.completed
};
});
default:
return state;
}
};
Here we can see all the important aspects of a reducer. Specifically the use of the spread operator in order to avoid mutation. On a side note, it is recommendable to have one reducer that calls other reducers that abstract how an action should be handled. This is not necessary but its good practice and can help with maintainability and readability.
//this is completely new, its an abstraction
const todo = (state, action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo => {
if (state.id !== action.id) {
return state;
}
return {
...state,
completed: !state.completed
};
});
default:
return state;
}
}
const todos = (state = [], action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
// this changed compared to the previos example
todo(undefined, action)
];
case 'TOGGLE_TODO':
//this also changed
return state.map(t => todo(t, action));
default:
return state;
}
};
Ok this can be a lot. At least it took me a while to understand. This peice of code does exactly the same as the previous one. So what changed? we extracted the TODO creation/modification. For this we created a new function called todo, while todoS still exists but only to call todo with the right arguments.
Go through the code and check the comments. We could refer to todos as a parent function, it still has the switch and it will call todo with the right arguments to perform the correct action. Note that in the todo function the first argument is still called state, altough it isn't the state, it is, in the case of TOGGLE_TODO, one of the todos beign looped through.
In simpler words. Reducer composition is extracting functions to perform our state changing tasks more clearly. That makes it easier to mantain and read. Ish.
Personal opinion
This example that Dan uses is a clear view of what I don't like about Redux, it is unnecessarily complicated. Extracting functions to make code readable is pretty normal but Dan's use of terminology and naming is too complicated. The same thing happens with Redux all around. Why call it state in the case of TOGGLE_TODO, why not abstract that and call the argument todo? I don't know, maybe someone with more experience could enlight me. Also, breaking up the visual format of the arguments of the reducer took me off guard, again, nothing crazy, but was it really necessary?
rant over
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state;
}
};
Here we created a new function. We are going to combining all reducers into a single one for ease of use. This one right here takes care of the visibility filter, only the visibility filter.
const todoApp = (state = {}, action) => {
return {
todos: todos(
state.todos,
action
),
visibilityFilter: visibilityFilter(
state.visibilityFilter,
action
)
};
};
This is the combiner. Look at what it returns: a single object. So, in the return it is going to call all the reducers (in this case the todos reducer and the visibilityFilter reducer) and then combines them all in one object which it returns. Thats why the filter reducer only changes action.filter, and doesn't need any spread operator or state.
This pattern helps scale Redux apps.
const { combineReducers } = Redux;
const todoApp = combineReducers({
todos: todos,
visibilityFilter: visibilityFilter
});
This pattern is so common that is present in most redux apps so redux provides a function to create the top level reducer. This way we can avoid typing the todoApp
function we just used. The only argument it needs is an object, this lets you specify the mapping between the state field names and the reduces managing them. Thats all it does.
const { component} = React;
let nextTodoId = 0;
class TodoApp extends Component {
render() {
return (
<div>
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
text: 'Test',
id: TodoId++
});
}}>
Add Todo
</button>
<ul>
{this.props.todos.map(todo =>
<li key={todo.id}>
{todo.text}
</li>
)}
</ul>
</div>
);
}
}
const render = () => {
ReactDOM.render(
<TodoApp
todos={store.getState().todos}
/>,
document.getElementById('root')
);
}
This is basic example with React. On the render function we get all the todos from the store and pass them as props. Then we render all of them inside a list using the map method. The button adds a new one.
Dan goes onto extracting presentational and container components. The most interesting the part of this is the 'callback hell'. Passing tons of props and callbacks through parent components defeats the purpose of Redux, so the ideal method is to obtain the state in the container functional components directly from the store and pass it to the presentational components that are children to it as props.
Also, instead of suscribing the whole app to the store, we can suscribe to it in the componentDidMount lifecicle for efficiency.
class FilterLink extends Component {
componentDidMount() {
this.unsusbcribe = store.suscribe(() =>
this.forceUpdate();
);
}
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const props = this.props;
const state = store.getState();
return (
...
)
}
What is important here is to realize that you can access the state of the whole app from anywhere, to keep it clean we do it within the container components, so they're clean and easy to use and also so that the presentational components are easy to reuse.
We first tried passing the store as props to every container component but that doesn't scale well. The best practice is to create a Provider. The react feature 'context' is used here, this makes the context object available to every children (with const { store } = this.context;
).
class Provider extends Component {
getChildContext() {
return {
store: this.props.store;
}
}
render() {
return this.props.children;
}
}
//this is essential for context to be turned on
Provider.childContextTypes = {
store: React.PropTypes.object
};
//make sure you add a similar thing in the container components
ContainerComponent.contextTypes = {
store: React.PropTypes.object
};
//the context should be accessed as this.context
ReactDOM.render(
<Provider store={createStore(todoApp)}>
<TodoApp />
</Provider>,
document.getElementById('root');
)
import { Provider } from 'react-redux';
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsuscribe = store.suscribe(() =>
this.forceUpdate()
);
}
componentWillMount() {
this.unsuscribe();
}
render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
return (
...
)
}
};
const mapStatetToProps = (state) => {
return {
todos: getVisibleTodos(
state.todos,
state.visibilityFilter
)
};
};
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch({
type: 'TOGGLE_TODO',
id
})
}
}
}
import { connect } from 'react-redux';
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
So here is the actual way to do it. The connect function does almost everything for you! by declaring the 2 functions that we need (stateToProps and dispatchToProps) we only need to pass the presentational component to the function and voila.