Table of Contents
Paystop Engine in Actual Operation
TypeScript
reducer as pure function
Test reducer function
Integrate all content
Conclusion
Home Web Front-end JS Tutorial A Deep Dive into Redux

A Deep Dive into Redux

Feb 14, 2025 am 10:13 AM

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:

1

npm init

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

npm i typescript --save-dev

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

2

3

4

5

6

7

8

9

10

11

{

  "compilerOptions": {

    "strict": true,

    "lib": ["esnext", "dom"],

    "outDir": ".bin",

    "sourceMap": true

  },

  "files": [

    "src/index"

  ]

}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

npm init

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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

1

npm i typescript --save-dev

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

2

3

4

5

6

7

8

9

10

11

{

  "compilerOptions": {

    "strict": true,

    "lib": ["esnext", "dom"],

    "outDir": ".bin",

    "sourceMap": true

  },

  "files": [

    "src/index"

  ]

}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login

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:

1

2

3

"jest": {

  "preset": "ts-jest"

}

Copy after login
Copy after login
Copy after login
Copy after login

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:

1

npm init

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

npm i typescript --save-dev

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
The type checker makes sure that these are the correct values ​​belonging to this object. With the initial state, start creating the reducer function:

1

2

3

4

5

6

7

8

9

10

11

{

  "compilerOptions": {

    "strict": true,

    "lib": ["esnext", "dom"],

    "outDir": ".bin",

    "sourceMap": true

  },

  "files": [

    "src/index"

  ]

}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login
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:

1

2

3

"jest": {

  "preset": "ts-jest"

}

Copy after login
Copy after login
Copy after login
Copy after login
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:

1

2

3

it('is true', () => {

  expect(true).toBe(true);

});

Copy after login
Copy after login
Copy after login
Please note that you will deduct stockOptions as the money will be used to buy company stocks. Suppose you want to deal with reimbursement:

1

npm init

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

npm i typescript --save-dev

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

2

3

4

5

6

7

8

9

10

11

{

  "compilerOptions": {

    "strict": true,

    "lib": ["esnext", "dom"],

    "outDir": ".bin",

    "sourceMap": true

  },

  "files": [

    "src/index"

  ]

}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login

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:

1

2

3

"jest": {

  "preset": "ts-jest"

}

Copy after login
Copy after login
Copy after login
Copy after login

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

1

2

3

it('is true', () => {

  expect(true).toBe(true);

});

Copy after login
Copy after login
Copy after login

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?

1

npm i redux --save

Copy after login
Copy after login

The pattern of handling bonuses is the same as this:

1

2

3

4

5

const BASE_PAY = 'BASE_PAY';

const REIMBURSEMENT = 'REIMBURSEMENT';

const BONUS = 'BONUS';

const STOCK_OPTIONS = 'STOCK_OPTIONS';

const PAY_DAY = 'PAY_DAY';

Copy after login
Copy after login

For stock options:

1

2

3

4

interface PayrollAction {

  type: string;

  amount?: number;

}

Copy after login
Copy after login

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:

1

npm init

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

npm i typescript --save-dev

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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:

1

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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

1

2

3

4

5

6

7

8

9

10

11

{

  "compilerOptions": {

    "strict": true,

    "lib": ["esnext", "dom"],

    "outDir": ".bin",

    "sourceMap": true

  },

  "files": [

    "src/index"

  ]

}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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

What about the default branch in reducer?

1

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

Copy after login
Copy after login
Copy after login
Copy after login

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:

1

2

3

"jest": {

  "preset": "ts-jest"

}

Copy after login
Copy after login
Copy after login
Copy after login

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

1

2

3

it('is true', () => {

  expect(true).toBe(true);

});

Copy after login
Copy after login
Copy after login

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:

1

npm i redux --save

Copy after login
Copy after login

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:

1

2

3

4

5

const BASE_PAY = 'BASE_PAY';

const REIMBURSEMENT = 'REIMBURSEMENT';

const BONUS = 'BONUS';

const STOCK_OPTIONS = 'STOCK_OPTIONS';

const PAY_DAY = 'PAY_DAY';

Copy after login
Copy after login

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

1

2

3

4

interface PayrollAction {

  type: string;

  amount?: number;

}

Copy after login
Copy after login

Finally, run another payroll and unsubscribe to Redux storage:

1

2

3

4

5

6

7

8

9

10

11

12

13

interface PayStubState {

  basePay: number;

  reimbursement: number;

  bonus: number;

  stockOptions: number;

  totalPay: number;

  payHistory: Array<PayHistoryState>;

}

 

interface PayHistoryState {

  totalPay: number;

  totalCompensation: number;

}

Copy after login

The final result is as follows:

1

2

3

4

5

6

7

8

9

10

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

Copy after login

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 of this Website
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

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

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1664
14
PHP Tutorial
1266
29
C# Tutorial
1239
24
Demystifying JavaScript: What It Does and Why It Matters Demystifying JavaScript: What It Does and Why It Matters Apr 09, 2025 am 12:07 AM

JavaScript is the cornerstone of modern web development, and its main functions include event-driven programming, dynamic content generation and asynchronous programming. 1) Event-driven programming allows web pages to change dynamically according to user operations. 2) Dynamic content generation allows page content to be adjusted according to conditions. 3) Asynchronous programming ensures that the user interface is not blocked. JavaScript is widely used in web interaction, single-page application and server-side development, greatly improving the flexibility of user experience and cross-platform development.

The Evolution of JavaScript: Current Trends and Future Prospects The Evolution of JavaScript: Current Trends and Future Prospects Apr 10, 2025 am 09:33 AM

The latest trends in JavaScript include the rise of TypeScript, the popularity of modern frameworks and libraries, and the application of WebAssembly. Future prospects cover more powerful type systems, the development of server-side JavaScript, the expansion of artificial intelligence and machine learning, and the potential of IoT and edge computing.

JavaScript Engines: Comparing Implementations JavaScript Engines: Comparing Implementations Apr 13, 2025 am 12:05 AM

Different JavaScript engines have different effects when parsing and executing JavaScript code, because the implementation principles and optimization strategies of each engine differ. 1. Lexical analysis: convert source code into lexical unit. 2. Grammar analysis: Generate an abstract syntax tree. 3. Optimization and compilation: Generate machine code through the JIT compiler. 4. Execute: Run the machine code. V8 engine optimizes through instant compilation and hidden class, SpiderMonkey uses a type inference system, resulting in different performance performance on the same code.

Python vs. JavaScript: The Learning Curve and Ease of Use Python vs. JavaScript: The Learning Curve and Ease of Use Apr 16, 2025 am 12:12 AM

Python is more suitable for beginners, with a smooth learning curve and concise syntax; JavaScript is suitable for front-end development, with a steep learning curve and flexible syntax. 1. Python syntax is intuitive and suitable for data science and back-end development. 2. JavaScript is flexible and widely used in front-end and server-side programming.

JavaScript: Exploring the Versatility of a Web Language JavaScript: Exploring the Versatility of a Web Language Apr 11, 2025 am 12:01 AM

JavaScript is the core language of modern web development and is widely used for its diversity and flexibility. 1) Front-end development: build dynamic web pages and single-page applications through DOM operations and modern frameworks (such as React, Vue.js, Angular). 2) Server-side development: Node.js uses a non-blocking I/O model to handle high concurrency and real-time applications. 3) Mobile and desktop application development: cross-platform development is realized through ReactNative and Electron to improve development efficiency.

How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration) How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration) Apr 11, 2025 am 08:22 AM

This article demonstrates frontend integration with a backend secured by Permit, building a functional EdTech SaaS application using Next.js. The frontend fetches user permissions to control UI visibility and ensures API requests adhere to role-base

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration) Building a Multi-Tenant SaaS Application with Next.js (Backend Integration) Apr 11, 2025 am 08:23 AM

I built a functional multi-tenant SaaS application (an EdTech app) with your everyday tech tool and you can do the same. First, what’s a multi-tenant SaaS application? Multi-tenant SaaS applications let you serve multiple customers from a sing

From C/C   to JavaScript: How It All Works From C/C to JavaScript: How It All Works Apr 14, 2025 am 12:05 AM

The shift from C/C to JavaScript requires adapting to dynamic typing, garbage collection and asynchronous programming. 1) C/C is a statically typed language that requires manual memory management, while JavaScript is dynamically typed and garbage collection is automatically processed. 2) C/C needs to be compiled into machine code, while JavaScript is an interpreted language. 3) JavaScript introduces concepts such as closures, prototype chains and Promise, which enhances flexibility and asynchronous programming capabilities.

See all articles