2.10 Create Reducers For Each Action
We'll cover the following...
Reducers are functions that receive state and action objects from a Redux store and return a new state to be stored back into Redux.
It’s important not to directly modify the given state here. Reducers must be pure functions and must return a new state.
-
Reducer functions are called from the Container that will be created when a user action occurs.
-
When the Reducer returns a state, Redux passes the new state to each component, and React renders each component again.
2.10.1 Immutable Data Structures
-
JavaScript primitive data type(number, string, boolean, undefined, and null) => immutable
-
Object, array and function => mutable
Changes to the data structure are known to be buggy. Since our store consists of state objects and arrays, we need to implement a strategy to keep the state immutable.
There are three ways to change the state here:
ES5
In the example above, the first and second mutate the state object. The second example mutates because Object.assign() merges all its arguments into the first argument.
The third example doesn’t mutate the state. It merges the contents of state and { foo: 123 } into a new empty object which is the first argument.
The spread operator introduced in ES6 provides a simpler way to keep the state immutable.
ES6 (ES2015)
For more information about the spread operator, see here.
2.10.2 Create a Reducer for ChangeClimate
First, we will create ChangeClimate through test-driven development method.
In Part1, our app was generated through create-react-app, so we basically use jest as test runner.
The jest looks for a test file using one of the following naming conventions:
Create teslaRangeApp.spec.js in src/reducers and create a test case.
After create the test, run the npm test command. You should be able to see the following test failure message. This is because we have not written the appReducer yet.
To make the first test successful, we need to create teslaRangeApp.js in the same reducers directory and write initial state and reducer functions.
src/reducers/teslaRangeApp.js
Next, import teslaRangeApp.js from teslaRangeApp.spec.js and set initialState.
src/reducers/teslaRangeApp.spec.js
Run npm test again and the test will succeed.
In the current test case, the action type is {}, so the initialState is returned.
Now let’s test the CHANGE_CLIMATE action.
Add the following climateChangeState and CHANGE_CLIMATE test cases to teslaRangeApp.spec.js.
Then add the CHANGE_CLIMATE case, updateStats, and calculateStatsfunctions to teslaRangeApp.js. Then import the BatteryService.js that was used in part 1.
If you check the test results, you can see that the two test cases are successful.
What we have implemented so far is that the changes in the state that occur when the user turns the air conditioner on and off in the application through the test runner only from the viewpoint of Action and Reducer without Redux Store or View.
-
Check out teslaRangeApp.js as we’ve written it so far
-
Check out teslaRangeApp.spec.js
2.10.3 Create Reducer for others
If you create the rest of the test cases by referring to the above method, you finally define the teslaRangeApp.js file in which the reducers of all the apps are defined and the teslaRangeApp.spec.js to test them.
The final code can be found at:
After completing the code and testing, a total of seven test cases must succeed.
import appReducer from './teslaRangeApp';
const initialState = {
carstats:[
{miles:246, model:"60"},
{miles:250, model:"60D"},
{miles:297, model:"75"},
{miles:306, model:"75D"},
{miles:336, model:"90D"},
{miles:376, model:"P100D"}
],
config: {
speed: 55,
temperature: 20,
climate: true,
wheels: 19
}
}
const climateChangeState = {
carstats:[
{miles:267, model:"60"},
{miles:273, model:"60D"},
{miles:323, model:"75"},
{miles:334, model:"75D"},
{miles:366, model:"90D"},
{miles:409, model:"P100D"}
],
config: {
speed: 55,
temperature: 20,
climate: false,
wheels: 19
}
}
const speedUpState = {
carstats:[
{miles:242, model:"60"},
{miles:248, model:"60D"},
{miles:292, model:"75"},
{miles:303, model:"75D"},
{miles:332, model:"90D"},
{miles:371, model:"P100D"}
],
config: {
speed: 60,
temperature: 20,
climate: false,
wheels: 19
}
}
const speedDownState = {
carstats:[
{miles:267, model:"60"},
{miles:273, model:"60D"},
{miles:323, model:"75"},
{miles:334, model:"75D"},
{miles:366, model:"90D"},
{miles:409, model:"P100D"}
],
config: {
speed: 55,
temperature: 20,
climate: false,
wheels: 19
}
}
const wheelChangeState = {
carstats:[
{miles:261, model:"60"},
{miles:268, model:"60D"},
{miles:316, model:"75"},
{miles:327, model:"75D"},
{miles:359, model:"90D"},
{miles:389, model:"P100D"}
],
config: {
speed: 55,
temperature: 20,
climate: false,
wheels: 21
}
}
const temperatureUpState = {
carstats:[
{miles:264, model:"60"},
{miles:272, model:"60D"},
{miles:319, model:"75"},
{miles:333, model:"75D"},
{miles:367, model:"90D"},
{miles:398, model:"P100D"}
],
config: {
speed: 55,
temperature: 30,
climate: false,
wheels: 21
}
}
const temperatureDownState = {
carstats:[
{miles:261, model:"60"},
{miles:268, model:"60D"},
{miles:316, model:"75"},
{miles:327, model:"75D"},
{miles:359, model:"90D"},
{miles:389, model:"P100D"}
],
config: {
speed: 55,
temperature: 20,
climate: false,
wheels: 21
}
}
describe('test reducer', () => {
it('should handle initial stat', () => {
expect(
appReducer(undefined, {})
).toEqual(initialState)
})
it('should handle CHANGE_CLIMATE', () => {
expect(
appReducer(initialState,{
type: 'CHANGE_CLIMATE'
})
).toEqual(climateChangeState)
})
it('should handle SPEED_UP', () => {
expect(
appReducer(climateChangeState,{
type: 'SPEED_UP',
value: 55,
step: 5,
maxValue: 70
})
).toEqual(speedUpState)
})
it('should handle SPEED_DOWN', () => {
expect(
appReducer(speedUpState,{
type: 'SPEED_DOWN',
value: 60,
step: 5,
minValue: 45
})
).toEqual(speedDownState)
})
it('should handle CHANGE_WHEEL', () => {
expect(
appReducer(speedDownState,{
type: 'CHANGE_WHEEL',
value: 21
})
).toEqual(wheelChangeState)
})
it('should handle TEMPERATURE_UP', () => {
expect(
appReducer(wheelChangeState,{
type: 'TEMPERATURE_UP',
value: 20,
step: 10,
maxValue: 40
})
).toEqual(temperatureUpState)
})
it('should handle TEMPERATURE_DOWN', () => {
expect(
appReducer(temperatureUpState,{
type: 'TEMPERATURE_DOWN',
value: 30,
step: 10,
minValue: -10
})
).toEqual(temperatureDownState)
})
})