1 of 17

Redux as data store

Cristian Bogdan

2 of 17

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?

3 of 17

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.

4 of 17

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.

5 of 17

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;

}

}

6 of 17

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).

See also Redux combineReducers()

You can combine reducers in many other ways

7 of 17

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

8 of 17

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:

9 of 17

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.

10 of 17

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!

  • numberOfGuests reducer does not change its state parameter (it could not, it is an integer)
  • dishes reducer must not change its state parameter (it could e.g. add to the array but it should not!)
  • Composed reducer does not change its state parameter (it is an Object so it could change its properties) but creates a new object at each invocation: return {...};

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:

  • array.push(newElement) is an Array method that mutates the array and returns the new length
  • [...array, newElement] is the immutable equivalent. It creates a new array. This immutable option should be used with Redux!

11 of 17

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

  • state becomes much easier to manage, all actions go through a central gatekeeper
  • a number of utilities (like combineReducers() ) can be written

Other easy-to-implement concepts that make the respective domain easier to manage and code:

  • Hyperscript (much easier to code with than innerHTML or DOM createElement() + append() )
    • Custom events, custom components, JSX...
  • Promises (fetch() much easier to work with than XMLHttpRequest)
    • Not very easy to implement a promise based on XHR, but doable. Try :)

12 of 17

Using Redux in the (advanced) labs

  • model has a single property: store= Redux.createStore(yourReducer)
    • setNumberOfGuests() (week 1), addToMenu(), removeFromMenu() (week 2)... just dispatch an action
    • Getter methods use store.getState()
    • This means that you are wrapping the Redux store (Adapter design pattern)
    • Regarding remote data (fetch()): it is typical to change the Redux state when data has arrived. That is not required for now.
  • addObserver() can use store.subscribe().
  • In week 2-3 you can connect the Redux store to React (or Vue) using react-redux and get more advanced points
    • Details about react-redux come in a later advanced lecture
    • Optionally you can change the Redux state if remote data has arrived

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script>

npm NOT needed in week 1,2,3

13 of 17

Design patterns involved in Redux

  • Composite:
    • composing the state
    • composing the overall reducer from several state property reducers
    • this is the classic way to combine Redux reducers, but other ways are possible
  • Chain of Responsibility
    • The action is dispatched to all reducers and each reducer can choose to treat it or not
    • This is similar to UI event capturing and bubbling (introduced in the Interaction lecture)
    • Logging is another example of this pattern. The action is logging at different levels (warning, info, error). Different loggers will act on it differently, depending on their logging level.
  • Adapter (wrapper) (lab1 suggestion)
    • a solution for using existing lab tests and code with the existing model code but prepare the Redux store for later use (e.g. with react-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

14 of 17

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

15 of 17

Organization

Action Creators:

  • “Action creators are functions that create actions”
  • This makes actions portable and easy to test.

Single file with all action types:

  • Makes types portable and DRY

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

};

};

16 of 17

Middleware

“provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.”

Can be used for:

  • Logging
  • Crash reports
  • Communicating with asynchronous API (!)

In order to fetch from API like spoonacular, look at thunk.

17 of 17

Middleware: Thunk

“Redux Thunk middleware allows you to write action creators that return a function instead of an action.”

  • Importing (index.html):

<script src="https://unpkg.com/redux-thunk@2.3.0/dist/redux-thunk.min.js"></script>

<script> const ReduxThunk = ReduxThunk.default; </script>

  • Loading middleware in store

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))

  • Dispatch async function:

// dishActions.js

function getDish(dishId) {

return function(dispatch) {

return spoontacularApiFetch(url).then(dish => {

dispatch({ type: DISH, dish:dish})

})

}

}

// dishController.js

store.dispatch(getDish(dishId))