2

Complete Introduction to Redux 🚀

 2 years ago
source link: https://dev.to/cenacr007_harsh/complete-introduction-to-redux-2281
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Cover image for Complete Introduction to Redux 🚀

Complete Introduction to Redux 🚀

Jul 1

・13 min read

Mastering React (2 Part Series)

Does Redux have you confused? 😕

In this blog, you'll learn the fundamentals of Redux stores, actions, reducers, and middleware to manage data throughout your application.

What is Redux?

Redux is an open-source JavaScript library for managing the application state. It is most commonly used with libraries such as React or Angular for building user interfaces.

Why Redux?

As applications grow in size and scope, managing shared data becomes much more difficult. Redux is defined as a "predictable state container for JavaScript apps" that helps ensure your apps work predictably and are easier to test.

Now let's hop into the basics that make Redux go 🚀

1️⃣ Create a Redux Store

In Redux, there is a Single State Object that's responsible for the entire state of your application. This means if you had a React app with ten components, and each component had its local state, the entire state of your app would be defined by a single state object housed in the Redux store. This is the first important principle to understand when learning Redux: the Redux store is the single source of truth when it comes to application state.

This also means that any time any piece of your app wants to update its state, it must do so through the Redux store. The unidirectional data flow makes it easier to track state management in your app.

The Redux store is an object which holds and manages the application state. There is a method called createStore() on the Redux object, which you use to create the Redux store. This method takes a reducer function as a required argument. We'll soon see what a reducer is in Point 7.

Keep in mind that the createStore() method is available from the Redux object. For example: Redux.createStore()

const reducer = (state = 5) => {
  return state;
}

const store=Redux.createStore(reducer);
Enter fullscreen modeExit fullscreen mode

2️⃣ Get State from the Redux Store

The Redux store object provides several methods that allow you to interact with it. For example, you can retrieve the current state held in the Redux store object with the getState() method.

let currentState = store.getState();
Enter fullscreen modeExit fullscreen mode

3️⃣ Define a Redux Action

Since Redux is a state management framework, updating the state is one of its core tasks. In Redux, all state updates are triggered by dispatching actions. An action is simply a JavaScript object that contains information about an action event that has occurred. The Redux store receives these action objects, then updates its 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.

Think of Redux actions as messengers that deliver information about events happening in your app to the Redux store. The store then conducts the business of updating the state based on the action that occurred.

Writing a Redux action is as simple as declaring an object with a type property.

let action={
  type: 'LOGIN'
}
Enter fullscreen modeExit fullscreen mode

4️⃣ Define an Action Creator

After creating an action, the next step is sending the action to the Redux store so it can update its state. In Redux, you define action creators to accomplish this. An action creator is simply a JavaScript function that returns an action. In other words, action creators create objects that represent action events.

function actionCreator() {
  return action;
}
Enter fullscreen modeExit fullscreen mode

5️⃣ Dispatch an Action Event

The dispatch method is what you use to dispatch actions to the Redux store. Calling store.dispatch() and passing the value returned from an action creator sends an action back to the store.

Recall that action creators return an object with a type property that specifies the action that has occurred. Then the method dispatches an action object to the Redux store.

The following lines are equivalent, and both dispatch the action of type LOGIN:

store.dispatch(actionCreator());
store.dispatch({ type: 'LOGIN' });

const store = Redux.createStore(
  (state = {login: false}) => state
);

const loginAction = () => {
  return {
    type: 'LOGIN'
  }
};

store.dispatch(loginAction());
Enter fullscreen modeExit fullscreen mode

6️⃣ Handle an Action in the Store

After an action is created and dispatched, 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. It has no side effects — it never calls an API endpoint and it never has any hidden surprises. The reducer is simply a PURE FUNCTION that takes state and action, then returns the new state.

Another key principle in Redux is that state is read-only. In other words, the reducer function must always return a new copy of the state and never modify the state directly. Redux does not enforce state immutability, however, you are responsible for enforcing it in the code of your reducer functions.

const reducer = (state = defaultState, action) => {
  if (action.type === "LOGIN") {
    return {
      login: true
    };
  } else {
    return state;
  }
};
Enter fullscreen modeExit fullscreen mode

7️⃣ Use a Switch Statement to Handle Multiple Actions

You can tell the Redux store how to handle multiple action types.

Say you are managing user authentication in your Redux store. You want to have a state representative for when users are logged in and when they are logged out. You represent this with a single state object with the property authenticated. You also need action creators that create actions corresponding to user login and user logout, along with the action objects themselves.

Don't forget to write a default case in your switch statement that returns the current state. This is important because once your app has multiple reducers, they are all run any time an action dispatch is made, even when the action isn't related to that reducer. In such a case, you want to make sure that you return the current state.

const defaultState = {
  authenticated: false
};

const authReducer = (state = defaultState, action) => {
  switch (action.type) {
    case "LOGIN":
      return {
        authenticated: true
      };

    case "LOGOUT":
      return {
        authenticated: false
      };

    default:
      return defaultState;
  }
};

const store = Redux.createStore(authReducer);

const loginUser = () => {
  return {
    type: "LOGIN"
  };
};

const logoutUser = () => {
  return {
    type: "LOGOUT"
  };
};
Enter fullscreen modeExit fullscreen mode

8️⃣ Use const for Action Types

A common practice when working with Redux is to assign action types as read-only constants, then reference these constants wherever they are used.

It's generally a convention to write constants in all uppercase, and this is standard practice in Redux as well. We must always try to follow the conventions as it makes our code more readable for others.

const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';

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
  }
};
Enter fullscreen modeExit fullscreen mode

9️⃣ Register a Store Listener

Another method you have access to on the Redux store object is store.subscribe(). This allows you to subscribe listener functions to the store, which are called whenever an action is dispatched against the store.

Write a callback function that increments the global variable count every time the store receives an action, and pass this function into the store.subscribe() method 👇

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;

// Callback Function
const addOne = () => (count += 1);

// Passing Callback to store.subscribe()
store.subscribe(addOne);

store.dispatch({type: ADD});
console.log(count); //1
store.dispatch({type: ADD});
console.log(count); //2
store.dispatch({type: ADD});
console.log(count); //3
Enter fullscreen modeExit fullscreen mode

🔟 Combine Multiple Reducers

When the state of your app begins to grow more complex, it may be tempting to divide the state into multiple pieces.

Instead, remember the first principle of Redux:
all app state is held in a single state object in the store.

Therefore, Redux provides reducer composition as a solution for a complex state model. You define multiple reducers to handle different pieces of your application's state, then compose these reducers together into one root reducer. The root reducer is then passed into the Redux createStore() method.

To let us combine multiple reducers, Redux provides the combineReducers() method. This method accepts an object as an argument in which you define properties that associate keys to specific reducer functions. The name you give to the keys will be used by Redux as the name for the associated piece of state.

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.

For example, in a note-taking app with user authentication, one reducer could handle authentication while another handles the text and notes that the user is submitting. For such an application, we might write the combineReducers() method like this:

const rootReducer = Redux.combineReducers({
  auth: authenticationReducer,
  notes: notesReducer
});
Enter fullscreen modeExit fullscreen mode

Now, the key notes will contain all of the states associated with our notes and handled by our notesReducer. This is how multiple reducers can be composed to manage a 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.

In short we divide our state into different parts and convert it into one single object, whose each property represents one part of our state, this way we still have one single state object but our state is logically divided into different blocks, and similarly we define different reducers to deal with different parts of the state, and then combine it into one root reducer which is passed to the store.

const store = {
state_part1: {
//contains all code related to part 1
 }

state_part2: {
//contains all code related to part2
 }
}
Enter fullscreen modeExit fullscreen mode

Here is a more detailed 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;
  }
};

// define the root reducer here
const rootReducer = Redux.combineReducers({
  count: counterReducer,
  auth: authReducer
});

const store = Redux.createStore(rootReducer);
Enter fullscreen modeExit fullscreen mode

1️⃣1️⃣ Send Action Data to the Store

We've seen how to dispatch actions to the Redux store, but so far these actions have not contain any information other than a type. You can also send specific data along with your actions. This is very common because actions usually originate from some user interaction and tend to carry some data with them. The Redux store often needs to know about this data.

const ADD_NOTE = "ADD_NOTE";

const notesReducer = (state = "Initial State", action) => {
  switch (action.type) {

// Returning the text property on the incoming action as the new state.
    case ADD_NOTE:
      return action.text;

    default:
      return state;
  }
};

const addNoteText = note => {
// Returning an Action Object with a type property, and a text property set to the note data that's passed into the action creator. 
  return {
    type: ADD_NOTE,
    text: note
  };
};

const store = Redux.createStore(notesReducer);

console.log(store.getState());
store.dispatch(addNoteText("Hello!"));
console.log(store.getState());
Enter fullscreen modeExit fullscreen mode

Output in the console:

Initial State
Hello!
Enter fullscreen modeExit fullscreen mode

1️⃣2️⃣ Use Middleware to Handle Asynchronous Actions

Asynchronous Actions are an unavoidable part of web development.

At some point you'll need to call asynchronous endpoints in your Redux app, so how do we handle these types of requests?

Redux provides middleware designed specifically for this purpose, called Redux Thunk middleware.

To include Redux Thunk middleware, you pass it as an argument to Redux.applyMiddleware(). This statement is then provided as a second optional parameter to the createStore() function.

const store = Redux.createStore(
  asyncDataReducer,
  Redux.applyMiddleware(ReduxThunk.default)
);
Enter fullscreen modeExit fullscreen mode

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.

In this example 👇, an asynchronous request is simulated with a setTimeout() call. It's common to dispatch an action before initiating any asynchronous behavior so that your application state knows that some data is being requested (this state could display a loading icon, for instance). Then, once you receive the data, you dispatch another action that carries the data as a payload along with information that the action is completed.

Remember that you're passing dispatch as a parameter to this special action creator. This is what you'll use to dispatch your actions, you simply pass the action directly to dispatch and the middleware takes care of the rest.

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 };
};

