利用Redux-actions 來減少Redux的Boilerplate Code(2018/04/01更新)

Why redux-actions?

最近一直在Redux卡關, 除了有點玄妙的邏輯以外, 更重要的是他的boilerplate code很繁瑣, 於是在Telegram群組的大大推薦下, 我使用了Redux-Actions的插件, 效果差了很多! 人生也變得美好XD 在這邊我講記錄我更改的過程.

背景提要:

我要在我的程式用HTML 5的Api取得使用者的座標, 再來更新使用者位置的天氣, 當中會用到兩個api, 在這邊我其實會有點疑問: 到底HTML5 GeoLocation的取得, 是不是Async的呢? 不過我不管啦, 先設定成是Async的好了, 那在這個範例就是有兩個Async的動作.(目前進度的關係, 先只有用到座標Api)

正常來說在傳統的React-Redux應該是這個樣子:


詳細的Code請點我
weather-actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { getUserLocation as getUserLocationFromApi } from 'component/userLocation.jsx';

// 先設定好每一個action的return

function startGetLocation() {
return {
type: 'LOCATION/START_GET_LOCATION',
}
}

function endGetLocation(lat, lng) {
return {
type: 'LOCATION/END_GET_LOCATION',
lat,
lng
}
}

//下面的function是利用redux thunk來進行async程式

export function location() {
return (dispatch, getState) => {
dispatch(startGetLocation());

return getUserLocationFromApi().then(res => {
const {latitude: lat, longitude: lng} = res.coords;

dispatch(endGetLocation(lat, lng));
}) catch (err) {
console.log('Error getting User Location', err);
}
}
}


weather-reducer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const initLocationState = {
requestStatus: false,
lat: NaN,
lng: NaN
};

export function location( state= initLocationState, action) {
switch (action.type) {
case '@LOCATION/START_GET_LOCATION':
return {
...state,
};
case '@FORECAST/END_GET_LOCATION':
return {
...state,
lat,
lng
};
default:
return state;
}
}



createAction

這個是原本的code, 但是用上redux-actions之後code變成這樣:

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
function startGetUserLocation() {
return {
type: 'LOCATION/START_GET_LOCATION',
}
}

function endGetUserLocation(lat, lng) {
return {
type: 'LOCATION/END_GET_LOCATION',
lat,
lng,
}
}

After:

1
2
3
4
5
import { createAction } from 'redux-actions';
import { getUserLocation as getUserLocationFromApi } from 'component/userLocation.jsx';

const startGetUserLocation = createAction('START_GET_USER_LOCATION');
const endGetUserLocation = createAction('END_GET_USER_LOCATION');

handleActions

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const initLocationState = {
requestStatus: false,
lat: NaN,
lng: NaN
};

export function location( state= initLocationState, action) {
switch (action.type) {
case '@LOCATION/START_GET_LOCATION':
return {
...state,
requestStatus: false,
};
case '@FORECAST/END_GET_LOCATION':
return {
...state,
lat,
lng,
requestStatus: true,
};
default:
return state;
}
}

After:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { handleActions, combineActions } from 'redux-actions';

const initLocationState = {
requestStatus: false,
lat: NaN,
lng: NaN
};

export const location = handleActions({
GET_LOCATION: (state, action) => {
return {...state lat:action.payload.lat, lng: action.payload.lng}
},
GET_LOCATION_STATUS: (state, action) => {
return {...state, requestStatus: action.payload.requestStatus}
}
}, initLocationState);

Spread and Destructuring

但是我覺得action.payload.lat, action.payload.lng, res.coords.latitude很繁冗, 後來我利用了spread和解構的語法, 讓他看起來比較整潔:

Before:

1
2
3
4
5
6
7
8
export const location = handleActions({
GET_LOCATION: (state, action) => {
return {...state lat:action.payload.lat, lng: action.payload.lng}
},
GET_LOCATION_STATUS: (state, action) => {
return {...state, requestStatus: action.payload.requestStatus}
}
}, initLocationState);

After:

1
2
3
4
5
6
7
8
export const location = handleActions({  
GET_LOCATION: (state, action) => {
return {...state, ...action.payload}
},
GET_LOCATION_STATUS: (state, action) => {
return {...state, ...action.payload}
}
}, initLocationState);

combineActions:

再來我覺得原本handleActions那邊有點冗,

Before:

1
2
3
4
5
6
7
8
export const location = handleActions({
GET_LOCATION: (state, action) => {
return {...state, ...action.payload}
},
GET_LOCATION_STATUS: (state, action) => {
return {...state, ...action.payload}
}
}, initLocationState);

配合 Redux Actions 的 combineActions:

After:

1
2
3
4
5
export const location = handleActions({
[combineActions('GET_LOCATION', 'GET_LOCATION_STATUS')](state, action) {
return { ...state, ...action.payload}
}
}, initLocationState);

combineActions的小坑:

在官方的Api文檔範例中, 他使用的是return { …state, counter: state.counter + amount }; 讓人會誤以為, 只能單純為計算才能combineActions(還是只有我這樣覺得?). 而我發現只要在dispatch的時候, 用object({})的方式傳到reducer, 再把傳入的object使用spread action.payload, 他就會根據你之前的設定好的initState, 找出相應的key來覆蓋. 但是這邊有點問題: combineAction裡面的args會越來越多,變得超級無敵長, 真的很可怕, 除了多寫幾個handle actions來分隔以外, 還有其他方法嗎?

20180401 combineActions的理解錯誤

使用了以上的方法一段時間后,發現我什麽都能dispatch,甚至lat、lng的欄位,都能dispatch boolean值(預設值是數字),讓我維護變得更麻煩,更加難除錯。我發現了一個人,他寫了關於一系列相當清楚的教學,他不建議使用combineActions,畢竟很多時候每一個action都有自己的目的(不用到的幹嘛不簡化成一個XD),因此在某種程度上,combineActions是不被需要的。

下面將會新增利用這樣的概念寫好的code:

結論

簡單來說, Redux-Actions他簡化了一些模板code, 比方說action不用每一個都寫function, reducers不用寫switch case, 這樣的確少了很多麻煩. 今天的經驗分享就到這邊!

完整code

*20180401新增:


新版本的示範凑得請點我
todo-reducers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const initTodoState = {
todos: [],
todoLoading: false
}

export const todo = handleActions({
['SET_LIST_TODO']:
function(state, {payload}) {
console.log(payload);

return {
...state,
todos: payload
}
},
['SET_CREATE_TODO']:
function (state, {payload}) {
return state
},
['SET_CHECK_TODO']:
function (state, {paylaod}) {
return state
},
['FETCH_TODO_REQUEST']:
function (state, payload) {
return {
...state,
todoLoading: true
}
},
['FETCH_TODO_RESPONSE']:
function (state, {payload}) {
return {
...state,
todoLoading: false
}
}
}, initTodoState);


todo-actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { createAction, createActions } from 'redux-actions';

const {
// setlistTodo,
setCreateTodo,
setCheckTodo,
fetchTodoRequest,
fetchTodoResponse
} = createActions(
// 'SET_LIST_TODO',
'SET_CREATE_TODO',
'SET_CHECK_TODO',
'FETCH_TODO_REQUEST',
'FETCH_TODO_RESPONSE'
)

const setlistTodo = createAction('SET_LIST_TODO');

export function listTodo(searchText = '') {
return async (dispatch, getState) => {
dispatch(fetchTodoRequest());
try {
console.log(searchText);

const todos = await listTodoFromApi(searchText);
console.log(todos);

dispatch(setlistTodo(todos));
dispatch(fetchTodoResponse());
return todos;
} catch (error) {
console.error('Error getting todo', error);
};
}
}

export function createTodo(mood, text) {
return async (dispatch, getState) => {
dispatch(fetchTodoRequest());
try {
await createTodoFromApi(mood, text);
dispatch(setCreateTodo());
await dispatch(listTodo());
dispatch(fetchTodoResponse());
} catch (error) {
console.error('Error creating todo', error);
}
}
}

export function checkTodo(id) {
return async (dispatch, getState) => {
dispatch(fetchTodoRequest());
try {
await checkTodoFromApi(id);
dispatch(setCheckTodo());
await dispatch(listTodo());
dispatch(fetchTodoResponse());
} catch (error) {

}
}
}



完整的code請點我
weather-reducers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { handleActions, combineActions } from 'redux-actions';

const initLocationState = {
requestStatus: false,
lat: NaN,
lng: NaN
};


export const location = handleActions({
[combineActions('GET_LOCATION', 'GET_LOCATION_STATUS')](state, action) {
return { ...state, ...action.payload}
}
}, initLocationState);

const initWeatherState = {
city: 'na',
code: -1,
group:'na',
description: 'N/A',
temp: NaN,
weatherLoading: false,
masking: false,
lat: NaN,
lng: NaN,
};

export const weather = handleActions ({
[combineActions('START_GET_USER_LOCATION', 'END_GET_USER_LOCATION', 'GET_LOCATION_WEATHER', 'START_GET_WEATHER', 'END_GET_WEATHER', 'RESET_WEATHER', 'MASK_TODAY_BG', 'UNMASK_TODAY_BG')](state, action) {
console.log(`action.payload:`,action.payload);

return { ...state, ...action.payload, }
}
}, initWeatherState)


weather-actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { getUserLocation as getUserLocationFromApi } from 'component/userLocation.jsx';
import { getLocationWeatherToday as getLocationWeatherTodayFromApi } from 'Api/openWeatherMapApi.js';

/* Today */

const startGetWeather = createAction('START_GET_WEATHER');
const endGetWeather = createAction('END_GET_WEATHER');
const resetWeather = createAction('RESET_WEATHER');
const maskTodayBg = createAction('MASK_TODAY_BG');
const unmaskTodayBg = createAction('UNMASK_TODAY_BG');
const getUserLocation = createAction('GET_LOCATION');
const startGetUserLocation = createAction('START_GET_USER_LOCATION');
const endGetUserLocation = createAction('END_GET_USER_LOCATION');
const getWeatherLocation = createAction('GET_LOCATION_WEATHER');
const getWeatherLocationStatus = createAction('GET_LOCATION_WEATHER_STATUS');

export function getWeather(city, unit) {
return (dispatch, getState) => {
dispatch(startGetWeather({city, unit}));
dispatch(maskTodayBg({masking: true}));

return getWeatherFromApi(city, unit).then(weather =>{
console.log(weather);
const {city, code, group , description, temp, unit} = weather;
dispatch(endGetWeather({city, code, group, description, temp}));
dispatch(setUnit(unit));
}).then( () => {
setTimeout(() => {
dispatch(unmaskTodayBg({masking: false}));
}, 600);
}).catch(err => {
console.error('Error getting weather', err);
dispatch(resetWeather());
setTimeout(() => {
dispatch(unmaskTodayBg({masking: false}));
}, 600);
});
};
};

/**
* location
*/

export const getLocationWeather = function(unit){
return async (dispatch, getState) => {
dispatch(startGetUserLocation({requestStatus: true}));
dispatch(maskTodayBg({masking: true}));

try {
const res = await getUserLocationFromApi();
const {latitude: lat, longitude: lng} = res.coords;
console.log(lat, lng);
dispatch(getUserLocation({lat, lng}));

const weather = await getLocationWeatherTodayFromApi(lat, lng, unit);
console.log(weather);
const {city, code, group , description, temp} = weather;
dispatch(getWeatherLocation({city, code, group , description, temp}));

dispatch(endGetUserLocation({requestStatus: false}));
setTimeout(() => {
dispatch(unmaskTodayBg({masking: false}));
}, 600);
} catch (err) {
console.log('Error getting User Location', err);
setTimeout(() => {
dispatch(unmaskTodayBg({masking: false}));
}, 600);
// TODO: some reset action?
}
}
}


參考資料

  1. https://github.com/dc198689/orange/blob/master/src/actions/posts/getPosts.js
  2. https://github.com/ecmadao/Coding-Guide/blob/master/Notes/React/Redux/%E4%BD%BF%E7%94%A8%20Redux%20%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%BA%94%E7%94%A8%20%E2%80%94%E2%80%94%20redux-actions.md
  3. https://redux-actions.js.org/docs/introduction/Tutorial.html
  4. https://ithelp.ithome.com.tw/articles/10187009
  5. https://www.jianshu.com/p/6ba5cd795077