Subscribe to single property change in store in Redux

React NativeReduxRedux Framework

React Native Problem Overview


In Redux I can easily subscribe to store changes with

store.subscribe(() => my handler goes here)

But what if my store is full of different objects and in a particular place in my app I want to subscribe to changes made only in a specific object in the store?

React Native Solutions


Solution 1 - React Native

There is no way to subscribe to part of the store when using subscribe directly, but as the creator of Redux says himself - don't use subscribe directly! For the data flow of a Redux app to really work, you will want one component that wraps your entire app. This component will subscribe to your store. The rest of your components will be children to this wrapper component and will only get the parts of the state that they need.

If you are using Redux with React then there is good news - the official 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() function on your "smart" component and using the mapStateToPropsfunction as a first parameter. To recap:

Wrap root component with Provider component that subscribes to store changes

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

Now any child of <App /> that is wrapped with connect() will be a "smart" component. You can pass in mapStateToProps to pick certain parts of the state and give it those as props.

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)

Obviously <App /> can have many children and you can pick and choose which parts of the state the mapStateToProps should listen to for each of its children. I'd suggest reading the docs on usage with React to get a better understanding of this flow.

Solution 2 - React Native

Redux only offers a single generic way to know when the store has updated: the subscribe method. Callbacks to subscribe do not get any info on what might have changed, as the subscribe API is deliberately low-level, and simply runs each callback with no arguments. All you know is that the store has updated in some way.

Because of that, someone has to write specific logic to compare old state vs new state, and see if anything has changed. You could handle this by using React-Redux, specifying a mapStateToProps function for your component, implementing componentWillReceiveProps in your component, and checking to see if specific props from the store have changed.

There are also a couple addon libraries that try to handle this case: https://github.com/ashaffer/redux-subscribe and https://github.com/jprichardson/redux-watch . Both basically let you specify a specific portion of the state to look at, using different approaches.

Solution 3 - React Native

In addition to what Andy Noelker said, mapStateToProps not only passes part of the state properly down your component tree, it also subscribes to changes made directly in these subscribed portions of the state.

It is true that every mapStateToProp function you bind to the store gets called each time any part of the state is changed, but the result of the call gets shallow compared to the previous call - if top level keys you subscribed onto did not change (the reference stays the same). Then mapStateToProps would not call re-render. So if you want the concept to work, you have to keep mapStateToProps simple, no merging, type changing or anything, they should simply pass down parts of the state.

If you want to reduce the data from the state when subscribing, for example you had list data in the state and you want to convert it to object with ids as keys, or you want to join multiple states into data structures, you should combine mapStateToProps with createSelector from reselect library, by doing all these modifications inside selector. Selectors are pure functions that reduce and cache state chunks passed in as input and if input did not change - they return exactly the same reference they did on the last call - without performing the reduction.

Solution 4 - React Native

Created a hack to help understand the subscribers can be differentiated based on store data, with multiple store capability.

//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.

*/

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionkarol.barkowskiView Question on Stackoverflow
Solution 1 - React NativeAndy NoelkerView Answer on Stackoverflow
Solution 2 - React NativemarkeriksonView Answer on Stackoverflow
Solution 3 - React NativeIvanXView Answer on Stackoverflow
Solution 4 - React NativeKhazi AfzalView Answer on Stackoverflow