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

How can we access redux in componentDidMount with react-router 1.0? #239

Closed
happypoulp opened this issue Jul 9, 2015 · 16 comments
Closed

Comments

@happypoulp
Copy link

Hi,

I feel that this question is maybe half a redux question for the best practices of how to use redux and half a react-router question for the technical part regarding how to pass props to child components.

I use redux 0.12 and react-router 1.0 beta.

I am having a hard time figuring how to access redux and to read in its store from componentDidMount.

Here is an extract of the code I use:

./routes.jsx

export default (
  <Route name="app" component={require('./app.jsx')}>
    <Route path="/" name="home" component={require('./home-page-container.jsx')}/>
  </Route>
)

./client.js

import * as stores from './stores.js'
import { history } from 'react-router/lib/BrowserHistory'
import routes from './routes.jsx'
import { Provider } from 'redux/react'

var redux = createRedux(stores, window.__INITIAL_STATE__);

React.render(
  <Provider redux={redux}>
    { () => <Router history={history} children={routes} /> }
  </Provider>,
  document.getElementById('app-wrapper')
)

./app.jsx

export default class App extends React.Component {
  render() {
    return (
        <div>
            { this.props.children }
        </div>
    );
  }
}

./home-page-container.js

import HomePage from './home-page.jsx';
import  * as HomeActions from './home-actions.js';
import { bindActionCreators } from 'redux';
import { Connector } from 'redux/react';

function select(state) {
  return { list: state.home.list };
}

export default class HomePageContainer extends React.Component {
  static fetchData(redux) {
    var homeActions = bindActionCreators(HomeActions, redux.dispatch);
    return Promise.all([
      homeActions.loadList()
    ]);
  }

  componentDidMount() {
    /*
        I'd like to access redux here and read in its store (synchronously) to see if 
        I need to call an action to fetch some data (asynchronously)
        Ex:
        if (!redux.getState().home.list) {
          this.constructor.fetchData(redux);
        }
    */
  }

  render() {
    return (
      <Connector select={select}>
        { ({ list, dispatch }) => <HomePage list={ list } /> }
      </Connector>
    );
  }
}

With react-router 0.13, I could pass redux as a prop to the Handler chosen by the router but with react-router 1.0 there is no such thing as a Handler component to pass props to.

Example

With react-router 0.13 I could do:
React.render(
  <Handler redux={redux} />,
  document.getElementById('app-wrapper')
)

and if Handler == HomePageContainer I could access this.props.redux in HomePageContainer::componentDidMount

But with react-router 1.0 we now have
React.render(
  <Provider redux={redux}>
    { () => <Router history={history} children={routes} /> }
  </Provider>,
  document.getElementById('app-wrapper')
)

and if App::this.props.children == HomePageContainer I don't know how to access redux in HomePageContainer::componentDidMount.

I didn't even figured out how the association Router / Provider is working (I guess I lack some understanding of react router 1.0 itself here).

So here are my questions:

  • How can I pass redux to HomePageContainer?
  • Is it supposed to be automatically done by the redux Provider somehow?
  • How can I tell the router that I want to pass props (like redux) to the component that will passed in App::this.props.children?
  • Am I going all the wrong way about this and shouldn't I be trying to read from redux store to initiate data fetching from componentDidMount (I was doing so with flummox but maybe it's different with redux)?

Sorry for the many questions and thanks for any help!

@emmenko
Copy link
Contributor

emmenko commented Jul 9, 2015

The instance of redux is in the react context, so you can simply access it as:

class Foo extends React.Component {
  static contextTypes = {
    redux: React.PropTypes.object
  }

  componentDidMount () {
    this.context.redux.getState()
  }
}

You might want to look at some of the examples.

@happypoulp
Copy link
Author

Thanks for the quick answer!

I think I saw this in one of the many examples about redux but since I didn't found it the the redux documentation I felt that this was more a hack that the actual good way to go.

Just to make sure I understand correctly, is it the Provider that make the redux instance accessible through react context?

@emmenko
Copy link
Contributor

emmenko commented Jul 10, 2015

Correct, the instance of redux is put into context in the Provider.

@emmenko emmenko closed this as completed Jul 10, 2015
@happypoulp
Copy link
Author

Thanks!

@mewben
Copy link

mewben commented Jul 26, 2015

Is this applicable in v1.0.0-rc? I got undefined in context.redux. :(

@gaearon
Copy link
Contributor

gaearon commented Jul 26, 2015

@mewben

It's called store in 1.0 RC. Use this.context.store.
That said, context is a private API and is not supposed to be used directly.

(@emmenko Better not to suggest this to people :-)

@gaearon
Copy link
Contributor

gaearon commented Jul 26, 2015

Am I going all the wrong way about this and shouldn't I be trying to read from redux store to initiate data fetching from componentDidMount (I was doing so with flummox but maybe it's different with redux)?

Indeed, you're going a slightly wrong way.
There are two better solutions than reading from context directly.

Alternative 1: Use connect decorator instead of Connector.

We're probably going to remove Connector anyway in favor of a more powerful connect-centric API.

The reason you should use connect on your container component is that then you can access this.props.list and this.props.dispatch right from it instead of trying to read the state from a Redux store instance.

Something like:

import HomePage from './home-page.jsx';
import  * as HomeActions from './home-actions.js';
import { bindActionCreators } from 'redux';
import { connect } from 'redux/react';

function select(state) {
  return { list: state.home.list };
}

@connect(select)
export default class HomePageContainer extends React.Component {
  static fetchData(dispatch) {
    var homeActions = bindActionCreators(HomeActions, dispatch);
    return Promise.all([
      homeActions.loadList()
    ]);
  }

  componentDidMount() {
    if (!this.props.list) {
      this.constructor.fetchData(this.props.dispatch);
    }
  }

  render() {
    return <HomePage list={this.props.list} />;
  }
}

or, if you don't want to use decorator syntax,

import HomePage from './home-page.jsx';
import  * as HomeActions from './home-actions.js';
import { bindActionCreators } from 'redux';
import { connect } from 'redux/react';

function select(state) {
  return { list: state.home.list };
}

class HomePageContainer extends React.Component {
  static fetchData(dispatch) {
    var homeActions = bindActionCreators(HomeActions, dispatch);
    return Promise.all([
      homeActions.loadList()
    ]);
  }

  componentDidMount() {
    if (!this.props.list) {
      this.constructor.fetchData(this.props.dispatch);
    }
  }

  render() {
    return <HomePage list={this.props.list} />;
  }
}

export default connect(select)(HomePageContainer);

One Step Further: Smarter action creators + middleware

You can use redux-thunk middleware (it used to be included with 0.12, but in 1.0RC it's a separate package) to write “smarter” action creators that can check the current state and exit early.

For example:

// HomeActions.js

export function loadList() {
  // I don't know how you wrote it—maybe using redux-promise or something
}

export function loadListIfNeeded() {
  // This “return a function” form is supported thanks to redux-thunk
  return (dispatch, getState) => {
    if (getState().home.list) {
      return; // Exit early!
    }

    return dispatch(loadList()); // OK, do that loady thing!
  };
}

Now you can dispatch loadListIfNeeded() and avoid a check inside componentDidMount.

@happypoulp
Copy link
Author

@gaearon thanks a lot for your insights!

Regarding the alternative 1

I actually started using the connect decorator but I had to go back to the Connector instead because I need to access to a params props (written by react-router) to extract the correct data from redux state.

For example:

import HomePage from './home-page.jsx';
import  * as HomeActions from './home-actions.js';
import { bindActionCreators } from 'redux';
import { Connector } from 'redux/react';

function select(state) {
  return { list: state.home.list[this.props.params.listID] };
}

// ...

  render() {
    return (
      <Connector select={select.bind(this)}>
        { ({ list, dispatch }) => <HomePage list={ list } /> }
      </Connector>
    );
  }
}

Is it possible to achieve similar extraction of props using the connect decorator? Isn't the decorator only limited to access class variables and methods (instead of instance variables and methods)?

And I see that you're using this.props.dispatch in componentDidMount but don't I need to access redux from context to get this dispatch method?

Regarding the alternative 2

I am using a promise middleware and I imagine that I could call the thunk middleware before it to achieve what you suggest. I really like the idea of avoiding the test in componentDidMount and I guess I could pass props to the action if I need to take a decision that depends on props. Am I correct?

Again, thanks for the time you took to explain and thanks for redux! :)

@gaearon
Copy link
Contributor

gaearon commented Jul 27, 2015

Is it possible to achieve similar extraction of props using the connect decorator? Isn't the decorator only limited to access class variables and methods (instead of instance variables and methods)?

With connect decorator, you'll get the instance's props as the second parameter to your select function.

@mewben
Copy link

mewben commented Jul 27, 2015

Thanks @gaearon. I chose not to use connect since I'm only after an Action and not load any store like this:

const actions = bindActionCreators(AppActions, this.context.store.dispatch);
actions.login(credentials);

or maybe you have a more beautiful approach?

@gaearon
Copy link
Contributor

gaearon commented Jul 27, 2015

@mewben

Just noting that you don't have to use bindActionCreators if you don't pass the result object down.
You can just call dispatch directly:

this.context.store.dispatch(actions.login(credentials))

@mewben
Copy link

mewben commented Jul 27, 2015

Ahh I see.. Thank you very much.. :)

@happypoulp
Copy link
Author

@gaearon

Thanks, I didn't knew about the connect second parameter. I am still wondering how you get that dispatch you're using in this.constructor.fetchData(this.props.dispatch);. Do I have to add it in a parent container at some point, extracting it from context.redux?

@gaearon
Copy link
Contributor

gaearon commented Jul 27, 2015

@happypoulp That's what @connect decorator injects into your component.

@happypoulp
Copy link
Author

Thanks a lot!

@emmenko
Copy link
Contributor

emmenko commented Jul 27, 2015

Also connect is used to subscribe your component to changes.

But as @gaearon mentioned, the new upcoming connect API will be more powerful.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants