翻譯|Better Redux Selectors with Ramda

原文編程

目標

目標是把Redux中selector:redux

export const getUserName = state => state.user.name

export const isLoggedIn = state => state.user.id != null

export const getTotalItemCount = state =>
    Object.values(state.items.byId)
        .reduce((total, item) => total + item.count, 0)
複製代碼

轉換爲:數組

import R from 'ramda'

// Helper functions
const isNotNil = R.complement(R.isNil)
const pathIsNotNil = path => R.compose(isNotNil, R.path(path))
const addProp = propName => R.useWith(R.add, [R.identity, R.prop(propName)])
const sumProps = propName => R.reduce(addProp(propName), 0)
const sumCounts = sumProps('count')

// Selector functions
export const getUserName = R.path(['user', 'name'])
export const isLoggedIn = pathIsNotNil(['user', 'id'])
export const getTotalItemCount =
    R.compose(sumCounts, R.values, R.path(['items', 'byId']))
複製代碼

介紹

Selector函數回顧

selector 的概念在 Redux 的文檔中出現過, 爲了代替直接在 React 組件中訪問 state tree,能夠定義從 state 獲取數據的函數. 能夠認爲是從 state獲取數據的 API. 不是必須的, 甚至 Redux也不是必定要用.bash

Selector函數接收 Redux的 state 對象做爲參數, 返回任何你須要的數據. 實例:ide

從 State tree 獲取 屬性函數式編程

function getUserName(state) {
    return state.user.name
}
複製代碼

從屬性衍生數據函數

function isLoggedIn(state) {
    return state.user.id != null
}
複製代碼

從一個列表數據中衍生數據ui

function getTotalItemCount(state) {
    return Object.keys(state.items.byId)
        .reduce(function(total, id) {
            return total + state.items.byId[id].count
        }, 0)
}
複製代碼

假定列表的每一個 item都有一個count屬性, 這個 selector 使用array.reduce()函數計算count的綜合.spa

export function getUserName(state) {
    return state.user.name
}

export function isLoggedIn(state) {
    return state.user.id != null
}

export function getTotalItemCount(state) {
    return Object.keys(state.items.byId)
        .reduce(function(total, id) {
            return total + state.items.byId[id].count
        }, 0)
}
複製代碼

若是使用 ES2015語法,會更簡潔code

export const getUserName = state => state.user.name

export const isLoggedIn = state => state.user.id != null

export const getTotalItemCount = state =>
    Object.values(state.items.byId)
        .reduce((total, item) => total + item.count, 0)
複製代碼

Object.values 是 ES2017的語法

Ramda 的原則

  • 自動柯理化
  • 數據最後傳入

使用 Ramda編寫 Selectors

getUserName

export const getUserName = state => R.path(['user', 'name'], state)
複製代碼

改成柯理化的版本:

export const getUserName = state => R.path(['user', 'name'])(state)
複製代碼

就能夠等待數據了

export const getUserName = R.path(['user', 'name'])
複製代碼

這裏的函數就看不到數據了, 這個技術被稱爲 point-free style 或者tacit programming.

isLoggedIn

咱們想要的版本是獲取用於的 ID, 而後判斷是否爲 true

export const isLoggedIn = pathIsNotNullOrUndefined(['user', 'id'])
複製代碼

Ramda的 isNil 方法

R.isNil(null) // true
R.isNil(undefined) // true
R.isNil(false) // false
複製代碼

如今能夠改成:

const isNotNil = val => !R.isNil(val)
複製代碼

在更進一步:

const isNotNil = val => R.not(R.isNil(val))
複製代碼

另外一個方法:

const isNotNil = R.complement(R.isNil)

isNotNil(true) // true
isNotNil(null) // false
isNotNil(undefined) // false
複製代碼

進一步重構的版本:

const pathIsNotNil = (path, state) => isNotNil(R.path(path, state))

//⛔️柯理化的版本

const pathIsNotNil = path => state => isNotNil(R.path(path, state))
複製代碼

也就是從一個 state獲取嵌套屬性,而且判斷是否爲 true, 因爲屬性的路徑是在 default 中已經配置好的, 因此咱們可使用柯理化提早配置獲取的方法, 等待變化的 state 數據. 這就配置出了一個處理 state 的工廠. 爲何柯理化在函數式編程中很重要,這就是緣由. 配置出的工廠是與數據獨立的, 這就是上面提到的 tacit programming 無參數編程, 函數的配置和傳入的參數是無關的, 頂可能是對參數的類型作出約束.

在使用 R.compose 作重構

const pathIsNotNil = path => state => R.compose(isNotNil, R.path)(path, state)
複製代碼

整個操做和path,state 有關, 經過 path從 state 獲取屬性, 而後判斷是否爲 true,

const pathIsNotNil = path => state => R.compose(isNotNil, R.path(path))(state)
//👇👇柯理化

const pathIsNotNil = path => R.compose(isNotNil, R.path(path))
複製代碼

在 pathIsNotNil中實際的數據只有 state,path 是屬於配置項, 也就是在程序中是不改變的.

