Redux Intro
Why Redux
-
Simplicity
- Redux is able to cut some complexity corners by using functional composition where Flux uses callback registration
- Api is pretty straightforward and simple without Dispatcher and store registration
-
Minimizes unexpected behaviors
- Redux eliminates state mutations which can lead to unexpected behaviors
-
Ecosystem:
- Easily extendable with middlewares
- supports logging, promises, observables
-
Testability
- Functional approach make it easier to test
-
Developer experience
- Being able to track state changes
- Being able to execute undo/redo with DevMonitor
Three principles of Redux
- Single immutable state tree
- The only way to change state tree is to dispatch an action
- To describe state mutations, you have to write a function that takes the previous state of the app, the action being dispatched, and returns the next state of the app. This function has to be pure. This function is called the "Reducer."
Simple Reducer
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
Redux Store
const { createStore } = Redux;
const store = createStore(counter);
const render = () => {
document.body.innerText = store.getState();
};
store.subscribe(render);
render();
document.addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT' });
});
What is actually createStore
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch({});
return { getState, dispatch, subscribe };
};
Adding React
const Counter = ({
value,
onIncrement,
onDecrement
}) => (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
const { createStore } = Redux;
const store = createStore(counter);
const render = () => {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() =>
store.dispatch({
type: 'INCREMENT'
})
}
onDecrement={() =>
store.dispatch({
type: 'DECREMENT'
})
}
/>,
document.getElementById('root')
);
};
store.subscribe(render);
render();
Testing strategy
The basic test would be checking if the state is not mutating
const toggleTodo = (todo) => {
return {
...todo,
completed: !todo.completed
};
};
const testToggleTodo = () => {
const todoBefore = {
id: 0,
text: 'Learn Redux',
completed: false
};
const todoAfter = {
id: 0,
text: 'Learn Redux',
completed: true
};
deepFreeze(todoBefore);
expect(
toggleTodo(todoBefore)
).toEqual(todoAfter);
};
testToggleTodo();
console.log('All tests passed.');
Example with tests
Delegating control
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;
}
};
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;
}
};
Scaling with reducers composition
The main idea is that you have one root reducer that delegates control to other reducers
There are two approaches to do that:
- Native approach
- Redux approach
const todoApp = (state = {}, action) => {
return {
todos: todos(
state.todos,
action
),
visibilityFilter: visibilityFilter(
state.visibilityFilter,
action
)
};
};
Native approach
Redux aproach
const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});
combineReducers
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](
state[key],
action
);
return nextState;
},
{}
);
};
};
const todoApp = combineReducers({
todos,
visibilityFilter
});
Basic ToDo List in React
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;
}
};
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;
}
};
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};
const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});
const { createStore } = Redux;
const store = createStore(todoApp);
const { Component } = React;
let nextTodoId = 0;
class TodoApp extends Component {
render() {
return (
<div>
<input ref={node => {
this.input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
text: this.input.value,
id: nextTodoId++
});
this.input.value = '';
}}>
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')
);
};
store.subscribe(render);
render();
Extracting components
Container(Smart) Component:
Specifies data and behavior for component but does not know anything about appearance
Presentational(Dumb) Component:
Specify appearance of component but knows nothing about behavior
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;
}
};
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;
}
};
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};
const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});
const { createStore } = Redux;
const store = createStore(todoApp);
const { Component } = React;
const Link = ({
active,
children,
onClick
}) => {
if (active) {
return <span>{children}</span>;
}
return (
<a href='#'
onClick={e => {
e.preventDefault();
onClick();
}}
>
{children}
</a>
);
};
class FilterLink extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const props = this.props;
const state = store.getState();
return (
<Link
active={
props.filter ===
state.visibilityFilter
}
onClick={() =>
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: props.filter
})
}
>
{props.children}
</Link>
);
}
}
const Footer = () => (
<p>
Show:
{' '}
<FilterLink filter='SHOW_ALL'>
All
</FilterLink>
{', '}
<FilterLink filter='SHOW_ACTIVE'>
Active
</FilterLink>
{', '}
<FilterLink filter='SHOW_COMPLETED'>
Completed
</FilterLink>
</p>
);
const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}
>
{text}
</li>
);
const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);
let nextTodoId = 0;
const AddTodo = () => {
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = '';
}}>
Add Todo
</button>
</div>
);
};
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}
class VisibleTodoList extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const props = this.props;
const state = store.getState();
return (
<TodoList
todos={
getVisibleTodos(
state.todos,
state.visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}
/>
);
}
}
const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);
ReactDOM.render(
<TodoApp />,
document.getElementById('root')
);
Extracting components
Passing Store techniques
Passing explicitly via props
Passing implicitly via Context
The problem:
If you have a component tree, store must be passed through props for all components in the tree.
The solution:
Only container components get store from context when defining contextTypes.
Passing via Props
const { createStore } = Redux;
ReactDOM.render(
<TodoApp store={createStore(todoApp)} />,
document.getElementById('root')
);
Passing via Context
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
};
const { createStore } = Redux;
ReactDOM.render(
<Provider store={createStore(todoApp)}>
<TodoApp />
</Provider>,
document.getElementById('root')
);
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
return (
<TodoList
todos={
getVisibleTodos(
state.todos,
state.visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}
/>
);
}
}
VisibleTodoList.contextTypes = {
store: React.PropTypes.object
};
const TodoApp = () => (
<div>
<VisibleTodoList />
</div>
);
React redux
Provider:
Binds store to Context, so child components can get store from this.context if they enable context by passing contextTypes
const { Provider } = ReactRedux;
const { createStore } = Redux;
ReactDOM.render(
<Provider store={createStore(todoApp)}>
<TodoApp />
</Provider>,
document.getElementById('root')
);
React Redux
connect:
Used for generating Container components. Connect specifies contextTypes and subscribes for store changes
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}
const mapStateToTodoListProps = ( state ) => {
return {
todos: getVisibleTodos(
state.todos,
state.visibilityFilter
)
};
};
const mapDispatchToTodoListProps = ( dispatch ) => {
return {
onTodoClick: (id) => {
dispatch({
type: 'TOGGLE_TODO',
id
});
}
};
};
const VisibleTodoList = connect(
mapStateToTodoListProps,
mapDispatchToTodoListProps
)(TodoList);
//////////////////////////////
const mapStateToLinkProps = (
state,
ownProps
) => {
return {
active:
ownProps.filter ===
state.visibilityFilter
};
};
const mapDispatchToLinkProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
const FilterLink = connect(
mapStateToLinkProps,
mapDispatchToLinkProps
)(Link);
Action Creators
Action creator is function that returns the action. Action creator is passed to dispatch
const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
};
};
dispatch(adTodo(input.value))
Middlewares
Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store’s dispatch method for fun and profit
let createStoreWithMiddleware = applyMiddleware(logger)(createStore)
let store = createStoreWithMiddleware(reducer, [ 'Use Redux' ])
Applying middleware and passing it to store
Signature: applyMiddleware(...middlewares)(createStore)
Middleware API
Middleware Signature
({ getState, dispatch }) => next => action
Basically middleware gets two arguments getState and dispatch methods and returns a function with next argument that returns action function. This action function should call next(action) to pass the result to middleware chain. Finally store dispatch will end the chain
Writing Logger middleware
const { createStore, applyMiddleware } = Redux;
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;
}
};
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;
}
};
function logger({ getState }) {
return (next) => (action) => {
console.log('will dispatch', action)
// Call the next dispatch method in the middleware chain.
let returnValue = next(action)
console.log('state after dispatch', getState())
// This will likely be the action itself, unless
// a middleware further in chain changed it.
return returnValue
}
}
let createStoreWithMiddleware = applyMiddleware(logger)(createStore)
let store = createStoreWithMiddleware(todos, [ 'Use Redux' ])
store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
})
// (These lines will be logged by the middleware:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]
Thunk middleware
A thunk is a function that wraps an expression to delay its evaluation
Thunk middleware allows you to write actions that return functions instead of an action object
Thunk middleware usage:
- When you want to delay dispatch
- Dispatch conditionally
Thunk action examples
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
Conditional Dispatch
Delayed Dispatch
The End
Redux Intro
By vladimirnovick
Redux Intro
- 1,574