订阅 Redux 中存储中的单个 属性 更改

Subscribe to single property change in store in Redux

在 Redux 中,我可以使用

轻松订阅商店更改
store.subscribe(() => my handler goes here)

但是,如果我的商店充满了不同的对象,并且在我的应用程序的特定位置,我想订阅仅在商店中的特定对象中所做的更改,该怎么办?

直接使用 subscribe 时无法订阅商店的一部分,但正如 Redux 的创建者自己所说 - don't use subscribe directly! 为了让 Redux 应用程序的数据流真正发挥作用,您将需要一个组件来包装您的整个应用程序。该组件将订阅您的商店。您的其余组件将 children 到此包装器组件,并且只会获得它们需要的部分状态。

如果您将 Redux 与 React 一起使用,那么有个好消息 - "smart" 组件上的官方 react-redux package takes care of this for you! It provides that wrapper component, called a <Provider />. You will then have at least one "smart component" that listens to state changes passed down by the Provider from the store. You can specify which parts of the state it should listen to, and those pieces of the state will be passed down as props to that component (and then of course, it can pass those down to its own children). You can specify that by using the connect() 函数,并使用 mapStateToProps 函数作为第一个参数。回顾一下:

Provider 订阅存储更改的组件包装根组件

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

现在 <App /> 中用 connect() 包裹的任何 child 都将成为 "smart" 组件。您可以传入 mapStateToProps 以选择状态的某些部分并将其作为道具。

const mapStateToProps = (state) => {
    return {
        somethingFromStore: state.somethingFromStore
    }
}

class ChildOfApp extends Component {
    render() {
        return <div>{this.props.somethingFromStore}</div>
    }
}

//wrap App in connect and pass in mapStateToProps
export default connect(mapStateToProps)(ChildOfApp)

显然 <App /> 可以有很多 children,您可以选择 mapStateToProps 应该为每个 children 监听状态的哪些部分.我建议阅读 usage with React 上的文档以更好地理解此流程。

Redux 只提供了一种通用的方法来知道商店何时更新:subscribe 方法。对 subscribe 的回调不会获得任何关于可能发生变化的信息,因为 subscribe API 是故意低级别的,并且只是运行每个回调而没有参数。您所知道的是商店已经以某种方式更新了。

因此,必须有人编写特定的逻辑来比较旧状态和新状态,看看是否有任何变化。您可以通过使用 React-Redux 来处理这个问题,为您的组件指定一个 mapStateToProps 函数,在您的组件中实现 componentWillReceiveProps,并检查商店中的特定道具是否已更改。

还有一些插件库试图处理这种情况:https://github.com/ashaffer/redux-subscribe and https://github.com/jprichardson/redux-watch。两者基本上都让您使用不同的方法指定要查看的状态的特定部分。

除了 Andy Noelker 所说的,mapStateToProps 不仅将状态的一部分正确地向下传递到您的组件树中,它还订阅了直接在状态的这些订阅部分中所做的更改。

确实,每次状态的任何部分发生更改时,您绑定到存储的每个 mapStateToProp 函数都会被调用,但与上一次调用相比,调用的结果变得浅 - 如果是顶级您订阅的密钥没有改变(引用保持不变)。然后 mapStateToProps 不会调用重新渲染。所以如果你想让这个概念起作用,你必须保持 mapStateToProps 简单,没有合并,类型改变或任何东西,它们应该简单地传递状态的一部分。

如果你想在订阅的时候减少state中的数据,比如state中有list数据,你想把它转换成以id为key的object,或者你想将多个state拼接成数据结构,您应该通过在选择器中进行所有这些修改,将 mapStateToProps 与 reselect 库中的 createSelector 结合起来。选择器是纯函数,可以减少和缓存作为输入传入的状态块,如果输入没有改变 - 它们 return 与上次调用时所做的引用完全相同 - 不执行减少。

创建了一个 hack 来帮助理解订阅者可以根据商店数据进行区分,具有多个商店功能。

//import { createStore } from 'redux';
let createStore = require('redux').createStore;
let combineReducers = require('redux').combineReducers;
/**
 * This is a reducer, a pure function with (state, action) => state signature.
 * It describes how an action transforms the state into the next state.
 *
 * The shape of the state is up to you: it can be a primitive, an array, an object,
 * or even an Immutable.js data structure. The only important part is that you should
 * not mutate the state object, but return a new object if the state changes.
 *
 * In this example, we use a `switch` statement and strings, but you can use a helper that
 * follows a different convention (such as function maps) if it makes sense for your
 * project.
 */
function counter(state = 0, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1
        case 'DECREMENT':
            return state - 1
        default:
            return state
    }
}

function messanger(state = 'Mr, khazi', action) {
    switch(action.type) {
        case 'WELCOME':
            return 'Hello, Mr Khazi';
        case 'BYE':
            return 'Bye, Mr Khazi';
        case 'INCREMENT':
            return 'Incremented khazi';
        default:
            return state;
    }
};

function latestAction(state = null, action) {
    switch(action.type) {
        case 'WELCOME':
            return '$messanger';
        case 'BYE':
            return '$messanger';
        case 'INCREMENT':
            return '$messanger, $counter';
        case 'DECREMENT':
            return '$counter';
        default:
            return state;
    }
};

let reducers = {
    counts: counter,
    message: messanger,
    action: latestAction
};

let store = createStore(
    combineReducers(reducers, latestAction)
);
  
// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
//let store = createStore(counter)

// You can use subscribe() to update the UI in response to state changes.
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// However it can also be handy to persist the current state in the localStorage.
store.subscribe(() => {
    if(store.getState().action.indexOf('messanger') !== -1) {
        console.log('subscribed for counter actions', store.getState());
    }
});

store.subscribe(() => {
    if (store.getState().action.indexOf('counter') !== -1) {
        console.log('subscribed for messanger actions', store.getState());
    }
});

// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
console.log('----------------Action with both subscriber-------------');
store.dispatch({ type: 'INCREMENT' });
console.log('---------------Action with counter subscriber-----------');
store.dispatch({ type: 'DECREMENT' });
console.log('---------------Action with messenger subscriber---------');
store.dispatch({ type: 'WELCOME' });

/*
    every reducer will execute on each action.

*/