search
HomeWeb Front-endJS TutorialA Deep Dive into Redux

A Deep Dive into Redux

Core points

  • Redux simplifies state management in modern applications by acting as a predictable state container, which is critical to maintaining the stability of the application when it is scaled.
  • TypeScript integration enhances Redux by forcing type safety, which adds a layer of predictability and helps maintain large code bases by simplifying refactoring.
  • The reducer in Redux is designed as a pure function to ensure that it does not have side effects, thereby enhancing the testability and reliability of state management.
  • Use Jest to simplify unit testing, Jest works seamlessly with TypeScript to test Redux actions and reducers, ensuring that every component works as expected.
  • This article demonstrates the actual implementation of Redux by building a payroll engine, showing how Redux manages state transitions and deals with side effects in real application scenarios.

Building a stateful modern application is a complex task. As the state changes, the application becomes unpredictable and difficult to maintain. This is where Redux comes in. Redux is a lightweight library for handling state. Think of it as a state machine.

In this article, I will explore Redux's state containers in depth by building a payroll processing engine. The app will store payrolls along with all the extras such as bonuses and stock options. I'll use pure JavaScript and TypeScript for type checking to keep the solution simple. Since Redux is very easy to test, I will also use Jest to verify the application.

In this tutorial, I assume you have some understanding of JavaScript, Node, and npm.

First, you can initialize this application with npm:

npm init

When asking for test commands, continue to use jest. This means that npm t will start Jest and run all unit tests. The main file will be index.js to keep it simple. Feel free to answer the rest of npm init questions.

I will use TypeScript to do type checking and determine the data model. This helps conceptualize what we are trying to build.

To get started with TypeScript:

npm i typescript --save-dev

I will put some of the dependencies in the development workflow in devDependencies. This clearly shows which dependencies are prepared for developers and which dependencies will be used in production environments. Once TypeScript is ready, add a startup script in package.json:

"start": "tsc && node .bin/index.js"

Create an index.ts file under the src folder. This separates the source file from the rest of the project. If you do npm start, the solution won't be executed. This is because you need to configure TypeScript.

Create a tsconfig.json file with the following configuration:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}

I could have put this configuration in the tsc command line argument. For example, tsc src/index.ts --strict .... But putting all of this in a separate file is much clearer. Note that the startup script in package.json only requires a tsc command.

Here are some reasonable compiler options that will give us a good starting point and what each option means:

  • strict: Enable all strict type checking options, i.e. --noImplicitAny, --strictNullChecks, etc.
  • lib: A list of library files included in the compiled.
  • outDir: Redirect the output to this directory.
  • sourceMap: Generate source map file for debugging.
  • files: The input file provided to the compiler.

Because I will use Jest for unit testing, I will continue to add it:

npm init

ts-jest dependency adds type checking for the test framework. One thing to note is to add a jest configuration in package.json:

npm i typescript --save-dev

This allows the test framework to pick up TypeScript files and know how to convert them. A nice feature is that you can do type checking when running unit tests. To make sure this project is ready, create a __tests__ folder containing an index.test.ts file. Then, a sanitation check is performed. For example:

"start": "tsc && node .bin/index.js"

Now execute npm start and npm t will not cause any errors. This tells us that we can now start building solutions. But before we do this, let's add Redux to the project:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}

This dependency will be used in production environments. Therefore, there is no need to include it with --save-dev. If you check your package.json, it will be in dependencies.

Paystop Engine in Actual Operation

The payroll engine will contain the following: wages, reimbursements, bonuses and stock options. In Redux, you cannot update the status directly. Instead, an action is scheduled to notify storage of any new changes.

So this leaves the following operation type:

npm i jest ts-jest @types/jest @types/node --save-dev

PAY_DAY operation type can be used to issue checks on payday and track salary history. These types of operations guide the rest of the design as we perfect the payroll engine. They capture events in the state life cycle, such as setting the base wage amount. These action events can be attached to any content, whether it is a click event or a data update. Redux operation types are abstract about where scheduling comes from. The status container can run on the client and/or on the server.

TypeScript

Using type theory, I will determine the data model based on the state data. For each payroll operation, such as the operation type and optional amount. The amount is optional because PAY_DAY does not require funds to process the payroll. I mean, it can charge customers, but ignore it for now (maybe introduced in the second edition).

For example, put it in src/index.ts:

"jest": {
  "preset": "ts-jest"
}

For payroll status, we need an attribute for basic salary, bonus, etc. We will also use this status to maintain salary history.

This TypeScript interface should do:

npm init