export const isLoggedIn = pathIsNotNil(['user', 'id'])

//配置了從對象的路徑 user.id 獲取屬性值,而後判斷是否爲 true
複製代碼

getTotalItemCount

//剛開始的方法

export const getTotalItemCount = state =>
    Object.values(state.items.byId)
        .reduce((total, item) => total + item.count, 0)
複製代碼

面對複雜的問題, 使用函數式風格進行分解是比較好的選擇.建立一個函數sumCounts,接收一個數組, 返回項目中count屬性的總計.

const sumCounts = items =>
    items.reduce((total, item) => total + item.count, 0)
複製代碼

使用 map

const sumCounts = R.compose(R.sum, R.map(R.prop('count')))
複製代碼

R.map 對數組的每一項調用 R.prop('count')函數, 獲取的全部count 屬性,放到一個新的數組中, 以後用 R.sum 對數組中的屬性值作合計

Ramda 針對map 這個函數的使用,也有更簡單的方法

const sumCounts = R.compose(R.sum, R.pluck('count'))
複製代碼

R.pluck 從數組中獲取每一項的屬性值

使用 Reduce 函數的替代方案

const sumCounts = R.reduce((total, item) => total + item.count, 0)
複製代碼
const addCount=(total,item)=>total+item.count
const sumCounts = R.reduce(addCount, 0)
複製代碼

使用 Ramda 的 R.add 方法

const addCount = (total, item) => R.add(total, item.count)
複製代碼

從對象中獲取屬性

const addCount = (total, item) => R.add(total, prop('count', item))

// 柯理化形式
const addCount = (total, item) => R.add(total, prop('count')(item))
複製代碼

上面的函數通用的模式是: 接收兩個參數,傳遞給另外一個函數, 第一個參數不懂, 對第二個參數運用一個函數進行處理,以後再執行第一個函數

Ramda 有一個函數能夠幫咱們完成這個任務, useWith, useWith函數接收兩個參數, 第一個參數是單個的函數,和一個函數數組. 數組中的函數被稱爲變換函數-在對應位置的參數被第一個函數調用以前進行變換處理. 換句話說,數組的第一個函數對第一個參數進行處理, 第二個函數對第二個參數進行處理,以此類推.轉換後的參數傳遞個第一個參數的函數.

在咱們的實例中,第一個參數的函數 R.add,

const addCount = R.useWith(R.add, [/* transformers */])
複製代碼

須要對 R.add 的第二個參數進行處理, 從 count屬性中獲取值, 因此放在第二個函數的位置

const addCount = R.useWith(R.add [/* 1st */, R.prop('count')])
複製代碼

第一個參數怎麼辦? 這個參數對應的是 total 值, 不須要轉換 , Ramda有一個函數能夠原封不動的返回一個數值, R.identity.

const addCount = R.useWith(R.add, [R.identity, R.prop('count')])
複製代碼

如今的函數:

const addCount = R.useWith(R.add, [R.identity, R.prop('count')])

const sumCounts = R.reduce(addCount, 0)
複製代碼

如今能夠得到更爲通用的方式,獲取任意的屬性,

const addProp = propName => R.useWith(R.add, [R.identity, R.prop(propName)])

const sumProps = propName => R.reduce(addProp(propName), 0)

const sumCounts = sumProps('count')
複製代碼

參數的轉換方式也能夠抽象出來:

const addTransformedItem = transformer =>
    R.useWith(R.add, [R.identity, transformer])

const sumTransformedItems = transformer =>
    R.reduce(addTransformedItem(transformer), 0)

const totalItemComments = R.compose(R.length, R.prop('comments'))

const sumComments = sumTransformedItems(totalItemComments)
複製代碼

最終在 Ramda 的幫助下, 總的數據獲取流是有更小的能夠重用的函數組成的..

結構

若是有下面的數據

const state = {
    items: {
        byId: {
            'item1': { id: 'item1', count: 2 },
            'item2': { id: 'item2', count: 4 },
            'item3': { id: 'item3', count: 7 }
        }
    }
}
複製代碼
export const getTotalItemCount =
    R.compose(sumCounts, R.values, R.path(['items', 'byId']))
複製代碼

最終獲得的結果

mport R from 'ramda'

// Helper functions
const isNotNil = R.complement(R.isNil)
const pathIsNotNil = path => R.compose(isNotNil, R.path(path))
const addProp = propName => R.useWith(R.add, [R.identity, R.prop(propName)])
const sumProps = propName => R.reduce(addProp(propName), 0)
const sumCounts = sumProps('count')

// Selector functions
export const getUserName = R.path(['user', 'name'])
export const isLoggedIn = pathIsNotNil(['user', 'id'])
export const getTotalItemCount =
    R.compose(sumCounts, R.values, R.path(['items', 'byId']))
複製代碼

函數式編程最好的解釋應該是: 數據和要對數據進行操做的函數式分離開的. 基於此, 就能夠發現, React的組件也能夠當作一個函數, 接收應用的數據, 對數據進行處理,以後進行渲染.

相關文章
相關標籤/搜索