Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing store through context with <Provider> not working in connect() based scenario

Edit: Summary - the initial cause of my question was actually a typo: it wasn't working due to a capital 'G'.

However, the kind answerers addressed not only the typo, but the wrong premise in the approach I was taking - if you too are passing store using Provider and using connect, their answers are relevant to you. I have updated the title of the question to reflect this.


I am trying to follow the instructions in the awesome redux videos , but have come to grief with passing down the store using <Provider> from react-redux.

I have a root component:

export default class Root extends Component {
   render() {

      const { store } = this.props

      return (
         <Provider store={store}>
            <div>
               <ReduxRouter />
               <DevTools />
            </div>
         </Provider>
      )
   }
}

And a presentation component trying to use store:

const ProjectsSummary = (props, {store}) => {
   const state = store.GetState();
   const { projects } = state;

   return (
      <div className="home-projects col-md-10">
          <h3>Projects</h3>
          <ul>
              { projects.map(p => <li key={p.id}>{p.contract.client}</li>) }
          </ul>
      </div>
   )
}

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

class Home extends BasePage {

   render() {
      return (
         <div className="home-page container-fluid">
             {super.render()}
             <HomeLeftBar/>
             <HomePageHeader/>
             <ProjectsSummary/>
         </div>
      )
   }
    }

export default connect()(Home)

I get "Uncaught TypeError: store.GetState is not a function"

The store is coming from here:

import configureStore from './store/configureStore'

const store = configureStore({
   security:{
      jwt: 'mock'  // Mock data supplied below: only loaded when this is set.
   }, 
   projects: [
      {
            // elided for brevity
      }
   ]
})

/**
 * Main application render method that attaches it
 * to the HTML page.
 */
render(
   <Root store={store}/>,
   document.getElementById('app')
)

and is created here:

export default (initialState) => {
   const store = createDevStore(initialState)

   if (module.hot) {
      // Enable Webpack hot module replacement for reducers
      module.hot.accept(['../../common/reducers', '../reducers'], () => {
         const nextRootReducer = require('../../common/reducers')
         const nextBrowserReducers = require('../reducers')
         store.replaceReducer(nextRootReducer(nextBrowserReducers))
      })
   }

   return store
}

function createDevStore(initialState){
   if(initialState && initialState.security && initialState.security.jwt === 'mock')
      return mockCreateStore(rootReducer(browserReducers), initialState)
   else
      return finalCreateStore(rootReducer(browserReducers))
}

const mockCreateStore = compose(
   reduxReactRouter({routes, createHistory}),
   applyMiddleware(createLogger()),
   DevTools.instrument()
    )(createStore)

(Not my code, a framework that supports react native and browser client, which I am starting work in)

What am I missing?


I am copying this from the video - note that AddTodo component is not "wrapped" using connect():

const AddTodo = (props, { store }) => {
  let input;

  return (
    <div>
      <input ref={node => {
        input = node;
      }} />
      <button onClick={() => {
        store.dispatch({
          type: 'ADD_TODO',
          id: nextTodoId++,
          text: input.value
        })
        input.value = '';
      }}>
        Add Todo
      </button>
    </div>
  );
};
AddTodo.contextTypes = {
  store: React.PropTypes.object
};
like image 683
GreenAsJade Avatar asked Dec 02 '22 13:12

GreenAsJade


2 Answers

This answer is correct but I would like to clarify a few things.

You seem to have a lot of confusion around presentational and container components, and the role of connect() there. I suggest you to watch the relevant videos again, and make sure you watch them to the very end.

  1. Indeed, store.GetState() is not a valid method; store.getState() is.
  2. If you use store.getState() manually you have to also use store.subscribe() somewhere so you always get the most recent state. The example AddTodo component you pasted from the video will not work by itself—it only worked in the video because we had a store.subscribe(render) at the very top.
  3. Later videos in the course discuss how re-rendering from the top can get cumbersome, at which point we introduce container components. And later we show that it’s easier to generate container components using connect() than write them by hand—in this case, connect() takes care of subscribing to the store.
  4. Merely wrapping Home in connect() in your case has no effect. connect() generates a container component that subscribes to the store, but it won’t even subscribe to the store if you don’t specify the mapStateToProps argument. Using connect() is an efficient replacement for using store.getState(), store.subscribe() and contextTypes manually. It never makes senses to both use connect() on something and call store.getState() or specify contextTypes.

So, to sum up again:

  • store.getState() and store.subscribe() are low-level APIs. You have to use them together if you decide to use them; one without the other doesn’t make sense.

  • connect() is what takes care of calling getState() and subscribe() and passing the necessary information to child components via props for you. If you use connect() you never need store.getState(), store.subscribe(), or contextTypes. The whole point of connect() is to abstract them away.

The lessons teach you all of these tools to show you that there is no magic. However usually you should not be using store.getState() and store.subscribe() in real apps. You should almost exclusively use connect() unless you have a very specific reason why you access low-level APIs.

I would rewrite your code roughly like this:

// ProjectSummary is a presentational component
// that takes projects as a prop and doesn't care
// where it comes from.
const ProjectsSummary = ({ projects }) => {
  return (
    <div className="home-projects col-md-10">
      <h3>Projects</h3>
      <ul>
        {projects.map(p => <li key={p.id}>{p.contract.client}</li>)}
      </ul>
    </div>
  )
}

// Home by itself is also a presentational component
// that takes projects as a prop. However we will
// wrap it in a container component below using connect().
// Note that I got rid of inheritance: it's an anti-pattern
// in React. Never inherit components; instead, use regular
// composition and pass data as props when necessary.
const Home = ({ projects }) => (
  <div className="home-page container-fluid">
    <BasePage />
    <HomeLeftBar />
    <HomePageHeader />
    <ProjectsSummary projects={projects} />
  </div>
)

// How to calculate props for <Home />
// based on the current state of the store?
const mapStateToProps = (state) => ({
  projects: state.projects
})

// Generate a container component
// that renders <Home /> with props from store.
export default connect(
  mapStateToProps
)(Home)
like image 102
Dan Abramov Avatar answered Dec 15 '22 05:12

Dan Abramov


Update:

store.GetState() is not a method.

store.getState() is a method.


Your usage of connect is a little backwards.

connect takes a function as a first argument, which give you access to the store and the props. What you pass out the other side is the object that your view component will receive as its props.

const MyWidget = (props) => {
  const { title, data } = props;
  return (<div>{title}{data}</div>);
};
const MyWidgetContainer = connect((store, props) => {
  return { title: props.title, data: store.a.b.widget.data };
})(MyWidget);

If I were now to export that, and import it elsewhere in the project as MyWidget (because the outside world doesn't know the difference between the widget and the container), and give it a title, it should just work.

import MyWidget from "./components/my-widget/";

//...
render ( ) {
   return (
     <MyWidget title="My Title" />
   );
}

MyWidget will now have its own separate access to the store.

In your case, you didn't give connect the transformer function you see, you applied it to <Home>, meaning that Home would have access to the store if you used a HomeContainer in your app...

Which is 100% fine, but unless you also wrap your presentation component, your presentation component won't have store access. That's okay too, but it means that Home now needs to pass the component the props that it wants.

// Home

Home {
  render () {
    { a, b, c } = this.props;
    return (
      <MyWidget a={ a } b={ b } c={ c } />
    );
  }
}
container = connect(/* ... */)(Home);
like image 25
Norguard Avatar answered Dec 15 '22 05:12

Norguard