For each property, note that TypeScript uses a colon to specify the type. For example, : number. This determines the type contract and adds predictability to the type checker. Redux can be enhanced using a type system with explicit type declarations. This is because Redux state containers are built for predictable behavior.

This idea is not crazy or radical. Learning Redux Chapter 1 (SitePoint Premium members only) explains this well.

As the application changes, type checking adds additional predictability. As applications expand, type theory also helps to simplify the reconstruction of large code segments.

Using the type conceptualization engine now helps create the following operation functions:

npm i typescript --save-dev

The good thing is that if you try to do processBasePay('abc'), the type checker will warn you. Destroying type contracts reduces predictability of state containers. I use a single operation contract like PayrollAction to make the payroll processor more predictable. Note that the amount is set in the operation object through the ES6 attribute abbreviation. The more traditional approach is amount: amount, which is more verbose. Arrow functions, such as () => ({}), are a concise way to write functions that return object literals.

reducer as pure function

The reducer function requires a state and an operation parameter. The state should have an initial state with a default value. So, can you imagine what our initial state might look like? I think it needs to start from scratch with an empty salary history list.

Example:

"start": "tsc && node .bin/index.js"
The type checker makes sure that these are the correct values ​​belonging to this object. With the initial state, start creating the reducer function:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}
Redux reducer has a pattern in which all operation types are processed by switch statements. But before iterating through all switch cases, I will create a reusable local variable:

npm i jest ts-jest @types/jest @types/node --save-dev
Note that if you do not change the global state, you can change the local variables. I use the let operator to convey that this variable will change in the future. Changing the global state (such as state or operational parameters) can cause the reducer to be impure. This functional paradigm is crucial because the reducer function must be kept pure. JavaScript From Newbie to Ninja Chapter 11 (SitePoint Premium members only).

Start the switch statement of reducer to handle the first use case:

"jest": {
  "preset": "ts-jest"
}
I use the ES6 rest operator to keep the state property unchanged. For example,...state. You can overwrite any attribute after the rest operator in a new object. basePay comes from deconstruction, which is much like pattern matching in other languages. The computeTotalPay function is set as follows:

it('is true', () => {
  expect(true).toBe(true);
});
Please note that you will deduct stockOptions as the money will be used to buy company stocks. Suppose you want to deal with reimbursement:

npm init

Since the amount is optional, make sure it has a default value to reduce failure. This is the advantage of TypeScript, as the type checker will spot this trap and warn you. The type system knows certain facts, so it can make reasonable assumptions. Suppose you want to deal with the bonus:

npm i typescript --save-dev

This mode makes the reducer readable because it maintains only the state. You get the amount of the operation, calculate the total salary, and create a new object text. Nothing is different when dealing with stock options:

"start": "tsc && node .bin/index.js"

For processing payroll on payday, it requires erasing bonuses and reimbursement. These two attributes are not kept in the state in each payroll. And, add an entry to the salary history. Basic wages and stock options can be kept in the state because they do not change frequently. With that in mind, this is how PAY_DAY is handled:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}

In an array like newPayHistory, use the extension operator, which is the antonym for rest. Unlike rest of the property in the collection object, it expands the project. For example, [...payHistory]. Although the two operators look similar, they are not the same. Watch carefully, as this may appear in interview questions.

Using pop() for payHistory will not change the state. Why? Because slice() returns a brand new array. Arrays in JavaScript are copied by reference. Assigning an array to a new variable does not change the underlying object. Therefore, care must be taken when dealing with these types of objects.

Because lastPayHistory is likely undefined, I use the poor man's null value merge to initialize it to zero. Please note that (o && o.property) || 0 mode is used for merging. There may be a more elegant way to do this in future versions of JavaScript or even TypeScript.

Each Redux reducer must define a default branch. To ensure that the state does not become undefined:

npm i jest ts-jest @types/jest @types/node --save-dev

Test reducer function

One of the many benefits of writing pure functions is that they are easy to test. Unit testing is a test where you have to expect predictable behavior, and you can automate all tests as part of the build. In __tests__/index.test.ts, cancel the virtual test and import all the functions of interest:

"jest": {
  "preset": "ts-jest"
}

Note that all functions are set to export, so you can import them. For basic salary, start the payroll engine reducer and test it:

it('is true', () => {
  expect(true).toBe(true);
});

Redux Sets the initial state to undefined. Therefore, it is always a good idea to provide default values ​​in the reducer function. How about handling reimbursement?

npm i redux --save

The pattern of handling bonuses is the same as this:

