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

时间:2017-03-30 22:37:44

标签: reactjs react-native redux react-redux

我有一个名为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不是函数

所以我在storeDidMount中传入calcVol()的商店一定不能成为商店。

我做错了什么?

1 个答案:

答案 0 :(得分:1)

一般来说,如果可以完全从状态计算某些东西(即它是该状态的函数),那么它本身不应该处于状态。例如,状态如:

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

这导致状态内的信息重复,并最终导致各种非平凡的问题,使事物彼此保持同步。 React和Redux的共同主题应该是“单一的事实来源”,即信息只存在于一个地方和一个地方。在上面的示例中,有关卷的信息存储在volume属性中,但也存储在xyz属性的组合中,因此存在现在有两个真相来源。

在将此方法应用于应用程序方面,最好在商店中保留最简单的数据形式,并将这些数据合并到返回我们需要的聚合数据的选择器函数中。

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

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来实现自定义相等检查器。

基本上重新选择检查是否有任何组合的选择器值已更改,默认情况下使用===(如果它们在内存中确实是相同的对象,则仅返回true)。如果您从组合选择器返回不可变地图,那么您需要检查地图的值是否相同,例如使用不可变地图的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深度比较的快速性。最终,无论哪种方式都可行。