Subscribe to single property change in store in Redux
Asked Answered
P

4

95

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?

Pro answered 25/3, 2016 at 1:50 Comment(0)
P
144

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.

Predispose answered 25/3, 2016 at 4:32 Comment(6)
That's great. I would like to have a component to react to an action. Let's say there is one component NavBar that dispatches an action toggleDrawer. This action stores a value in store that represents the state of the drawer isDrawerOpen. Then, how I make the drawer to be opened when the action is dispatched? Also here there is a potential conflict: the drawer itself can be opened and it controls it's open property, so changing isDrawerOpen from drawer would cause an infinite loop...Gilligan
still there is code in connect to respond to all changes and execute at least some code before comparing the previous and next return of mapStateToProps, thereby affecting performance unnecessarily. I just wonder if we could detect the names of keys desired, eg: function mapStateToProps( ({someKey, anotherReducerKey}) => ... and only subscribed to those. In javascript you can get the names of arguments--so the question is can you get the names of destructured keys of those arguments.Dipole
UPDATE: here's how you can get argument names: Here's how to get argument names: #1008481 ...and I simply did func.toString() to see if I could get destructured object parameters, and you can! So we could re-write that function to get the state keys you want to subscribe to. Then we just need to implement a store.subscribeByKey() function.Dipole
2 questions follow though: 1) how can we do this in a cross-browser compatible way, as not all browsers natively support destructuring like the chrome console, and 2) if this is in fact faster than the connect running your mapStateToProps (and mapDispatchToProps and mergeProps) functions. That will depend to some degree on your application, not just the connect function. That said, I think we only need to run it once at startup to get the keys you are watching, since I think it's assumed that connect function calls can be statically analyzed, i.e. can't be changed at runtime.Dipole
what is the purpose of export default connect.. in this specific example? why would you export it?Worthwhile
What I meant was, what if your app if flat and not split into files where you input and export things. then how to you connect the store to each of the components so they'll have access to it?Worthwhile
S
27

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.

Skeie answered 25/3, 2016 at 5:17 Comment(1)
Solid Answer. Just want to add that redux-watch, and redux-subscribe are both listed in the Redux docs for Store recommendations redux.js.org/introduction/ecosystem#store .Moslemism
S
5

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.

*/
Sombrous answered 4/3, 2019 at 13:33 Comment(0)
K
4

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.

Kush answered 10/6, 2017 at 12:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.