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

Accessing the store without @connect-ing? #108

Closed
jamiewinder opened this issue Sep 15, 2015 · 29 comments
Closed

Accessing the store without @connect-ing? #108

jamiewinder opened this issue Sep 15, 2015 · 29 comments
Labels

Comments

@jamiewinder
Copy link

Is there currently a way for a component to access the root Provider store without being connect-ed to it? It sounds like it might be a bit of an anti-pattern, but I'm not sure. I've a couple of circumstances where I can see it coming in handy (dumb components which can still query related data from prop).

@gaearon
Copy link
Contributor

gaearon commented Sep 15, 2015

What specifically is your use case? If you connect() without specifying parameters, you'll get just dispatch injected in props. Do you need something other than dispatch()?

@jamiewinder
Copy link
Author

Basic example: I have a component for showing a 'Task':

<TaskItem task={taskData} />

But now let's say my tasks have an assigned user that I want to show. In my case, I have a 'userId' property on the task, but without access to the store I need this passed into the component as a prop. This gets more and more complex with nesting. For example, if this is used in a TaskList component, then the TaskList needs be updated to have access to both the Task and Users of my store so I can pass the required props. If my user belongs to a group, and I want to show that in TaskItem... well, you see how it goes!

If I had access to the store, I could do a lookup in the TaskItem and not have to rely on my parent component to resolve the related data.

Thanks

@gnoff
Copy link
Contributor

gnoff commented Sep 15, 2015

@jamiewinder this might not work with you but consider possibly using selectors (consider faassen/reselect)

You taskData will exist in your store normalized but when you connect your task list or whatever use selectors to compose the userData with the taskData into a denormalized unit for consumption by your TaskItem component.

This lets your dumb components be truly dumb, they get exactly and only what they need and they don't have to know how to query your user data

@jamiewinder
Copy link
Author

Thanks @gnoff - it looks like it might help, though if I have my dumb components @connect-ed then doesn't that make smart by definition? Not that it's necessarily a problem, but I imagine having thousands of store-connected components (i.e. my lists and their items) will have a performance impact, I'd guess? I'll have a look into it all the same. Thanks again.

@ghost
Copy link

ghost commented Sep 15, 2015

reduxjs/redux#419 might be of interest to you

@gnoff
Copy link
Contributor

gnoff commented Sep 15, 2015

well i was actually meaning that your TaskList or something higher might connect and it would receive the denormalized task data but you can really do it any way. The thing I think is important here is just that your TaskItem has an API (which includes user info) so it's preferable to satisfy that API externally via props than have to know about redux and query for user state within TaskItem.

@gnoff gnoff added the question label Sep 16, 2015
@gnoff
Copy link
Contributor

gnoff commented Sep 16, 2015

closing since I think this isn't actionable. tagged as question

@gnoff gnoff closed this as completed Sep 16, 2015
@gaearon
Copy link
Contributor

gaearon commented Sep 16, 2015

If you really really need store, grab it from context like connect() does it.

class MyComponent {
  someMethod() {
    doSomethingWith(this.context.store);
  }
}

MyComponent.contextTypes = {
  store: React.PropTypes.object.isRequired
};

@bmueller-sykes
Copy link

Hi,

I'm struggling with the same issue, actually. It's entirely possible that in the long run, reading the store directly is the "wrong" way of going about things, but for now, it's what I need--or would like, at any rate.

My problem is that this.context doesn't appear to exist.

My index.js file looks like this:

const store = configureStore();
React.render(
<Provider store={store}>
    { () => <App /> }
</Provider>,
document.getElementById('root')
);

...and then within App.js' constructor function, if I do this:

constructor(props) {
    super(props);
    console.log("context", this.context);
}

...this.context is undefined. If one of you fine folk would kindly point out the error of my ways, I would be most appreciative.

@gaearon
Copy link
Contributor

gaearon commented Sep 22, 2015

@bmueller-sykes

Context is opt-in; you have to specify contextTypes on the component to get it.
See the above comment for an example.

@gaearon
Copy link
Contributor

gaearon commented Sep 22, 2015

@bmueller-sykes

Oh, also, if you want to access this.context in the constructor, you need to change your super call:

constructor(props, context) {
    super(props, context);
    console.log("context", this.context);
}

You can skip this if you're only reading it later because React will put context onto your instance after the constructor has run.

@bmueller-sykes
Copy link

bless you. (-;

@rodryquintero
Copy link

Just a note about using MyComponent.contextTypes along with other components that might use context (react-router). The declaration of contextTypes on the component overwrites the context types of other parent components.

In my case, using contextTypes on a container component that uses react-router as well I did the following (assumes use of lodash).

MyComponent.contextTypes = _.extend(MyComponent.contextTypes, {
 store: PropTypes.object.isRequired
});

@gaearon
Copy link
Contributor

gaearon commented Feb 8, 2016

The declaration of contextTypes on the component overwrites the context types of other parent components.

What do you mean by “parent components”? It will not overwrite contextTypes of parent components. Parent components are components that contain your component in their render() method.

However, if you use inheritance, yes, you need to be careful. That said React actively discourages using inheritance for component classes. Just don’t use it.

@rodryquintero
Copy link

Sorry for the mixup. In my particular case, trying to grant access to the redux "store" in a component using contextTypes replaced the context variables set by react-router (route, history, etc..). I was getting "undefined" errors until I realized what was happening.

The main app.js looks more or less like this.

<Provider store={store}>
 <Router>
   <Route path="/" component={component}/>
 </Router>
</Provider>

Is there a way to avoid replacing other components context?

@gaearon
Copy link
Contributor

gaearon commented Feb 10, 2016

Sorry for the mixup. In my particular case, trying to grant access to the redux "store" in a component using contextTypes replaced the context variables set by react-router (route, history, etc..). I was getting "undefined" errors until I realized what was happening.

Please show the full code. It is impossible to say what exactly was wrong without seeing where and how you applied contextTypes.

@rodryquintero
Copy link

Ok, here is the code. I am just pasting the relevant parts (ommitted lots of import calls). Thanks for looking into this @gaearon

app.js (look at the OrderContainer component)

import React from 'react';
// import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createHistory } from 'history';

// do not start app until cache-login is resolved
login().then((response) => {


  // perform login in intervals to mantain session
  loginInterval();

  const store = createStore();

  // create history object for "onEnter" hooks
  const history = createHistory();

  function onUpdate() {
    store.dispatch(navigate());
  }

  const NoRoute = (props) =>{
    window.location = "#auth/orders/list";
    return <span/>;
  };


  ReactDOM.render(<div>
    <div id="redux-devtools container">
      <Provider store={store}>
        <Router onUpdate={onUpdate}>
          <Route path="/" component={App}>
            <IndexRoute component={NoRoute}/>
            <Route path="auth" onEnter={checkAuthRoute}>

              {/* DASHBOARD */}
              <Route path="dashboard">
                <Route path="main" component={DashboardMain}/>
              </Route>

              {/* ORDERS */}
              <Route path="orders">
                <Route path="list" component={OrdersContainer}/>
                <Route path="form(/:id)" component={OrderContainer}/>
                <Route path="new" component={OrderContainer}>
                  <Route path="patient(/:id)" component={PatientContainer}/>
                </Route>
                <Route path="trace/:tableRelated" component={TraceAuditContainer}/>
              </Route>
              {/* PATIENTS */}
              <Route path="patients">
                <Route path="list" component={PatientsContainer}>
                  <Route path="new(/:id)" component={PatientContainer}/>
                </Route>
              </Route>
              {/* APPOINTMENTS */}
              <Route path="appointments">
                <Route path="calendar" component={ApptsCalendarContainer}>
                  <Route path="date/:date" component={ApptsCalendarModalContainer}/>
                </Route>
                <Route path="collection" component={ApptsSampleCollectContainer}/>
              </Route>

              {/* CONF */}
              <Route path="conf" onEnter={checkAdmin} history={history}>
                <Route path="general" component={ConfGeneral}/>
                <Route path="calendar" component={ConfCalendar}/>
                <Route path="demo_order" component={ConfOrderDemographics}/>
                <Route path="demo_patient" component={ConfPatientDemographics}/>
                <Route path="test_prereqs" component={ConfTestPreReqs}/>
                <Route path="bcprinters" component={ConfBCPrinters}/>
              </Route>
            </Route>
            <Route path="login" component={LoginContainer} />
          </Route>
        </Router>
      </Provider></div>
      {/*}<DebugPanel top right bottom>
      <DevTools store={store} monitor={LogMonitor} />
    </DebugPanel>*/}
  </div>,
  document.getElementById('root')
  );
})
.catch(function (error) {
    throw(error);
}).done();

order-container.jsx

import React, { PropTypes } from 'react';
import { createHistory, useBeforeUnload } from 'history';
import { connect } from 'react-redux';
...

import ConfirmNavMixin from '../util/mixin-confirm-nav';

const OrderContainer = React.createClass({

  mixins: [ Lifecycle, ConfirmNavMixin ],

  getInitialState: function() {
    return {
      showPrintModal: false
    };
  },

  getCustomFieldValues(fields, props) {
    return fields.map(field => {
      _.each(props, (value, key) => {
        if (field.label == key) {
          field.value = value;
        }
      });

      return field;
    });
  },

  fetchOrder(id) {

    // lock record
    lock_record('CITAS.wsORDERS.cls',20000, id).then((obj) => {
      this.lockObj = obj;

      // if lock was not succesfull, clear interval
      if (!this.lockObj.locked) {
        clearInterval(this.lockObj.intervalToken);
      }
    });

    this.props.dispatch(get_order(id));
  },

  componentWillUnmount() {
    // clear lock interval
    if (typeof(this.lockObj) !== 'undefined') {
      clearInterval(this.lockObj.intervalToken);
    }

    this.props.dispatch(initial_order_state());
  },

  componentWillMount() {

    const { params, dispatch } = this.props;

    dispatch(get_tests());

    // ** get appointments for current month and year
    const month = moment().format("MM");
    const year = moment().format("YYYY");

    dispatch(get_appts_monyear(month, year));

    if (params.id) {
      this.fetchOrder(params.id);
    }
    else {
      dispatch(initial_order_state_noloading());
    }
  },

  componentWillReceiveProps(nextProps) {
    if (nextProps._showPatientModal) {
      nextProps.dispatch(not_show_patientmodal());
      nextProps.history.pushState(null, 'auth/orders/new/patient/' + nextProps.patientid);
    }
  },

  onBlurOrderID(orderID) {
    const { confOrderNo, printedBarcode, dispatch, _lastOrderID } = this.props;

    const compOrderID = get_orderno(orderID, confOrderNo);

    if (compOrderID != _lastOrderID) {
      dispatch(get_order(compOrderID));
    }
  },

  handleSubmit(data) {

    const { dispatch, _recordPulled, _printBarcodeAuto, printedBarcode,
       _printWorkOrderAuto, activeBCPrinter, orderID } = this.props;

    const tests = _.map(this.props.tests, (test) => {
      return {TestID: test.TestID};
    });

    delete data.test_listbox;
    data.tests = JSON.stringify(tests);
    const bcPrintSettings = { activeBCPrinter, _printBarcodeAuto, printedBarcode };

    dispatch(save_order(data, _recordPulled, bcPrintSettings));

    // automatic  work order printing
    if (_printWorkOrderAuto) {
      this.onClickPrint();
    }
  },

  handleClickNew() {

    const { dispatch, history } = this.props;
    const url = "/auth/orders/new";
    history.pushState("",url);
    dispatch(initial_order_state_noloading());
  },

  onClickPrint() {

    const { conf, firstName, lastName, dob, patientid, tests, orderID, registerDate, sex,
      appointmentDate, OrderField1, OrderField2, OrderField3, OrderField4, intl } = this.props;

    const data = { firstName, lastName, dob, patientid, tests, orderID, registerDate, sex,
      appointmentDate, OrderField1, OrderField2, OrderField3, OrderField4 };


    data.tests = _.chain(tests)
    .filter((test) => {
      return test.Profile == "0";
    })
    .groupBy((test) => {
        var section = (test.SectionName) ? test.SectionName : intl.messages.other;
        return section;
    })
    .value();

    // save order data in localStorage
    localStorage.setItem("CITAS.printOrder",JSON.stringify(data));

    const url = 'print.html?order-template';
    window.open(url,'_blank');
  },

  handleBlurPatientID(value, name) {
    const { dispatch } = this.props;

    dispatch(set_order_value(name,value));
    dispatch(get_order_patient(value));
  },

  handleClickPrintBarcode() {
    const { dispatch, activeBCPrinter, orderID } = this.props;

    dispatch(print_barcode(orderID, activeBCPrinter));
  },

  render () {

    const { browser, locale, confOrderNo, orderID, registerDate, appointmentDate, firstName, lastName,
      tests, testCatalog, dob, patientid, age, sex, _recordPulled, dispatch, history, customFields,
      selectedTest, disabledDates, calendarConf, appointments, sentToHost, priority, OrderField1,
      OrderField2, OrderField3, OrderField4, OrderField5, OrderField6, OrderField7, OrderField8,
      OrderField9, OrderField10, activeBCPrinter, _printBarcodeAuto, _printWorkOrderAuto, _showLoading } = this.props;

    // compute order number according to configuration
    const compOrderID = (_recordPulled) ? orderID : get_orderno(orderID, confOrderNo);

    const customFieldAndValues = this.getCustomFieldValues(customFields, this.props);

    return (<div ref="div">
            <Loader isLoading={_showLoading}>
              <OrderForm
                ref="order_container"
                confOrderNo={confOrderNo}
                _printWorkOrderAuto={_printWorkOrderAuto}
                _printBarcodeAuto={_printBarcodeAuto}
                onToggleConfItem={(item) => dispatch(toggle_conf(item))}
                calendarConf={calendarConf}
                appointments={appointments}
                browser={browser}
                history={history}
                locale={locale}
                orderID={compOrderID}
                patientid={patientid}
                registerDate={registerDate}
                priority={priority}
                sentToHost={sentToHost}
                appointmentDate={appointmentDate}
                lastName={lastName}
                firstName={firstName}
                customFields={customFieldAndValues}
                sex={sex}
                tests={tests}
                dob={dob}
                age={age}
                onViewDateChange={(date,moment,view) => dispatch(get_appts_monyear(moment.format("MM"),moment.format("YYYY")))}
                onChangeValue={(name,value) => dispatch(set_order_value(name,value))}
                onClickNew={() => this.handleClickNew()}
                selectedTest={selectedTest}
                testCatalog={testCatalog}
                onSubmit={this.handleSubmit}
                onBlurOrderID={this.onBlurOrderID}
                onBlurPatientID={this.handleBlurPatientID}
                onClickPrint={this.onClickPrint}
                onClickPrintBarcode={this.handleClickPrintBarcode}
                _recordPulled={_recordPulled}
              />
            </Loader>
              {this.props.children}
            </div>);
  }
});

OrderContainer.contextTypes = _.extend(OrderContainer.contextTypes, {
 store: PropTypes.object.isRequired
});

function select(state) {

  const confOrderNo = {
    digits: state.conf.digits,
    prefix: state.conf.prefix,
    suffix: state.conf.suffix,
    prefixType: state.conf.prefixType,
    suffixType: state.conf.suffixType
  };

  confOrderNo.totalDigits = calc_orderdigits(confOrderNo);

  return {
    _hasChanged: diff_state(state.order),
    activeBCPrinter: state.bcprint_modal.activeBCPrinter,
    appointments: state.appointments.list,
    calendarConf: state.conf.calendarConf,
    disabledDates: state.appointments.disabledDates,
    browser: state.conf.browser,
    conf: state.conf,
    locale: state.language.lang,
    sentToHost: state.order.sentToHost,
    firstName: state.order.firstName || "",
    lastName: state.order.lastName || "",
    printedBarcode: state.order.printedBarcode,
    registerDate: state.order.registerDate,
    priority: state.order.priority,
    dob: state.order.dob,
    tests: state.order.tests,
    age: calc_age(state.order.dob),
    sex: state.order.sex,
    patientid: state.order.patientid,
    orderID: state.order.orderID,
    appointmentDate: state.order.appointmentDate,
    customFields: state.conf_demographics.customOrderFields,
    OrderField1: state.order.OrderField1,
    OrderField2: state.order.OrderField2,
    OrderField3: state.order.OrderField3,
    OrderField4: state.order.OrderField4,
    OrderField5: state.order.OrderField5,
    OrderField6: state.order.OrderField6,
    OrderField7: state.order.OrderField7,
    OrderField8: state.order.OrderField8,
    OrderField9: state.order.OrderField9,
    OrderField10: state.order.OrderField10,
    confOrderNo: confOrderNo,
    _lastOrderID: state.order._lastOrderID,
    _showLoading: state.order._showLoading,
    _recordPulled: state.order._recordPulled,
    _error: state.order._error,
    _showPatientModal: state.order._showPatientModal,
    _printBarcodeAuto: state.order._printBarcodeAuto,
    _printWorkOrderAuto: state.order._printWorkOrderAuto,
    testCatalog: state.tests.testCatalog,
    selectedTest: state.tests.selectedTest
  };
}

export default injectIntl(connect(select)(OrderContainer));

Feel free to critique anything that you see wrong/unusual :)

@gaearon
Copy link
Contributor

gaearon commented Feb 10, 2016

Why are you doing this?

OrderContainer.contextTypes = _.extend(OrderContainer.contextTypes, {
 store: PropTypes.object.isRequired
});

connect() generates a new component that wraps your component and already has contextTypes specified for you. You never need to specify them explicitly when you use connect().

@rodryquintero
Copy link

Thanks for the quick reply @gaearon. At the time I posted my comment I needed access to getState() to tell wether there were any changes in the component state and thus prevent/allow navigating away from it.

The code I posted has been refactored and I no longer need the access state within the component (I check if there are changes to be saved in the reducer. So you are right, I don't need access the store from context.

connect() generates a new component that wraps your component and already has contextTypes specified for you. You never need to specify them explicitly when you use connect().

The weird thing is, I don't have access to the store in a "connected" component. Here is the context for the OrderContainer component.

image

I refactored the code and it is now much cleaner. I use the state from the select function to check for differences. But if you ever need to access the store within the component, at least in my case, I was not able to.

@gaearon
Copy link
Contributor

gaearon commented Feb 10, 2016

We don’t provide access to it on purpose because people will misuse store.getState() in render() and wonder why it does not update. You are right that for such cases you need to specify contextTypes manually.

The reason for the overwriting in your case is that you are literally overwriting them. When you use mixins and createClass(), it intelligently merges contextTypes. However that requires you to specify them on the field on the object you pass to createClass(). If you move them there, the merging will work correctly. (However, this is irrelevant for ES6 classes which have no merging behavior and no mixins.)

@rodryquintero
Copy link

Thanks Dan for all the info provided. It helps to understand redux better.

@cristianocca
Copy link

Hello, react-redux 6.0 removed the possibility to use the legacy context to access the store.

How would we do it now? It's ok that we shouldn't be using it but when you have lots of tiny components that need to access the store but not to re-render based on it, connecting is overkill and slows down terribly.

@markerikson
Copy link
Contributor

@cristianocca : you might want to read the discussions in #1126 .

What is your use case? How and why are you trying to access the store separately? What kind of performance issues are you seeing?

@cristianocca
Copy link

@markerikson : I have some components (such as buttons, modals, menus, pretty much anything clickable) that need to access some data from the store but do not need to be re-rendered every time the store changes, nor going through any of the overhead added by connect(). These components just need to know the values from the store when interacted with (such as clicking). Also, I know that perhaps these components should not rely on the store at all but instead receive the props from somewhere else, but that's another story.

Not only this, updating to version 6 also broke components that would render outside of the main component. An example of this is the react material-ui Dialog component, which renders its content as a top level component. This change made it impossible to have connected components that render inside such Dialogs because they don't have access to the store at all now.

I'm not criticizing the change, I actually like that you guys are keeping up with react's recommended implementation, but I wish this wouldn't be such of a breaking change and a migration / work around path was documented in the same changelog.

@markerikson
Copy link
Contributor

markerikson commented Dec 27, 2018

@cristianocca : the migration should just have been updating the version of React-Redux you're using, with the couple exceptions that were documented in the release notes.

If you're having further problems, please file a separate issue to discuss those. I'm particularly confused by your description of Material-UI dialogs.

If you only need to access some data from the store when the action is being dispatched, I'd say that's a perfect use case for a thunk:

function doSomethingWhenClicked(someId) {
    return (dispatch, getState) => {
        const state = store.getState();
        const item = state.items[id];
        dispatch({type : "SOMETHING_HAPPENED", payload : {item}});
    }
}

That way your component just calls this.props.doSomethingWhenClicked(this.props.someId) or similar.

@cristianocca
Copy link

@markerikson : I understood the exceptions, but I failed to find a way to keep the previous behavior with the new version (and hence my question here).

You are right about using a thunk, but again, literally all I need in the componet is a simple value check from the store. Do I really need to go through all the overhead of making an action, connecting, and dispatching just to achieve something as simple as doing this.context.store.getState().someValue in a component check? The previous way of obtaining the state from the context was really good for these cases, and had no impact on performance. I'm talking about a component that's probably instantiated over a 100 times.

About the Material-ui Dialog component issue (sorry if this goes offtopic). The 0.x version implementation simply creates a component for the modal under the react's root node, and hence it is not inside the redux node sub-tree, which means it won't have access to the store and any component rendered in it will fail to connect. This wasn't an issue with version < 6 and is likely related to the same context change.

@markerikson
Copy link
Contributor

@cristianocca : the discussion in #1126 shows how to access the store state yourself if you really need it. That said, please note that is not considered part of the official API.

Problems with MaterialUI should probably be filed on that repo. That said, if there's use cases the current React-Redux API doesn't cover, please file a separate issue with your concerns and use cases, and we can discuss things there.

@markerikson
Copy link
Contributor

@cristianocca : we just added a docs page that shows the techniques discussed in #1126:

https://react-redux.js.org/using-react-redux/accessing-store#using-reactreduxcontext-directly

@cristianocca
Copy link

@markerikson : That's great, thanks!

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

No branches or pull requests

7 participants