Before starting, things that became clear to you later on: It is the job of the Reducer (more precisely, the different reducer functions, but you can think of them collectively as a reducer) to modify the state [Actually, to return a new copy of the state as the state is read only]. So all actions (and action creators) end up at the Reducer, who then goes and modifies the state.
Redux uses an object called store to keep track of the state of the whole app. We declare
store as a variable (in the create react app setup, we did it in the index.js - the place where the root App component is called), and called the createStore()
function to create it. Note
that createStore()
is a redux method, so it should be called with Redux.something. Also, the
createStore method always needs to take a function called 'reducer' as an argument (we will
cover reducer later). Here is the code:
//creating a reducer function first:
const reducer = (state = 5) => {
return state;
}
Redux methods are available from a Redux object
For example: Redux.createStore()
//now using the reducer function as an argument:
const store = Redux.createStore(reducer)
Another way to write the above code more concisely is to throw the function body directly
inside createStore()
:
const store = Redux.createStore(
(state = 5) => state
);
//note how we can skip the 'return' in 'return state'
Now, assume we want to create a variable called currentState and assign to it the value of the
current state. We can get the value of current state by using store.getState()
. The code:
const store = Redux.createStore(
(state = 5) => state
);
const currentState = store.getState();
currentState
will now have the value of 5. We can do this because we now have a universal store object and we can check its state whenever we want.
Note that the Redux store object provides several methods that allow you to interact with it,
and getState()
is one of them.
Updating states in one of the core functionalities in Redux. In Redux, state updates are triggered by actions. An action is simply a JS object that contains information about an action event that has occured (like onClick, I guess). The redux store receives these action objects and updates the state accordingly.
Sometimes a Redux action also carries some data. For example, the action carries a username after a user logs in. While the data is optional, actions must carry a type property that specifies the 'type' of action that occurred.
Writing a Redux action is as simple as declaring an object with a type property. Example:
const action ={
type : 'LOGIN'
}
After creating an action, the next step is sending the action to the Redux store so it can update its state. But before that, you need an to define an action creator. An action creator is simply a JavaScript function that returns an action. In other words, action creators create objects that represent action events:
const action = {
type: 'LOGIN'
}
// Define an action creator here:
function actionCreator(){
return action;
}
//in the above, const instead of function
//could have also worked
Both bi darbe wa7de:
const actionCreator = () => {
return {
type: 'LOGIN'
}
};
Note that creating an action does not mean that it has been passed to redux store. We will pass it in the next step.
To dispatch (pass?) an action to the redux store, you use the store.dispatch
function.
Based on the previous example, both these ways of dispatching would work:
store.dispatch(actionCreator());
store.dispatch({ type: 'LOGIN' });
//This works too because type is unique to every action
//note: the dispatch itself is called based on some kind
//of event (like lifecycle event, login, form submission, etc)
Here is a code of the whole thing running together:
//store created, with reducer function inside,
//state initialized to first value, where login is false
const store = Redux.createStore(
(state = {login: false}) => state
);
//a login action, which once sent to the reducer,
//will update the state of login based on how its 'reducer function' works (see next section)
const loginAction = () => {
return {
type: 'LOGIN'
}
};
//the code that triggers and action to be sent to the reducer,
//can be called on button click or whatever
store.dispatch(loginAction());
If a certain action is dispatched based on some event, the Redux store needs to know how to respond to that action. This is the job of a reducer function. Reducers in Redux are responsible for the state modifications that take place in response to actions.
A reducer takes state and action as arguments, and it always returns a new state. It is important to see that this is the only role of the reducer.
Another key principle in Redux is that state is read-only. In other words, the reducer function must always return a new copy of state and never modify state directly.
Here is a code:
const defaultState = {
login: false
};
const reducer = (state = defaultState, action) => {
// change code below this line
if(action.type === 'LOGIN'){
return { login : true};
}
else return defaultState;
// change code above this line
};
const store = Redux.createStore(reducer);
const loginAction = () => {
return {
type: 'LOGIN'
}
};
Note: reducer function doesn't necessarily need to be named reducer. Also you can have more than one reducer function.
Remember: a reducer returns a new state based on an action. To be more precise, it changes a certain 'property' among the many properties that exist within a state, according to a certain action.
In its second argument, a reducer can receive any of the actions in the app, and according to the specific action (which action fits its cases) it delivers a new state.
So I have a bunch of actions that exist within action functions. Whenever any of these actions
is triggered, a[/all?] reducer function will be called (with arguments state
,
and that specific action
). According to that specific action, a part (property) of the state
will be updated. See example:
const defaultState = {
authenticated: false
};
const authReducer = (state = defaultState, action) => {
// change code below this line
switch(action.type) {
case 'LOGIN':
return { authenticated: true};
case 'LOGOUT':
return defaultState;
default:
return defaultState;
}
// change code above this line
};
const store = Redux.createStore(authReducer);
const loginUser = () => {
return {
type: 'LOGIN'
}
};
const logoutUser = () => {
return {
type: 'LOGOUT'
}
};
Note: I am not sure if all reducer functions are called in an action dispatch, or if we can specify which reducer function to be called? I guess not, I guess all of them are called and all of them search through their switch statement.
In the above example, we have two actions: LOGIN, and LOGOUT. We have a store that is connected to a reducer function that focus on login and logout. We have a default state for our state when it comes to authentication, which makes it false by default.
When Login or Logout actions are triggered, the reducer function will be called (by dispatch) to update the state accordingly. Note that we used the switch syntax. When using the switch syntax here, make sure there are no break statements, and that a default statement is included.
It is better to have LOGIN and LOGOUT as variables assigned to strings instead of using the strings directly. So instead of the above code, it is better to write it this way:
// change code below this line
const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';
// change code above this line
const defaultState = {
authenticated: false
};
const authReducer = (state = defaultState, action) => {
switch (action.type) {
case LOGIN:
return {
authenticated: true
}
case LOGOUT:
return {
authenticated: false
}
default:
return state;
}
};
const store = Redux.createStore(authReducer);
const loginUser = () => {
return {
type: LOGIN
}
};
const logoutUser = () => {
return {
type: LOGOUT
}
};
Note how the const string declarations were all caps. It is a convention to write them that way.
Another redux method related to the store is store.subscribe()
. It allows me to subscribe
listener functions to the store which are called whenever an action is dispatched against
the store.
A simple way of using it is to subscribe a function to your store that simply logs a message every time an action is received and the store is updated.
The following code was confusing to me because no action was defined (but maybe it is not needed anyway) -EDIT: you are already dispatching actions at the end of the code, I guess we can safely assume they are defined.
const ADD = 'ADD';
const reducer = (state = 0, action) => {
switch(action.type) {
case ADD:
return state + 1;
default:
return state;
}
};
const store = Redux.createStore(reducer);
// global count variable:
let count = 0;
// change code below this line
const increment = () => {
count++;
}
store.subscribe(increment);
// change code above this line
store.dispatch({type: ADD});
console.log(count);
store.dispatch({type: ADD});
console.log(count);
store.dispatch({type: ADD});
console.log(count);
The subscriber listener function was called on each store dispatch. We will have a console
log as follows:
1
2
3
As our state becomes more complex, we might be tempted to devide it into multiple pieces. But remember: Redux works on the principle that all states of the app are put together in one place inside the store. (In a single state object). Therefore, to be able to 'divide' or organize states inside the store, we do something called reducer composition.
We define multiple reducers to handle different pieces of the app state, then combine these
reducers together into one ROOT REDUCER
. The root reducer will be what will be passed into the
redux method: createStore()
.
So to combine those reducers, we use combineReducers()
method. This method takes an object
as an argument, and this object has properties as a key:reducer
pair.
The name given to the keys will be sued by Redux as the name of the associated piece of state.
Example:
const rootReducer = Redux.combineReducers({
auth: authenticationReducer,
notes: notesReducer
});
Typically, it is a good practice to create a reducer for each piece of application state when they are distinct or unique in some way. In the above example (a note-taking app with user authentication), one reducer could handle authentication while another handles the text and notes that the user is submitting.
The key notes
will contain all of the state associated with our notes and handled by our
notesReducer
. This is how multiple reducers can be composed to manage more complex
application state. In this example, the state held in the Redux store would then be a
single object containing auth
and notes
properties.
Here is a more complete example:
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const counterReducer = (state = 0, action) => {
switch(action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};
const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';
const authReducer = (state = {authenticated:
false}, action) => {
switch(action.type) {
case LOGIN:
return {
authenticated: true
}
case LOGOUT:
return {
authenticated: false
}
default:
return state;
}
};
const rootReducer = Redux.combineReducers({
auth: authReducer,
count: counterReducer
});
const store = Redux.createStore(rootReducer);
Note how when we created the store, it was given only one reducer - the combiner reducer. Note how our state has been devided into two main 'sections': one handling authentication, and one handling a counter. Each of these state pieces has its own reducer method, and each of these state pieces has several 'state' possiblities for actions.
So far we've learned how to dispatch actions to the Redux store, but they haven't
included any useful information except the type
. We can also send data inside actions: actually,
that's mainly what actions are used for.
const ADD_NOTE = 'ADD_NOTE';
const notesReducer = (state = 'Initial State',
action) => {
switch(action.type) {
// change code below this line
case ADD_NOTE:
return action.text;
// change code above this line
default:
return state;
}
};
const addNoteText = (note) => {
// change code below this line
return {
type: ADD_NOTE,
text: note
}
// change code above this line
};
const store = Redux.createStore(notesReducer);
console.log(store.getState());
store.dispatch(addNoteText('Hello!'));
console.log(store.getState());
Note: remeber that the dispatcher is what triggers the action: kind of calls the action handler.
The reducer function is taking the state and action, checking whether action is of a certain type, and returning a new state according to that type.
So here is how the sytem works together: you have a
reducer
, astore creator
, anaction handler
, and adispatcher
.The
store creator
is always present, it creates a store inside redux to hold a state, and always takes a reducer method as a parameter, which is 'binded' to that state. Remeber that you can have more than one state, each with its own reducer, but then you have to combine reducers into one root reducer and have the store creator call that one root reducer (because we can only create the store once).The store creator method took a
reducer
as a parameter. The reducer is a method that takes bothstate
andaction
as a parameters.Now, what is the use of the action parameter taken by the reducer function? The action parameter is not 'useful' unless the action itself triggers the reducer. So a reducer method is triggered to work when a certain action is performed. That action talks to the state (which mean it triggers the state reducer), and the reducer will check if it covers that 'type' of action. So I might have 10 actions, and it is expected that all these actions have cases inside the reducer, and whenever any of these actions is triggered (by dispatch), it goes to the reducer to check for a case and a new state is generated.
I am not sure dispatch is the only thing that can trigger an action, but so far, it is the way we've been triggering actions.
SO IN SUMMARY:
We create a redux store to hold the state through createStore. createStore()
calls a reducer
function that initializes the state to a certain value [actually I am not sure of this, it could be that reducers are only called when actions are dispatched]. Reducers are always ready to cover action dispatches
that might arise in the future.
Whenever an action
(that has been intialized by an action handler
) is triggered by a dispatcher
,
that action will go through all reducers
, tells them: "look, here is my type
, do you have any case
covering me?" - if any of those reducers do, then that case is executed and a new state
is generated.
We can pass information through actions.
Note: asynchronous actions actually mean that they are synchronized (I guess?) -- Well, actually this means I am requesting data from somewhere outside my application and I need to wait for it.
Redux Thunk middleware
is used to handle asynchronous actions.
To include it: you pass it as an argument to Redux.applyMiddleware()
.
This statement is then provided as a second optional parameter to the createStore()
method.
Example:
const store = Redux.createStore(
asyncDataReducer,
Redux.applyMiddleware(ReduxThunk.default)
);
Then, to create an asynchronous action, you return a function in the action creator that takes dispatch as an argument. Within this function, you can dispatch actions and perform asynchronous requests.
Example:
const REQUESTING_DATA = 'REQUESTING_DATA'
const RECEIVED_DATA = 'RECEIVED_DATA'
const requestingData = () => {
return {type: REQUESTING_DATA}
}
const receivedData = (data) => {
return {type: RECEIVED_DATA, users: data.users} }
//the below is a special action creator to handle
//asynchronous actions
const handleAsync = () => {
return function(dispatch) {
// dispatch request action here
dispatch(requestingData());
//setTimeout here is for simulation:
//as if we're waiting on a server
setTimeout(function() {
let data = {
users: ['Jeff', 'William', 'Alice']
}
// dispatch received data action here
}, 2500);
dispatch(receivedData(data));
}
};
const defaultState = {
fetching: false,
users: []
};
const asyncDataReducer = (state = defaultState, action) => {
switch(action.type) {
case REQUESTING_DATA:
return {
fetching: true,
users: []
}
case RECEIVED_DATA:
return {
fetching: false,
users: action.users
}(
default:
return state;
}
};
const store = Redux.createStore(
asyncDataReducer,
Redux.applyMiddleware(ReduxThunk.default)
);
Note that we have two normal actions, a store creator that takes the Redux Thunk
as a second
parameter, a reducer
, and then we also have the special action handler function called
handleAsync
. Inside it, whenever an action needs to be called asynchronously, we call it using
dispatch(actionHandler)
.
That's it.
EDIT: Actually there is more to it. I finally understood everything. So normally in JS, when we have code, it doesn't execute chronologically. Meaning that, when a function is called, we don't wait for it to finish before executing the code below it.
That is what Redux Thunk is trying to solve: I want to be able to execute an action exactly when I finish fetching data from a database. That is why I have to use the special action handler (I am still slightly confused by the code in it though).
Review excercise: creating a simple counter:
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const counterReducer = (state = 0, action) => {
switch(action.type){
case INCREMENT:
return state + 1;
case DECREMENT:
return state -1;
default:
return state;
}
};
const incAction = () =>{
return {
type: INCREMENT
}
};
const decAction = () =>{
return {
type: DECREMENT
}
};
const store = Redux.createStore(
counterReducer);
14- Immutable State
Immutable state means that you never modify state directly, instead, you return a new copy of state.
Redux does not actively enforce state immutability in its store or reducers, that responsibility falls on the programmer. Fortunately, JavaScript (especially ES6) provides several useful tools you can use to enforce the immutability of your state, whether it is a string, number, array, or object.
Note that strings and numbers are primitive values and are immutable by nature. In other words, 3 is always 3. You cannot change the value of the number 3. An array or object, however, is mutable.
Here is an example where we had to be careful not to modify state (note, we weren't paying attention to this earlier):
const ADD_TO_DO = 'ADD_TO_DO';
// A list of strings representing tasks to do:
const todos = [
'Go to the store',
'Clean the house',
'Cook dinner',
'Learn to code',
];
const immutableReducer = (state = todos, action) => {
switch(action.type) {
case ADD_TO_DO:
// don't mutate state here or the tests will fail
return todos.concat(action.todo)
default:
return state;
}
};
// an example todo argument would be 'Learn React',
//note that todo is passed as a parameter to this
//function, it is like a user input
const addToDo = (todo) => {
return {
type: ADD_TO_DO,
todo
}
}
const store = Redux.createStore(immutableReducer);
Note: instead of the return todos.concat(action.todo)
, we could have selected anyway of adding
the item to the array provided it remains immutable:
Hint 1
const means: it cannot change through re-assignment, and it cannot be re-declared. Since objects and arrays are mutable, you can add to it by index (array[3] = 3), by property (object.name=“sam”), by extending (with various array methods)
Hint 2
.push()
and.splice()
directly modify the arrayHint 3
.concat()
doesn’t modify array but just returns a new arrayHint 4
.slice()
doesn’t modify array but just returns a new arrayHint 5
spread operator
[…array]
doesn’t modify array but just returns a new array
15- The Spread Operator [...]
The spread operator has a variety of applications, one of which is well-suited to the previous challenge of producing a new array from an existing array.
For example, if you have an array myArray and write:
let newArray = [myArray];
newArray
is now a clone of myArray
. Both arrays still exist separately in memory. If you
perform a mutation like newArray.push(5)
, myArray doesn't change. The ...
effectively
spreads out the values in myArray into a new array. To clone an array but add additional
values in the new array, you could write [...myArray, 'new value']
It's important to note that the spread operator only makes a shallow copy of the array. That is to say, it only provides immutable array operations for one-dimensional arrays.
Here is a small challenge where we used the spread operator:
const immutableReducer = (state = ['Do not mutate state!'], action) => {
switch(action.type) {
case 'ADD_TO_DO':
// don't mutate state here or the tests will fail
return [state, action.todo]
default:
return state;
}
};
const addToDo = (todo) => {
return {
type: 'ADD_TO_DO',
todo
}
}
const store = Redux.createStore(immutableReducer);
const immutableReducer = (state = [0,1,2,3,4,5], action) => {
switch(action.type) {
case 'REMOVE_ITEM':
// don't mutate state here or the tests will fail
return state.slice(0, action.index).concat(state.slice(action.index + 1, state.length))
default:
return state;
}
};
const removeItem = (index) => {
return {
type: 'REMOVE_ITEM',
index
}
}
const store = Redux.createStore(immutableReducer);
note how we didn't use the [...state] syntax here, because both slice() and concat() do not alter original array (they return a new one).
However, we could have also done it this way:
return [
state.slice(0, action.index),
state.slice(action.index + 1, state.length)
];
A useful tool for handling objects is the Object.assign()
utility. Object.assign()
takes a
target object and source objects and maps properties from the source objects to the target
object. Any matching properties are overwritten by properties in the source objects. This
behavior is commonly used to make shallow copies of objects by passing an empty object as the
first argument followed by the object(s) you want to copy. Here's an example:
const newObject = Object.assign({}, obj1, obj2);
This creates newObject as a new object, which contains the properties that currently exist in obj1 and obj2.
In the following example, we use Object.assing()
to create a new object, make it the same as
the previous state, except that we also pass a second object as source object in order to modify
the 'status' property inside the state:
const defaultState = {
user: 'CamperBot',
status: 'offline',
friends: '732,982',
community: 'freeCodeCamp'
};
const immutableReducer = (state = defaultState, action) => {
switch(action.type) {
case 'ONLINE':
// don't mutate state here or the tests will fail
return Object.assign({}, state, {status: 'online'})
default:
return state;
}
};
const wakeUp = () => {
return {
type: 'ONLINE'
}
};
const store = Redux.createStore(immutableReducer);