const BASE_PAY = 'BASE_PAY';
const REIMBURSEMENT = 'REIMBURSEMENT';
const BONUS = 'BONUS';
const STOCK_OPTIONS = 'STOCK_OPTIONS';
const PAY_DAY = 'PAY_DAY';

For stock options:

interface PayrollAction {
  type: string;
  amount?: number;
}

Note that when stockOptions is greater than totalPay, totalPay must remain unchanged. Since this hypothetical company is ethical, it does not want to take money from its employees. If you run this test, please note that totalPay is set to -10, because stockOptions will be deducted. This is why we test the code! Let's fix the place where the total salary is calculated:

npm init

If the money earned by employees does not have enough money to buy company stocks, please continue to skip the deduction. Also, make sure it resets stockOptions to zero:

npm i typescript --save-dev

This fix determines whether they have enough money in newStockOptions. With this, the unit test passes, the code is sound and meaningful. We can test positive use cases where there is enough money to make deductions:

"start": "tsc && node .bin/index.js"

For paydays, use multiple statuses to test and make sure a one-time transaction does not persist:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}

Note how I adjust oldState to verify the bonus and reset the reimbursement to zero.

What about the default branch in reducer?

npm i jest ts-jest @types/jest @types/node --save-dev

Redux sets an operation type like INIT_ACTION at the beginning. We only care whether our reducer has some initial state set.

Integrate all content

At this point, you may start to wonder if Redux is more of a design pattern. If you answer that it is both a pattern and a lightweight library, you are right. In index.ts, import Redux:

"jest": {
  "preset": "ts-jest"
}

The next code example can be wrapped around this if statement. This is a stopgap so unit tests don't leak into integration tests:

it('is true', () => {
  expect(true).toBe(true);
});

I do not recommend doing this in actual projects. Modules can be placed in separate files to isolate components. This makes it easier to read and does not leak problems. Unit testing also benefits from the fact that modules run independently.

Use payrollEngineReducer to start Redux storage:

npm i redux --save

Each store.subscribe() returns a subsequent unsubscribe() function that can be used for cleaning. It unsubscribes to the callback when it is scheduled through the storage. Here I use store.getState() to output the current state to the console.

Suppose the employee earned 300, had 50 reimbursements, 100 bonuses, and 15 for the company's stock:

const BASE_PAY = 'BASE_PAY';
const REIMBURSEMENT = 'REIMBURSEMENT';
const BONUS = 'BONUS';
const STOCK_OPTIONS = 'STOCK_OPTIONS';
const PAY_DAY = 'PAY_DAY';

To make it more fun, make another 50 reimbursement and process another payroll:

interface PayrollAction {
  type: string;
  amount?: number;
}

Finally, run another payroll and unsubscribe to Redux storage:

interface PayStubState {
  basePay: number;
  reimbursement: number;
  bonus: number;
  stockOptions: number;
  totalPay: number;
  payHistory: Array<PayHistoryState>;
}

interface PayHistoryState {
  totalPay: number;
  totalCompensation: number;
}

The final result is as follows:

export const processBasePay = (amount: number): PayrollAction =>
  ({type: BASE_PAY, amount});
export const processReimbursement = (amount: number): PayrollAction =>
  ({type: REIMBURSEMENT, amount});
export const processBonus = (amount: number): PayrollAction =>
  ({type: BONUS, amount});
export const processStockOptions = (amount: number): PayrollAction =>
  ({type: STOCK_OPTIONS, amount});
export const processPayDay = (): PayrollAction =>
  ({type: PAY_DAY});

As shown, Redux maintains state, changes state and notifies subscribers in a neat small package. Think of Redux as a state machine, which is the true source of state data. All of this adopts coding best practices, such as a sound functional paradigm.

Conclusion

Redux provides a simple solution to complex state management problems. It relies on the functional paradigm to reduce unpredictability. Because reducer is a pure function, unit testing is very easy. I decided to use Jest, but any test framework that supports basic assertions will work.

TypeScript uses type theory to add an additional layer of protection. Combine type checking with functional programming and you get sturdy code that is almost never interrupted. Most importantly, TypeScript does not get in the way of working while adding value. If you notice, once the type contract is in place, there is almost no additional encoding. The type checker does the rest. Like any good tool, TypeScript automates encoding discipline while remaining invisible. TypeScript barking loudly, but it bites lightly.

If you want to try this project (I hope you do this), you can find the source code for this article on GitHub.

The above is the detailed content of A Deep Dive into Redux. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
The Origins of JavaScript: Exploring Its Implementation LanguageThe Origins of JavaScript: Exploring Its Implementation LanguageApr 29, 2025 am 12:51 AM