const handleAsync = () => {
  return function(dispatch) {
    // dispatch request action here

    dispatch(requestingData());

    setTimeout(function() {
      let data = {
        users: ["Jeff", "William", "Alice"]
      };
      // dispatch received data action here

      dispatch(receivedData(data));
    }, 2500);
  };
};

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)
);
Enter fullscreen modeExit fullscreen mode

Let's revise for a bit:

Write a Counter with Redux 🕓

Now we've learned all the core principles of Redux!

Let's implement a simple counter with Redux from scratch.

// define a constant for increment action types
const INCREMENT = "INCREMENT";
// define a constant for decrement action types
const DECREMENT = "DECREMENT";

// define the counter reducer which will increment or decrement the state based on the action it receives
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;

    case DECREMENT:
      return state - 1;

    default:
      return state;
  }
};

// define an action creator for incrementing
const incAction = () => {
  return {
    type: INCREMENT
  };
};

// define an action creator for decrementing
const decAction = () => {
  return {
    type: DECREMENT
  };
};

// define the Redux store here, passing in your reducers
const store = Redux.createStore(counterReducer);
Enter fullscreen modeExit fullscreen mode

1️⃣3️⃣ Never Mutate State

These final few points describe several methods of enforcing the key principle of state immutability in Redux. Immutable state means that you never modify the state directly, instead, you return a new copy of the state.

I repeat:

Never Mutate State

If you took a snapshot of the state of a Redux app over time, you would see something like state 1, state 2, state 3, state 4, ... and so on where each state may be similar to the last, but each is a distinct piece of data.

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. In practice, your state will probably consist of an array or object, as these are useful data structures for representing many types of information.

Read more about Immutability:

MDN or

Here is a detailed example:

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

      return state.concat(action.todo);
    // or return [...state, action.todo]

    default:
      return state;
  }
};

const addToDo = todo => {
  return {
    type: ADD_TO_DO,
    todo
  };
};

const store = Redux.createStore(immutableReducer);
Enter fullscreen modeExit fullscreen mode

Helpful Pointers:

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)

.push() and .splice() directly modify the array

.concat() doesn’t modify array but just returns a new array

.slice() doesn’t modify array but just returns a new array

spread operator […array] doesn’t modify array but just returns a new array

The easiest and widely used is ES6 spread operator

👉 Use the Spread Operator on Arrays

One solution from ES6 to help enforce state immutability in Redux is the spread operator: ...

let newArray = [...myArray];
Enter fullscreen modeExit fullscreen mode

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']. This would return a new array composed of the values in myArray and the string new value as the last value. The spread syntax can be used multiple times in array composition like this, but it's important to note that it 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 detailed example:

const immutableReducer = (state = ["Do not mutate state!"], action) => {
  switch (action.type) {
    case "ADD_TO_DO":
      // don't mutate state here
      let arr = [...state, action.todo];
      return arr;
    default:
      return state;
  }
};

const addToDo = todo => {
  return {
    type: "ADD_TO_DO",
    todo
  };
};

const store = Redux.createStore(immutableReducer);

Enter fullscreen modeExit fullscreen mode

👉 Remove an Item from an Array

The spread operator can be used here as well. Other useful JavaScript methods include slice() and concat().

const immutableReducer = (state = [0, 1, 2, 3, 4, 5], action) => {
  switch (action.type) {
    case "REMOVE_ITEM":
      // don't mutate state here
      return [
        ...state.slice(0, action.index),
        ...state.slice(action.index + 1, state.length)
      ];

    // or 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);
Enter fullscreen modeExit fullscreen mode

Code Explanation

  • array.slice(fromIndex, untilIndex) returns a new array
  • 1st slice from the first item’s index (0 inclusive) until indexToRemove(action.index exclusive)
  • 2nd slice from item right after indexToRemove (action.index + 1 inclusive) until length (last item’s index + 1 exclusive)
  • since slice returns a new array, combine both parts with […array1, …array2] spread operator
  • or combine them with .concat()

👉 Copy an Object with Object.assign

There are ways to help enforce state immutability when a state is an object, too. 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.

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

Here's a detailed example:

const defaultState = {
  user: "CamperBot",
  status: "offline",
  friends: "732,982",
  community: "freeCodeCamp"
};

const immutableReducer = (state = defaultState, action) => {
  switch (action.type) {
    case "ONLINE":
      // to enforce state immutability, return a new state object using Object.assign() method
      return Object.assign({}, state, { status: "online" });
    default:
      return state;
  }
};

const wakeUp = () => {
  return {
    type: "ONLINE"
  };
};

const store = Redux.createStore(immutableReducer);
Enter fullscreen modeExit fullscreen mode

That's the basics!

You've seen how to create action and action creators, create a Redux store, dispatch your actions against the store, and design state updates with pure reducers. You've even seen how to manage complex states with reducer composition and handle asynchronous actions.

The examples were simplistic, but these concepts are the core principles of Redux. If you understand them well, you're ready to start building your own Redux app.

I hope you found this article valuable. If yes do let me know in the comments 😊

This article was inspired by freecodecamp curriculum.

Also if you got any questions feel free to ping me on:

Twitter or Linkedin

Thank You!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK