Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't get store from context #193

Closed
marcuswhit opened this issue Nov 19, 2015 · 22 comments
Closed

Can't get store from context #193

marcuswhit opened this issue Nov 19, 2015 · 22 comments

Comments

@marcuswhit
Copy link

My top level component is like so:

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

And my App component is like so:

@connect(state => state)
export default class App extends React.Component {
    render() {
        return (
            <div className={cx('container')}>
                <Workspace />
                <Blotter />
            </div>
        );
    }
}

I'm not using React Router, and I've ensured that there is only one React instance loaded.

If I break at the top of App.render, this.context has no store on it. However, this._reactInternalInstance._context does have the store attached. Obviously I can grab this, but I'm guessing I shouldn't really use that one.

Why is there no store on this.context? I want to subscribe to certain state changes.

Thanks.

@m19
Copy link

m19 commented Nov 20, 2015

I actually have the same problem. I do use react router.

"react": "^0.14.2",
"react-router": "1.0.0-beta3",
"react-redux": "^4.0.0",
"redux": "^3.0.4",

I also noticed the exact same thing that this._reactInternalInstance._context of the Dashboard component does have a store, but this.context has not.

    <Provider store={store}>
      <Router history={history}>
        <Route path='/' component={Dashboard} />
      </Router>
    </Provider>

@gabrieledarrigo
Copy link

Having same issue, both with and without React Router.
Anyone solved this?

ps: I've ensured that there is only one React instance loaded
I Didn't receive the store even on props, and I have the exact dependecies of @m19:

"react": "^0.14.2",
"react-router": "1.0.0-beta3",
"react-redux": "^4.0.0",
"redux": "^3.0.4",

@0rvar
Copy link

0rvar commented Nov 21, 2015

You are not supposed to access this.context.store anyway. You use connect to pick the part of the state you want.

React only populates the context if you specify contextTypes in your component. You will however notice that the props in your component is populated from the store, as you specified in your mapStateToProps function.

If your want to subscribe to state changes, make sure you set contextTypes. Or, even better, handle the subscriptions on the root level (where you create the store).
However, I can't help but think you really don't need to subscribe to changes, as the react tree is re-rendered each time the state changes anyway.

@gabrieledarrigo
Copy link

Hi @awestroke;
Yep, props are populated with the data coming from the store and with the dispatch function.

So, Do You confirm that the store object is not injected into props?

About the subscribing, isn't store.subscribe purpose just to be used to listen and retrieve a piace of state from store?

@marcuswhit
Copy link
Author

@awestroke,

I am using connect to pick the parts of the state that I want. But that doesn't let me subscribe to state changes. I realise react will re render when the connected parts change, but I think it is a perfectly valid use case to observe changes to a specific property on the store, and trigger an action based on this - not all state changes need to trigger a re render. If the observed property can be updated via various means, this is a neat way of responding to that.

Yes I could do this where I setup my store, but then I end up with irrelevant logic together in that file.

I'm using redux-rx's observableFromStore to do this, which requires the store passed in.

I'm pretty sure I've already tried to set contextTypes without success, but can try again tomorrow.

@marcuswhit
Copy link
Author

@awestroke was correct, I just needed to correctly specify contextTypes in my component, and the store was then added as expected:

App.contextTypes = { store: React.PropTypes.object };

@gabrieledarrigo
Copy link

@awestroke @marcuswhit So it is confirmed that store is not passed in with props?

@gaearon
Copy link
Contributor

gaearon commented Nov 24, 2015

There are two possible approaches here.

Use connect() and don’t worry about the context

This is the approach we suggest. Use connect(), let it subscribe to the store, and don’t worry about the React context API. There are limitations: indeed, store itself is not passed down as a prop, but subscribing to it inside a connect()ed component is an anti-pattern anyway. Let connect() take care of subscriptions. If you want to cause a side effect when store state changes, do this in componentWillReceiveProps of the connect()ed component.

Specify contextTypes for store and access context directly

If you really want to grab store from context yourself (my advice: don’t do this unless you have a very good reason!) you need to specify contextTypes. Context is opt-in in React, and you won’t receive this.context if you don’t specify contextTypes. My free Egghead lesson on implementing <Provider> with context covers that.

@marcuswhit
Copy link
Author

Thanks @gaearon

I get that you should only use context.store with really good reason - I'm just wondering if my use case falls into that category. I'm using redux-rx's rx-middleware and observableFromStore, and have something like the following:

observableFromStore(store)
    .map(state => state.tiles[prop.tileId])
    .distinctUntilChanged(tile => tile.ccyPair)
    .flatMapLatest(tile => svc.getPriceStream(tile.ccyPair))

Essentially, I watch for any changes to the ccyPair on a tile object, and then subscribe to the price stream each time. The ccyPair could be changed by the user or programatically in a reducer, so putting this logic in a component handler doesn't quite fit. I'm defining this as a payload on an action, which then gets handled by rx-middleware to push the notifications through to my reducers.

Is this a valid use case for observing the store? If I just use connect() and watch for ccyPair changes in componentWillReceiveProps, it seems like much more work - I have to cache the last ccyPair to ensure it's changed, then if it has I should clean up the last priceStream subscription, then subscribe to a new one. It seems much cleaner all being contained within a single observable.

@gaearon
Copy link
Contributor

gaearon commented Nov 24, 2015

@marcuswhit Can you show your whole component? I'm confused why it needs both connect() to subscribe, and a store itself to form that stream. Maybe having just the store is enough?

@marcuswhit
Copy link
Author

Sure. I think I need both the connect() and store:

  • I need the store for my observable subscription - I could detect ccyPair changes in componentWillReceiveProps, but this would break the nice clean Rx flow - I'd have to manually cache the last ccyPair selected, dispose any old price stream, and subscribe to the new one. At the very least I want an event telling me the ccyPair on the state has changed, which I can convert into an Observable.
  • The connect() is for the other parts of the component. I'm pretty sure it needs to be a smart component as it'll have a lot of stuff going on in it's subtree - more than most entire apps - which aren't the concern of it's container.

.

// Component 
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';

import { propTypes, contextTypes } from 'common/utils/ReactDecorators';
import * as Actions from 'common/ActionTypes';
import { initTile, observePriceStream, changeCcy, closeTile } from './TileActions'

@connect(createSelector(
    (state, props) => state.tiles[props.tileId],
    state => state.refData.ccyPairs,
    (tile, ccyPairs) => ({ tile, ccyPairs })
))
@propTypes({ tileId: React.PropTypes.number.isRequired })
@contextTypes({ store: React.PropTypes.object })
export default class TradingTile extends React.Component {

    componentWillMount() {
        let { dispatch, tileId } = this.props;

        dispatch(initTile(tileId));

        this.priceSubscription = dispatch(observePriceStream(tileId, this.context.store))
            .subscribe();
    }

    componentWillUnmount() {
        if (this.priceSubscription)
            this.priceSubscription.dispose();
    }

    ccyChanged(evt) {
        this.props.dispatch(changeCcy(this.props.tileId, evt.target.value));
    }

    removeTile() {
        this.props.dispatch(closeTile(this.props.tileId));
    }

    render() {
        const { tile, ccyPairs } = this.props;

        return (
            <div>
                <header>
                        <select value={tile.ccyPair}
                                onChange={e => this.ccyChanged(e)}>
                            {ccyPairs.map(pair =>
                                <option key={pair} value={pair}>{pair}</option>
                            )}
                        </select>

                    <button onClick={() => this.removeTile()}>x</button>
                </header>
                <div>
                    // body here
                </div>
            </div>
        );
    }
}


// Actions
import { observableFromStore } from 'redux-rx';

import * as Actions from 'common/ActionTypes';
import TradingService from 'services/TradingService';

export function initTile(tileId) {
    return { type: Actions.TILE_INIT, tileId };
}

export function closeTile(tileId) {
    return { type: Actions.TILE_CLOSE, tileId };
}

export function changeCcy(tileId, newCcy) {
    return { type: Actions.CCY_CHANGE, tileId, newCcy };
}

var svc = new TradingService();

export function observePriceStream(tileId, store) {
    return {
        type: Actions.PRICE_UPDATE,
        payload: observableFromStore(store)
            .map(s => s.tiles[tileId])
            .filter(tile => tile)
            .distinctUntilChanged(tile => tile.ccyPair)
            .flatMapLatest(tile => svc.getPriceStream(tile.ccyPair))
            .map(tick => ({ ...tick, tileId }))
    };
}

@marcuswhit
Copy link
Author

Any thoughts on the above @gaearon?

@gaearon
Copy link
Contributor

gaearon commented Nov 29, 2015

Seems OK to me. Yes, getting store from context explicitly seems like best option here.

@undefinedplayer
Copy link

@gaearon Is there any better way to use store.subscribe other than getting store from context ('connect' doesn't help)? I'm new to Redux but I feel it's quite weird when I try to follow http://redux.js.org/docs/api/Store.html doc to do subscribe but don't know how to get the store object. Searched a long time on the internet and finally get here. I feel there should be a more apparent place to let people know the normal way to get store for store.subscribe().

@markerikson
Copy link
Contributor

markerikson commented Jan 23, 2017

@shlinx : Dan isn't actively maintaining Redux any more.

As Dan said earlier in the thread, 99.9% of the time, you shouldn't be trying to access the store directly in your React component. Use the connect function from the React-Redux library, and let it manage the subscriptions for you. This is described in the Redux docs at http://redux.js.org/docs/basics/UsageWithReact.html .

There's also many other Redux tutorials listed at https://github.com/markerikson/react-redux-links/blob/master/redux-tutorials.md as well.

@undefinedplayer
Copy link

@markerikson Thanks.

@Damnum
Copy link

Damnum commented Jan 27, 2017

@markerikson What if I want to pass state but don't want to subscribe to store updates? Whenever I pass mapStateToProps the component will get subscribed, or is there another way around it?

@jimbolla
Copy link
Contributor

@Damnum connect(..., ..., ..., { shouldHandleStateChanges: false })

@0rvar
Copy link

0rvar commented Jan 27, 2017

@Damnum: could you share a scenario where you do not want to subscribe to state changes?

@Damnum
Copy link

Damnum commented Jan 28, 2017

Sorry guys, after some investigation I realized that my question was based on a misunderstanding how/when components are rendered in React. So I solved it differently now. Thanks anyway!

@rafalolszewski94
Copy link

@awestroke was correct, I just needed to correctly specify contextTypes in my component, and the store was then added as expected:

App.contextTypes = { store: React.PropTypes.object };

How do you import PropTypes, it's undefined when using it as you mentioned.

@0rvar
Copy link

0rvar commented Feb 22, 2019

How do you import PropTypes, it's undefined when using it as you mentioned.

Use the prop-types package.

import PropTypes from 'prop-types'

@reduxjs reduxjs deleted a comment from andrew-aladev Oct 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants