Redux Middleware and Sagas

Anton Ioffe - September 22nd 2023 - 20 minutes read

Advancements in web development techniques and an increasingly complex digital environment require developers to constantly adapt and optimize their approaches. One disruption in web development is the incorporation of asynchronous operations and side-effects in state management, a realm where Redux middleware, particularly Redux-Saga, is making a significant impact. This article will delve deep into understanding Redux middleware and Sagas, promising to offer an enriched perspective that leverages on practical insights, comparisons, and advanced concepts of these critical tools.

We begin with uncovering the Redux architecture, followed by discussions on the heart of Redux-Saga, exposing its operation principles and how it leverages ES6 features. We will also provide an interesting comparative study between Redux-Saga, Redux-Thunks, and Redux Toolkit, revealing where each of these libraries shines. In the quest for practical understanding, we dig into real-world implementations of Redux-Saga and explore advanced concepts that drive complex operations.

From testing and debugging Redux-Sagas to exploring their relevance in the present and future projections, every section ensures a comprehensive analysis that takes you one step closer to mastering these vital aspects of modern web development. Prepare to dive in, the journey promises to be full of intriguing insights and practical knowledge.

Unveiling the Redux Architecture

Redux, a state management library, provides a centralized store where the state of your application resides. In large scale applications, where various parts of the system need to share and manipulate state data, Redux eases the complexity by providing a unified platform for all data-related operations. Each component within your application, whether it's intended to modify data or merely read from it, can access the centralized state data store. As a result, Redux fulfills a significant role in managing complex states within extensive systems.

With an aim to further understanding Redux, let's delve into its middleware layer. In Redux, middleware is a mechanism that resides between the dispatching of an action and the time it reaches the reducer. Middleware serves as an important extension point that enhances the dispatch function. It broadens Redux's capabilities by enabling additional features such as handling asynchronous tasks, logging, crash reporting, and more.

One notable functionality of Redux middleware is to customize the dispatch function. Redux uses enhancers to override the dispatch function, but, middleware extends this flexibility to allow execution of custom logic before action dispatch. This is illustrated in the example below:

const customMiddleware = store => next => action => {
 // Middleware code goes here
 next(action);
};

In the above code snippet, customMiddleware is our Redux middleware. It's a curried function that gets the store and next dispatcher reference, and finally the action to dispatch. Here, executing next(action); allows us to pass the action to the next middleware in line if it exists, or to the root reducer if it doesn't. This way, middleware complements the dispatch function to enable a richer handling of actions.

The functionality of middleware in Redux parallels its usage in other libraries such as Express.js. Similar to Redux, Express.js employs middleware to customize or extend the primary functionality of handling HTTP requests. In that context, middleware serves to add features such as logging, authorization, or error handling much like how Redux middleware adds features to the Redux ecosystem.

Venturing further into Redux's middleware landscape, we encounter Redux-Saga, a companion library for Redux that efficiently manages the asynchronous flow of the application. Redux-Saga empowers the Redux store to interact with resources outside of it, such as making HTTP requests, accessing local storage, or managing other I/O operations, all asynchronously. Have a look at the following code snippet demonstrating the utilization of Redux-Saga in handling an HTTP request,

import { call, put } from 'redux-saga/effects';
import { fetchUsersSuccess } from './actions';
import axios from 'axios';

function* fetchUsersSaga() {
  try {
    const users = yield call(axios.get, 'https://jsonplaceholder.typicode.com/users');
    yield put(fetchUsersSuccess(users));
  } catch (error) {
    console.error(error);
  }
}

In the code above, fetchUsersSaga is a saga that fetches users from a remote API via the axios.get method. The call effect is used to asynchronously execute the HTTP request. Upon successful completion, the put effect dispatches the fetchUsersSuccess action, carrying the fetched users as its payload. Essentially, fetchUsersSuccess is an action creator that dispatches an action to the reducer to update the state with the fetched users' data. This showcases the effective orchestration of asynchronous operations in Redux-Saga.

To wrap up, Redux architecture, along with middleware and specifically Redux-Saga, presents a robust solution for managing complex states within large-scale applications. It allows comprehensive control over state data alongside a flexible dispatch function and an effective management of side effects. Consider these potent features while designing your next web application using Redux architecture.

The Core Mechanics of Redux-Saga

The Role of Generators in Redux-Saga for Side Effects Control

Redux-Saga employs generator functions, an ES6 feature, to effectively manage side effects within an application. These generator functions offer a unique ability to pause and resume, enabling Redux-Saga to handle side effects in seemingly synchronous sequences.

Let's delve into this interaction by first understanding a generator function:

function* myGenerator(){
    let first = yield 'first yield value';
    let second = yield 'second yield value';
    return 'third returned value';
}

When invoked, such a function doesn't return the third returned value, but an Iterator object. Now, calling the .next() method on the Iterator prompts it to proceed to the next yield point where it emits a value and pauses. This pause-resume mechanism is the gem that Redux-Saga hones to immaculately control side effects within the Redux framework without introducing unwanted mutations in the state or breaching Redux principles.

Let's take a quick real-world example for better understanding of the concept, using fetching data from an API:

import { call, put } from 'redux-saga/effects'
import Api from '...'

function* fetchData(action) {
   try {
      const data = yield call(Api.fetchUser, action.payload.url);
      yield put({type: "FETCH_SUCCEEDED", data});
   } catch (error) {
      yield put({type: "FETCH_FAILED", error});
   }
}

In the above implementation, fetchData is a generator function that leverages the yield keyword to pause and perform the API call. It will then resume after the API call is complete, and dispatch an action with the fetched data.

Setting Up Redux-Saga as Middleware: Operational Mechanism

Redux-Saga is a middleware that listens for dispatched actions, undertakes related side effects, and informs Redux to make the corresponding state changes, all without directly altering the state itself. View the following code snippet to comprehend its integration as a middleware in the Redux store:

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducers';
import saga from './sagas';

// create a Redux-Saga middleware
const sagaMiddleware = createSagaMiddleware();

// create a Redux store. Apply the saga middleware during creation
const store = createStore(reducer, applyMiddleware(sagaMiddleware));

// run the saga
sagaMiddleware.run(saga);

Redux-Saga serves as an intermediary within the application's data flow pipeline, situated between actions and reducers. Saga middleware lay alert, awaiting a dispatched action that it's configured to respond to, triggering only upon recognizing such an event. It then executes the necessary side effects, preserving the application state from mutations.

Redux-Saga is designed to operate in a non-blocking, asynchronous manner. It listens for specific action types and can simultaneously start multiple tasks based on different actions without blocking the overall execution. This model equips Redux-Saga to usefully handle complex situations like race conditions and cancellation, features that can be crucial when dealing with real-world applications.

Enhancing Testability with Redux-Saga

The ability to pause and resume during execution by generator functions, combined with the features offered by ES6 'yield', make testing easier and more accurate. More than just pausing and resuming execution, the yielded effects from generator functions are easy to test since they represent pure data, providing a consistent and predictable output based on input.

Consider the following example of testing a saga:

it('Saga Test', () => {
  const gen = mySaga()

  // Check the first yield
  expect(gen.next().value).toEqual('first yield value')

  // Then the second
  expect(gen.next().value).toEqual('second yield value')

  // Check if the generator function is done
  expect(gen.next()).toEqual({value: undefined, done: true})
})

Testing with Redux-Saga draws its strength from its enhanced control over execution flow, making tests more deterministic. When using mock data, you can effectively step through your generator function, predicting and verifying each yield accurately. This granular control leads to a clear testing scope and enhanced maintainability.

Redux-Saga, by strengthening control over side effects, and boosting testability, serves to greatly elevate the efficacy of managing data flow within Redux architecture.

Comparative Study: Redux-Saga vs. Redux-Thunks vs. Redux Toolkit

In our quest to choose the best tool for managing state in our web-based applications, we will examine three libraries: Redux-Saga, Redux-Thunks, and Redux Toolkit. We will assess these options based on different criteria including performance, memory management, complexity, readability, modularity, and reusability.

Let's start by taking a brief look at each library.

Redux Thunks is essentially the default middleware for Redux due to its inclusion in Redux Toolkit by default. Redux-Saga, like Redux Thunk, is also a Redux middleware library. It is designed for enhanced asynchronous flow control, making it easy to handle complex side effects. But, as we will see, differing in it is design approach. The Redux Toolkit is a modern toolset for Redux, created with simplicity in mind, and again, comes with Redux Thunk as default middleware.

Analyzing Performance

When it comes to performance, all three libraries can handle average-sized applications efficiently. However, for larger and more complex applications, Redux-Saga's declarative approach allows for better management of race conditions and asynchronous actions, giving it a slight edge over Redux Thunks.

Memory Management

In terms of memory management, Redux Toolkit excels due to its default usage of Immer. This allows Redux Toolkit to handle immutable updates safely without causing unnecessary memory allocations, thus leading to more efficient memory usage.

Complexity

For newbie and intermediate developers, Redux-Saga may be a bit complex due to their extensive use of JavaScript generator functions. In contrast, Redux Thunk has a simpler and more straightforward approach, making it easier to grasp for developers new to state management. Redux Toolkit, on the other hand, reduces boilerplate code and simplifies complex Redux concepts, making it even easier to understand and use.

Code Readability and Modularity

When it comes to code readability and modularity, Redux Thunk may fall behind due to its compact design pattern. Redux-Saga, although complex, leads to a clearer data flow and organization due to decoupling. The usage of actions, workers, and watchers in Redux-Saga results in a clearer separation of concerns, making the code more readable and modular. Redux Toolkit once again shines by reducing boilerplate code and providing utilities for common use cases, increasing readability.

Reusability

Regarding reusability, Redux-Saga lends itself well to code reuse due to its declarative style and usage of generator functions. It makes it rather simpler to utilize common asynchronous patterns across different actions, unlike Redux Thunk. However, Redux Toolkit goes a step further, providing the createSlice and createAsyncThunk functions to encourage code reusability and DRY practices.

Conclusion

In summary, each of the discussed tools has its strengths and weaknesses. Redux Thunk provides ease of use and simplicity but may not be suitable for larger applications with complex states. Redux-Saga is preferable for complex asynchronous operations. In contrast, Redux Toolkit offers good performances, enhanced readability, and excellent reusability. It stood out in several categories, making it a strong recommendation if you anticipate your application to scale but also want a balance of simplicity.

However, remember that there is no generic 'right' or 'wrong' choice - it depends entirely on your application's specifics and team's experience levels. Keep asking yourself about your application's requirements: Do you need to perform complex asynchronous operations? Is your team familiar with generator functions? What's the scale of your application and how is it likely to grow? Answers to these, will guide you to the right choice.

Redux-Saga in Action: Real-world Implementations

First, let's take a look at a simple Redux-Saga implementation by setting up an application that fetches a list of users from a mock API.

Here's how our saga fetchUsers would look:

import { call, put, takeLatest } from 'redux-saga/effects';
import { FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE } from './actionTypes';
import Api from './api';

// worker Saga: fetches the list of users by making a request to the mock API
function* fetchUsers(action) {
    try {
        const users = yield call(Api.fetchUsers);
        yield put({ type: FETCH_USERS_SUCCESS, payload: users });
    } catch(e) {
        yield put({ type: FETCH_USERS_FAILURE, message: e.message });
    }
}

export default function* rootSaga() {
    yield takeLatest(FETCH_USERS_REQUEST, fetchUsers);
}

In this following code, the fetchUsers generator function acts as a worker saga. It performs the actual task of fetching the user data. We use the call function, provided by Redux-Saga, to invoke the Api.fetchUsers method, which returns a promise. The promise's result will be assigned to the users constant once it resolves.

If the promise resolves successfully, the saga dispatches a FETCH_USERS_SUCCESS action with the users payload. If the promise rejects, it dispatches a FETCH_USERS_FAILURE action, with an error message.

The rootSaga generator function acts as a watcher saga. It's looking for FETCH_USERS_REQUEST actions using the takeLatest helper function. When takeLatest sees such an action, it calls our worker saga, fetchUsers.

A common mistake you can encounter when using Redux-Saga relates to executing blocking calls. Ensure you are using the yield keyword when making a call to external services or when dispatching an action with put.

Avoid coding like this:

function* fetchUsersWrong(action) {
    try {
        const users = call(Api.fetchUsers); // Mistake!
        // We should use yield call(Api.fetchUsers)
        put({ type: FETCH_USERS_SUCCESS, payload: users }); // Mistake!
        // We should use yield put({type: FETCH_USERS_SUCCESS, payload: users})
    } catch(e) {
        put({ type: FETCH_USERS_FAILURE, message: e.message }); // Mistake!
        // We should use yield put({type: FETCH_USERS_SUCCESS, payload: users})
    }
}

The correct way is:

function* fetchUsers(action) {
    try {
        const users = yield call(Api.fetchUsers);
        yield put({ type: FETCH_USERS_SUCCESS, payload: users });
    } catch(e) {
        yield put({ type: FETCH_USERS_FAILURE, message: e.message });
    }
}

We must use the yield keyword because Redux Saga's call and put create Effect descriptions that are interpreted by the middleware. When we yield these Effect descriptions, the Redux-Saga middleware executes our actual intent.

Redux-Saga indeed brings an additional layer of handling asynchronous operations in Redux apps. It requires understanding ES6 generator functions and a bit of a mind shift, but once you get the hang of it, it provides a robust solution for managing state in complex scenarios.

Consider this: What makes Redux-Saga a better choice for handling various async activities in your application compared to other solutions you've tried?

Beyond the Basics: Advanced Redux-Saga Concepts

Watchers and Workers

In most Redux-Saga applications, two core actors are watchers and workers. The watcher function listens to dispatched actions and responds by executing a worker function. The worker function takes care of asynchronous side effects and then dispatches fresh actions to update the store.

Consider a hypothetical scenario where users make requests to an API and we need to manage the asynchronous flow. In that case, we could define our watcher and worker like this:

import { takeEvery, call, put } from 'redux-saga/effects';
import Api from '...';

function* watcherSaga() {
    yield takeEvery('API_REQUEST', workerSaga);
}

function* workerSaga() {
    try {
        const response = yield call(Api.fetchData);
        const data = response.data;

        yield put({type: 'API_SUCCESS', data}); //update the Redux store
    } catch (error) {
        yield put({type: 'API_ERROR', error}); //update the Redux store
    }
}

In this example, the watcher saga responds every time API_REQUEST action is dispatched by invoking the worker saga. The worker saga is wrapped in a try-catch block. It makes an API call and based on the response or error, dispatches API_SUCCESS or API_ERROR, respectively.

Effects

Redux-Saga uses the concept of effects - objects that contain instructions to be fulfilled by the middleware. The effects allow us to write declarative, testable, and readable asynchronous code that hardly uses callbacks or promises. Noteworthy effects include:

  1. call(fn, ...args) - Invokes the function fn with args. Useful for making API calls.

  2. put(action) - Instructs the middleware to dispatch an action to the Redux store. Useful for actions, especially those that update the state.

  3. take(pattern) - Pauses until an action that matches the pattern is dispatched.

  4. takeEvery(pattern, saga, ...args) - Spawns a new saga on each action dispatched to the store that matches pattern.

  5. select([selector, ...args]) - Selects a part of the state.

Know that despite their importance, these are not the only effects. There's a whole range of effects for managing control flow strategies, non-blocking calls, racing conditions, and more.

Helpers

Redux-Saga is rich in helper functions that abstract some common patterns of Saga composition. One such helper is takeEvery as used in the above example. Another notable helper is takeLatest. It spawns a new worker saga for every dispatched action that matches the pattern, cancelling any previous saga task if still in progress. It's beneficial when we want to handle only the last action of a certain type:

import { takeLatest, call, put } from 'redux-saga/effects';

//...

function* watcherSaga() {
    yield takeLatest('API_REQUEST', workerSaga);
}

In this example, if API_REQUEST action is dispatched while the previous request hadn't finished, takeLatest aborts the previous request before running workerSaga.

Advanced Operation Handling

Redux-Saga has built-in solutions for handling complex operations such as managing multiple asynchronous requests. For instance, if we had several API calls to be made concurrently, we could leverage all effect:

import { all, call } from 'redux-saga/effects';
import Api from '...';

function* fetchAllSaga() {
    const [users, products] = yield all([call(Api.fetchUsers), call(Api.fetchProducts)]);
    //...
}

In the example above, all effect triggers fetchUsers and fetchProducts simultaneously. It then waits for all of them to complete.

What other advanced Redux-Saga concepts might you need to leverage in your application? How could these tools change the way you handle complex operations in your JavaScript application?

Testing and Debugging with Redux-Saga

One of the most prominent advantages of using Redux-Saga over its counterparts is the ease and straightforwardness of testing. A comprehensive knowledge of unit testing is a prerequisite to understanding how Redux-Saga simplifies this task. Moreover, debugging, an essential part of every programming task, becomes much more manageable with Redux-Saga. Let's dive deeper into these aspects.

Testing Redux-Saga

The Saga model is built upon ES6 Generator Functions which allow Redux-Saga to have a clear flow control. Async functions (promises) are handled sequentially in a manner that mirrors synchronous blocking code in other languages. Because of this feature, testing becomes much simpler since it doesn't involve checking internal state or the ordering of callback executions—which are some of the complexities associated with testing async functions.

A Redux-Saga test only requires checking the order of yielded Effects, which is just an array of Objects. Here's how you can write a test for a simple Saga:

import { call, put } from 'redux-saga/effects'
import { api } from './services'
import { fetchUsers } from './sagas'

function* testSaga() {
    const users = yield call(api.fetchUsers);
    yield put({type: 'FETCH_USERS_SUCCESS', users});
}

it('Fetch Users Saga test', () => {

    const gen = fetchUsers();

    // Check if the first yield of the generator matches the api call
    expect(gen.next().value).toEqual(call(api.fetchUsers));

    const users = {};

    // Verify if the second yield matches the put Effect
    expect(gen.next(users).value).toEqual(put({type: 'FETCH_USERS_SUCCESS', users}));

    // Check if Saga is finished
    expect(gen.next().done).toBe(true);
});

The test checks each effect yielded by the generator in order, verifying if they yield the correct output.

Now, how can Redux-Saga contribute to one of the vital part of any programming process—the debugging?

Debugging with Redux-Saga

Debugging Redux-Saga, as with testing, benefits from the declarative nature of Sagas. Errors can be much easier to track within the contained environment of a Saga. Here are some practical methods for troubleshooting common issues with Redux-Saga:

  1. Check the Action Order: Redux-Logger or Redux-Devtools can be used to monitor the order of dispatched actions. If an action executes unintentionally, investigating the order of execution can hint at the root of the problem.

  2. Inspect the State: Redux-Devtools allows the current state and changes to be inspected in the browser. When you're dealing with a complex state, it can be handy to see the exact state at any given time.

  3. Error Handling in Sagas: By using Redux-Saga's declarative Effects in your Apps, error boundaries naturally form around your Saga, where you can catch and handle exceptions. This strategy significantly helps with avoiding unhandled promise rejections.

function* fetchUsers() {
    try {
        const users = yield call(api.fetchUsers);
        yield put({type: 'FETCH_USERS_SUCCESS', users});
    } catch(error) {
        yield put({type: 'FETCH_USERS_FAILED', error});
    }
}

Here, any error that occurs during the execution of the call effect will be caught and mapped to a FETCH_USERS_FAILED action.

To sum up, Redux-Saga provides clear and concise methods for testing and debugging. Its declarative nature assists in structurally managing side effects in Redux applications. As a result, testing, a challenging part of any development process, becomes less tricky. It also offers a number of practical debugging strategies that are a great help in a developer's day-to-day routine. Are you leveraging these features in your Redux-Saga application yet?

The Relevance of Redux-Saga in Modern Web Development

Redux-Saga continues to play an instrumental role in modern web development, especially in handling asynchronous operations and side effects within applications. With its ability to manage the asynchronous data flow effortlessly, Redux-Saga has established itself as a crucial tool within the frontend development landscape.

Pros of Redux-Saga

Before exploring modern alternatives, it is important that we delve into what makes Redux-Saga such a standout tool.

  1. Testability: One of Redux-Saga's greatest strengths is its ability to create testable code. Testing can often be a challenging task, especially when dealing with asynchronous behavior, but Redux-Saga's declarative effects simplify this process by providing an easy way to test your sagas without having to invoke any side effects. Take a look at this code snippet for an example:
import { call, put } from 'redux-saga/effects';
import { apiFetchData } from './api';
import { fetchDataSuccess, fetchDataFailure } from './actions';

function* fetchDataSaga() {
    try {
        const data = yield call(apiFetchData);
        yield put(fetchDataSuccess(data));
    } catch (error) {
        yield put(fetchDataFailure(error));
    }
}

In this simple saga, we're fetching data from an external API and dispatching an action based on the response. We can easily test this saga without having to make a real API call or dispatch a real action.

  1. Handling Complex Scenarios: Redux-Saga shines when it comes to handling complex scenarios, such as canceling operations, running operations in parallel, or managing operation order. Here's an example of how Redux-Saga can handle parallel operations:
import { all, call } from 'redux-saga/effects';
import { fetchUsers, fetchPosts } from './sagas';

function* rootSaga() {
    yield all([
        call(fetchUsers),
        call(fetchPosts)
    ]);
}

In the rootSaga, we're fetching users and posts data from external APIs at the same time by using the all effect.

  1. Centralizing Side Effect Operations: By taking control of side effects and moving them away from action creators and reducers to sagas, Redux-Saga can greatly simplify your codebase, making it more modular and manageable.

  2. Manage Efficiency: Redux-Saga can also efficiently manage resources, for instance, by preventing unnecessary network requests for fetching the same data multiple times.

Projecting the Future of Redux-Saga

While the demand and popularity of Redux-Saga have remained relatively stable, its future will still be shaped by several factors. These include the ongoing evolution of JavaScript and React, the development of state management solutions, and the emergence of new tools and libraries that may compete with it.

Modern Alternatives

Despite the relevance and strengths of Redux-Saga, there are other modern alternatives that developers can explore. These include Hooks, Context API, and RTK Query.

  1. Hooks: Introduced in version 16.8 of React, hooks provide a new way of handling side effects outside of components. Hooks like useCallback, useEffect, and useState can play similar roles to Redux-Saga but in a more straightforward and intuitive manner.
import { useState, useEffect } from 'react';
import { fetchUsers } from './api';

const useFetchUsers = () => {
    const [users, setUsers] = useState([]);

    useEffect(() => {
        fetchUsers()
            .then(data => setUsers(data))
            .catch(error => console.error('Failed to fetch users: ', error));
    }, []);

    return users;
}

In this example, we're using useState and useEffect hooks to fetch users from an external API and store them in the users state.

  1. Context API: Although not initially designed to manage asynchronous actions like Redux-Saga, with creative code design and a few additions, Context API can be used to handle async operations.

  2. RTK Query: Part of the Redux Toolkit, RTK Query simplifies data fetching and caching by abstracting Redux's state management, making these operations easier to manage.

import { createAsyncThunk } from '@reduxjs/toolkit';
import { apiFetchUsers } from './api';

const fetchUsers = createAsyncThunk(
    'users/fetch',
    async (_, { rejectWithValue }) => {
        try {
            const response = await apiFetchUsers();
            return response.data;
        } catch (error) {
            return rejectWithValue(error);
        }
    }
);

In the code above, the createAsyncThunk function from RTK Query makes fetching users from an external API simple and straightforward, without the need for manually dispatching actions or handling errors.

To wrap it up, while Redux-Saga continues to play a crucial role in modern web development, there are other viable alternatives worth considering. Developers should always stay open to exploring new tools and methodologies to keep up with the dynamic landscape of web development. Staying updated on the evolution of Redux-Saga, and scrutinizing its alternatives isn't just strategically wise, but a fundamental requirement in the ever-evolving arena of web development.

Summary

The article provides a deep understanding of Redux middleware and Sagas in modern web development. It explains how Redux-Saga, a companion library for Redux, manages asynchronous flow and side effects in applications. The article covers the architecture of Redux-Saga, the role of generators in handling side effects, setting up Redux-Saga as middleware, comparing Redux-Saga with other libraries like Redux-Thunks and Redux Toolkit, and real-world implementations of Redux-Saga.

Key takeaways from the article include the importance of Redux-Saga in managing complex states and handling asynchronous tasks in Redux applications. Redux-Saga provides a clear and concise method for testing and debugging, simplifying the development process. It offers advanced concepts like watchers and workers, effects, and helpers that allow developers to handle complex scenarios and efficiently manage resources.

A challenging technical task for the reader could be to create a Redux-Saga implementation for fetching data from an API and updating the Redux store based on the response. The task would require understanding how to define watcher and worker sagas, handle errors, and dispatch actions. The reader would need to leverage the knowledge gained from the article to complete the task successfully.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers