Redux as data store
Cristian Bogdan
Purpose
Consider the dinnerplanner Data Model. From an application logic perspective, the model contains the following properties:
{
dishes,
numberOfGuests
}
Is this enough in a highly useable web application?
If the user navigates away, will this data be enough for them to feel effective when returning to the dinner planner? �What if they receive a dish page URL sent by a friend?
From Data Model to State
{
dishes,
numberOfGuests,
searchString ,
currentlyViewedDish (and past?)
currentRoute, previousRoute
}
The state contains the traditional data model, but adds other pieces of data necessary e.g. for improved user experience. This is called State, and is held in a Redux Store.
Different programmers can add new properties to the state (e.g. searchString) independently of each other.
The state is thus composed dynamically similar to the way we compose a user interface from UI components and containers.
Entire objects can be added to the state, which in turn can have their own properties, etc. The Redux state is thus hierarchical. See the Composite pattern.
From methods to Actions (and Reducers)
The traditional way of changing the data Model is by calling a method. The Redux way to change state is by dispatching an action.
State properties and the way they are changed by actions are defined together in a Reducer
To add a new state member, just define a reducer for it!
function numberOfGuests(state=2, action) {
if (action.type === 'SET_NO_GUESTS') {
return action.value;
} else {
return state;
}
}
Note the initial value of the state prop defined as a default value for the function parameter. This is standard JavaScript syntax.
Note that if the reducer does not recognize the action, it returns the state unchanged. This leaves it to other reducers registered in the Redux store to treat the action if they wish. See Chain of responsibility pattern.
Exercise: the dishes reducer
function dishes(state=[], action) {
if (action.type === 'ADD_TO_MENU') {
return ...;
} else if (action.type === 'REMOVE_FROM_MENU') {
return ...;
}
else {
return state;
}
}
Combining reducers
You can combine reducers by simple Javascript mechanisms, no Redux magic involved!
function reducer(state = {}, action) {
return { // create a new object, with the following props:
dishes: dishes(state.dishes, action) ,
numberOfGuests: numberOfGuests(state.numberOfGuests, action)
};
}
The composed reducer returns an object {} whose properties are results of functions. Those functions are the individual property reducers we defined previously. Therefore, at each action, all individual property reducers are applied. Most probably only one will take effect! Individual reducers are applied only to “their” part of the state (e.g. state.numberOfGuests).
TODO
Explain that the store is hierarchical (Composite). The store/state we’ve just created can be added to another parent state, along with other children etc
Add an 1-2 animations on how the state changes when the individual reducers are applied
Putting it all together: the Redux store
const store= Redux.createStore(reducer);
The initial state will be calculated from the reducers themselves by using the initial value returned by the reducers (1 for numberOfGuests, state=1).
To change the state, use dispatch()
store.dispatch({type:'SET_NO_GUESTS', guests:3} )
To read the starte, use getState()
store.getState().numberOfGuests
function numberOfGuests(state=1, action) {
if (action.type === 'SET_NO_GUESTS') {
return action.guests;
} else {
return state;
}
}
This reducer will take effect:
Using Redux in Tutorial Weeks 1-3
Most Redux resources that you find on the Internet assume using Redux together with React and/or in a server-side toolset, e.g. with Webpack or similar. Therefore you will see a lot of npm commands flying around.
To use Redux you do NOT need npm or any kind of server side processing.
Redux is well designed for using separately from React. It was actually invented before. You can use it with Vue, or without any framework, like we do.
Just load Redux in your HTML before you load DinnerModel.js
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script>
Then in DinnerModel.js you can use Redux.createStore with the reducers that you also define in your DinnerModel. As simple as that.
Redux state is immutable
An important principle in Redux is that the state object is never changed. Instead, at each action dispatch, a whole new state is created!
Immutability is an important topic in Javascript. Programmers have noticed that code is much more reliable if functions do not modify (mutate) their object parameters (or their “this” object).
Exercise: look at Array methods and consider which of them mutate the array and what would be the immutable equivalent. Example:
Redux is trivial to implement
newState= reducer(oldState, action)
For each action, Redux basically applies the combined reducer to the current state.
Redux is not complex technology, but rather a useful conceptualization. Once we conceptualize state management in terms of actions and reducers
Other easy-to-implement concepts that make the respective domain easier to manage and code:
Using Redux in the (advanced) labs
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script>
npm NOT needed in week 1,2,3
Design patterns involved in Redux
“lets clients treat individual objects and compositions uniformly”
“send a request to a chain of receivers without having to know which one handles the request.”
“converting the interface of one class into an interface expected by the clients”
More resources
Can be found in Canvas in the context of implementing a simple drawing editor.
See drawing editor React implementation with Redux and
compare it with React example without Redux
Organization
Action Creators:
Single file with all action types:
function addTodo(text) {
return {
type: ‘ADD_TODO’,
text
};
};
store.dispatch(addTodo(‘test’));
store.dispatch({
type: ‘ADD_TODO’,
text: ‘test’
});
// types.js
const ADD_TODO = ‘ADD_TODO’;
export ADD_TODO;
// todoActions.js
import { ADD_TODO } from ‘types’;
function addTodo(text) {
return {
type: ADD_TODO,
text
};
};
function addTodo(text) {
return {
type: ‘ADD_TODO’,
text
};
};
Middleware
“provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.”
Can be used for:
In order to fetch from API like spoonacular, look at thunk.
Middleware: Thunk
“Redux Thunk middleware allows you to write action creators that return a function instead of an action.”
<script src="https://unpkg.com/redux-thunk@2.3.0/dist/redux-thunk.min.js"></script>
<script> const ReduxThunk = ReduxThunk.default; </script>
import { applyMiddleware, createStore, compose } from 'redux'; // not needed if you imported redux with <script>
import thunk from 'redux-thunk'; // not needed if you imported thunk as above
const store = createStore(composedReducer, applyMiddleware(thunk))
// dishActions.js
function getDish(dishId) {
return function(dispatch) {
return spoontacularApiFetch(url).then(dish => {
dispatch({ type: DISH, dish:dish})
})
}
}
// dishController.js
store.dispatch(getDish(dishId))