将使用 `connect()` 隐式创建的容器组件转换为显式容器组件

Convert container component implicitly created with `connect()` to explicit container component

我有一个名为 Volume 的无状态功能组件,它使用 connect() 创建容器组件:

const mapStateToProps = (state) => ({
  volume: state.get('volume')
})

let Volume = (props) => {
  if (props.volume === 'Infinity') {
    return (
      <Text
        style={{ ...formStyles.text, ...styles.text }}>
        Incalculable
      </Text>
    )
  } else {
    return (
      <Text
        style={{ ...formStyles.text, ...styles.text }}>
        {props.volume + ' litres'}
      </Text>
    )
  }
}

Volume.propTypes = {
  volume: React.PropTypes.string
}

Volume = connect(
  mapStateToProps,
  null
)(Volume)

export default Volume

我现在需要在其上实现 componentDidMount 生命周期方法,它将 运行 一个将整个 redux 存储作为参数的函数,然后调度一个操作来更新 store.volume ,然后可以将其传递给初始的 Volume 演示组件进行显示。所以我想返璞归真,不使用connect(),这样我就可以在容器组件中实现生命周期方法。我从来没有用过connect()

这是我的尝试:

import { Text } from 'react-native'
import React, { Component } from 'react'
import { formStyles } from '../../style'
import calcVol from '../../calcVol'
import { updateVolume } from '../../actions/updateDimension.action'

export class VolumeContainer extends Component {
  componentDidMount() {
    const { store } = this.context
    this.unsubscribe = store.subscribe(() =>
      this.forceUpdate()
    )
    let litres = calcVol(store)
    store.dispatch(updateVolume(litres))
  }

  componentWillUnmount() {
    this.unsubscribe()
  }

  render() {
    const { store } = this.context
    return (
      <VolumePresentational volume={store.getState().volume} />
    )
  }
}

VolumeContainer.contextTypes = {
  store: React.PropTypes.object
}

let VolumePresentational = (props) => {
  if (props.volume === 'Infinity') {
    return (
      <Text
        style={{ ...formStyles.text, ...styles.text }}>
        Incalculable
      </Text>
    )
  } else {
    return (
      <Text
        style={{ ...formStyles.text, ...styles.text }}>
        {props.volume + ' litres'}
      </Text>
    )
  }
}

VolumePresentational.propTypes = {
  volume: React.PropTypes.string
}

const styles = {
  text: {
    marginTop: 20,
    fontSize: 30,
    fontWeight: 'bold'
  }
}

export default VolumeContainer

代码进入我的 volCalc(store) 函数时出现错误:

state.get is not a function

所以我传递给 componentDidMount 中的 calcVol() 的商店不能是商店。

我做错了什么?

一般来说,如果某物可以完全根据状态计算(即它是该状态的函数),那么它不应该处于状态本身。比如一个状态如:

{
    x: 3,
    y: 2,
    z: 5,
    volume: 30
}

这会导致状态中的信息重复,并最终导致各种与保持事物同步的重要问题。贯穿 React 和 Redux 的共同主题应该是 "single source of truth",即信息存在于一处且仅存在于一处。在上面的示例中,有关音量的信息存储在 volume 属性 中,但也存储在 xyz 属性的组合中- 所以现在有两个真相来源。

就将此方法应用于应用程序而言,最好在存储中保留最简单的数据形式,并在选择器函数中组合该数据,return 我们需要的聚合数据.

以上面的例子为例,我们将有:

const state = {
    x: 3,
    y: 2,
    z: 5
}

和计算体积的选择器函数:

const selectVolume = state => state.x * state.y * state.z;

如果计算量很大,那么我们可以memoize选择器函数来避免重复计算相同的数据:

const makeSelectVolume = () => {
    const memo = {};
    return state => {
        const (x, y, z} = state;
        // if we have memoized a value for these parameters return it
        if( x in memo && y in memo[x] && z in memo[x][y] ) {
            return memo[x][y][z];
        }
        // otherwise calculate it
        const volume = x * y * z;
        // and memoize it
        memo[x][y][z] = volume;
        return volume;
    }
}

幸运的是,优秀的库 reselect 自动创建了我们可以与 redux 一起使用的记忆选择器,因此我们不需要自己记忆它们。通过重新选择,我们将使我们的选择器功能如下:

const makeSelectVolume = () => createSelector(
    state => state.x,
    state => state.y,
    state => state.z,
    (x, y, z) => x * y * z
};

现在我们只需要将其与我们的组件集成:

// other imports as usual
import { makeSelectVolume } from 'path/to/selector';

const Volume = (props) => {
  if (props.volume === 'Infinity') {
    return (
      <Text
        style={{ ...formStyles.text, ...styles.text }}>
        Incalculable
      </Text>
    )
  } else {
    return (
      <Text
        style={{ ...formStyles.text, ...styles.text }}>
        {props.volume + ' litres'}
      </Text>
    )
  }
}

Volume.propTypes = {
  volume: React.PropTypes.string
}

const mapStateToProps = createStructuredSelector({
    volume: makeSelectVolume()
});

export default connect(
  mapStateToProps
)(Volume);

在您的情况下,只需将选择器中的体积计算逻辑替换为您的 calcVol 函数即可。请注意,calcVol 必须是纯函数,即它不会以任何方式修改状态。

编辑

如果我们的状态是具有以下结构的 ImmutableJS 映射:

{
  widths: {
    width1: 2,
    width2: 7,
  },
  heights: {
    height1: 4,
    height2: 3
  }
}

我们可以将选择器函数编写为:

const makeSelectVolume = () => createSelector(
    state => state.get('widths'),
    state => state.get('heights'),
    (widths, heights) => calculateSomething(widths.toJS(), heights.toJS())
};

或如:

const makeSelectVolume = () => createSelector(
        state => state.get('widths').get('width1'),
        state => state.get('widths').get('width2'),
        state => state.get('heights').get('height1'),
        state => state.get('heights').get('height2'),
        (width1, width2, height1, height2) => calculateSomething({ width1, width 2 },{ height1, height2 })
    };

如果您选择第二个,那么 memoization 将正常工作,但如果您选择第一个,您可能需要使用 reselect 的 createSelectorCreator 来实现自定义相等性检查器。

本质上,重新选择检查是否有任何组成的选择器值已更改,默认情况下使用 ===(只有 return 如果它们实际上是内存中的同一对象,则为真)。如果您 return 来自组合选择器的不可变映射,那么您需要检查这些映射的值是否相同,例如使用不可变映射的 equals 方法:

import { createSelectorCreator, defaultMemoize } from 'reselect';

const createImmutableMapSelector = createSelectorCreator(
    defaultMemoize,
    (a, b) => a.equals(b)
);

const makeSelectVolume = () => createImmutableMapSelector(
    state => state.get('widths'),
    state => state.get('heights'),
    (widths, heights) => calculateSomething(widths.toJS(), heights.toJS())
};

这带来了更多的复杂性,但利用了 Immutables 深度比较的快速性。最终无论哪种方式都可以。