JavaScript originated in 1995 and was created by Brandon Ike, and realized the language into C. 1.C language provides high performance and system-level programming capabilities for JavaScript. 2. JavaScript's memory management and performance optimization rely on C language. 3. The cross-platform feature of C language helps JavaScript run efficiently on different operating systems.

Behind the Scenes: What Language Powers JavaScript?Behind the Scenes: What Language Powers JavaScript?Apr 28, 2025 am 12:01 AM

JavaScript runs in browsers and Node.js environments and relies on the JavaScript engine to parse and execute code. 1) Generate abstract syntax tree (AST) in the parsing stage; 2) convert AST into bytecode or machine code in the compilation stage; 3) execute the compiled code in the execution stage.

The Future of Python and JavaScript: Trends and PredictionsThe Future of Python and JavaScript: Trends and PredictionsApr 27, 2025 am 12:21 AM

The future trends of Python and JavaScript include: 1. Python will consolidate its position in the fields of scientific computing and AI, 2. JavaScript will promote the development of web technology, 3. Cross-platform development will become a hot topic, and 4. Performance optimization will be the focus. Both will continue to expand application scenarios in their respective fields and make more breakthroughs in performance.

Python vs. JavaScript: Development Environments and ToolsPython vs. JavaScript: Development Environments and ToolsApr 26, 2025 am 12:09 AM

Both Python and JavaScript's choices in development environments are important. 1) Python's development environment includes PyCharm, JupyterNotebook and Anaconda, which are suitable for data science and rapid prototyping. 2) The development environment of JavaScript includes Node.js, VSCode and Webpack, which are suitable for front-end and back-end development. Choosing the right tools according to project needs can improve development efficiency and project success rate.

Is JavaScript Written in C? Examining the EvidenceIs JavaScript Written in C? Examining the EvidenceApr 25, 2025 am 12:15 AM

Yes, the engine core of JavaScript is written in C. 1) The C language provides efficient performance and underlying control, which is suitable for the development of JavaScript engine. 2) Taking the V8 engine as an example, its core is written in C, combining the efficiency and object-oriented characteristics of C. 3) The working principle of the JavaScript engine includes parsing, compiling and execution, and the C language plays a key role in these processes.

JavaScript's Role: Making the Web Interactive and DynamicJavaScript's Role: Making the Web Interactive and DynamicApr 24, 2025 am 12:12 AM

JavaScript is at the heart of modern websites because it enhances the interactivity and dynamicity of web pages. 1) It allows to change content without refreshing the page, 2) manipulate web pages through DOMAPI, 3) support complex interactive effects such as animation and drag-and-drop, 4) optimize performance and best practices to improve user experience.

C   and JavaScript: The Connection ExplainedC and JavaScript: The Connection ExplainedApr 23, 2025 am 12:07 AM

C and JavaScript achieve interoperability through WebAssembly. 1) C code is compiled into WebAssembly module and introduced into JavaScript environment to enhance computing power. 2) In game development, C handles physics engines and graphics rendering, and JavaScript is responsible for game logic and user interface.

From Websites to Apps: The Diverse Applications of JavaScriptFrom Websites to Apps: The Diverse Applications of JavaScriptApr 22, 2025 am 12:02 AM

JavaScript is widely used in websites, mobile applications, desktop applications and server-side programming. 1) In website development, JavaScript operates DOM together with HTML and CSS to achieve dynamic effects and supports frameworks such as jQuery and React. 2) Through ReactNative and Ionic, JavaScript is used to develop cross-platform mobile applications. 3) The Electron framework enables JavaScript to build desktop applications. 4) Node.js allows JavaScript to run on the server side and supports high concurrent requests.

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Atom editor mac version download

Atom editor mac version download

The most popular open source editor

mPDF

mPDF

mPDF is a PHP library that can generate PDF files from UTF-8 encoded HTML. The original author, Ian Back, wrote mPDF to output PDF files "on the fly" from his website and handle different languages. It is slower than original scripts like HTML2FPDF and produces larger files when using Unicode fonts, but supports CSS styles etc. and has a lot of enhancements. Supports almost all languages, including RTL (Arabic and Hebrew) and CJK (Chinese, Japanese and Korean). Supports nested block-level elements (such as P, DIV),

Dreamweaver Mac version

Dreamweaver Mac version

Visual web development tools

SublimeText3 Linux new version

SublimeText3 Linux new version

SublimeText3 Linux latest version

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools