npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

tracker-react-redux

v1.0.0

Published

Use React Redux with Meteor's Tracker

Downloads

3

Readme

Tracker React Redux

Use React Redux with Meteor Tracker. React Native Meteor is also supported.

Installation

npm install --save tracker-react-redux

Usage

The main concept introduced by Tracker React Redux is that of trackers.

Similarly to how you have reducers/ and actions/, you now have trackers/. Each "tracker" is reponsible for returning a handle with a stop method.

If you happen to use the ducks pattern, you can place your related trackers at the bottom of each duck file.

Example:

Note that not all files referenced throughout the example are displayed.

index.js

import React from 'react'
import { render } from 'react-dom'
import { Meteor } from 'meteor/meteor'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, browserHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import * as reducers from '../../ducks'

import App from './containers/App'
import Dashboard from './containers/Dashboard'
import AdminUserList from './containers/WidgetList'
import AdminUserEdit from './containers/WidgetEdit'
import NotFound from './components/NotFound'

reducers.routing = routerReducer

const store = createStore(combineReducers(reducers))
const history = syncHistoryWithStore(browserHistory, store)

const Root = React.createClass({
  render () {
    return (
      <Provider store={store}>
        <Router history={history}>
          <Route path='/' component={App}>
            <IndexRoute component={Dashboard} />
            <Route path='/widgetlist'>
              <IndexRoute component={WidgetList} />
              <Route path='/widgetedit' component={WidgetEdit} />
              <Route path='/widgetedit/:userId' component={WidgetEdit} />
            </Route>
          </Route>
          <Route path='*' component={NotFound} />
        </Router>
      </Provider>
    )
  }
})

Meteor.startup(() => {
  render(React.createElement(Root), document.getElementById('app'))
})

./ducks/index.js

export { default as widgets } from './widgets'

./ducks/widgets.js

import { Meteor } from 'meteor/meteor'
import { Widgets } from './collections'

const SET_WIDGET_LIST = 'app/widgets/SET_WIDGET_LIST'
const SET_WIDGET = 'app/widgets/SET_WIDGET'

// -----------------------------------------------------------------------------

const defaultState = {
  widgetList: null,
  widget: null
}

// reducers

export default function reducer (state = defaultState, action = {}) {
  switch (action.type) {
    case SET_WIDGET_LIST:
      return Object.assign({}, state, { widgetList: action.widgetList })
    case SET_WIDGET:
      return Object.assign({}, state, { widget: action.widget })
    default:
      return state
  }
}

// actions

export function setWidgetList (value) {
  return {
    type: SET_WIDGET_LIST,
    widgetList: value
  }
}

export function setWidget (value) {
  return {
    type: SET_WIDGET,
    widget: value
  }
}

// trackers

export function trackMeteorMemberUsers (dispatch, autorun) {
  let run
  let sub = Meteor.subscribe('widget-list', () => {
    run = autorun(() => {
      let widgetList = Widgets.find().fetch()
      dispatch(setWidgetList(widgetList))
    })
  })

  return {
    stop: () => {
      sub && sub.stop()
      run && run.stop()
    }
  }
}

export function trackWidget (dispatch, autorun, props) {
  let run
  let sub = Meteor.subscribe('widget', props.widgetId, () => {
    run = autorun(() => {
      let widget = props.widgetId && Widgets.findOne({_id: props.widgetId})
      dispatch(setWidget(widget))
    })
  })

  return {
    stop: () => {
      dispatch(setWidget(defaultState.widget))
      sub && sub.stop()
      run && run.stop()
    }
  }
}

./containers/WidgetList.js

import { connect } from 'tracker-react-redux'
import WidgetListComponent from '../components/WidgetList'
import { trackWidgetList } from '../ducks/meteor'

const tracking = (track, props) => {
  track(trackWidgetList)
}

const mapStateToProps = (state) => {
  return {
    widgetList: state.widgets.widgetList || [],
    isLoading: state.widgets.widgetList === null
  }
}

export default connect(tracking, mapStateToProps)(WidgetListComponent)

./components/WidgetList.js

import React from 'react'
import Spinner from 'react-spinner'
import { Link } from 'react-router'
import { _ } from 'meteor/underscore'

const Widgets = React.createClass({
  propTypes: {
    widgetList: React.PropTypes.array.isRequired,
    isLoading: React.PropTypes.bool.isRequired
  },

  shouldComponentUpdate (nextProps, nextState) {
    if (this.props.isLoading !== nextProps.isLoading) return true
    if (!_.isEqual(this.props.widgetList, nextProps.widgetList)) return true
    return false
  },

  render () {
    return (
      <div>
        {this.props.isLoading ? <Spinner /> : (
          <table className='table'>
            <thead>
              <tr>
                <th>Widget Name</th>
                <th width='1'></th>
              </tr>
            </thead>
            <tbody>
              {this.props.widgetList.map((widget) => {
                return (
                  <tr key={widget._id}>
                    <td>{widget.name}</td>
                    <td><Link to={'/widgetedit/' + widget._id}>Edit</Link></td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        )}
        <hr />
        <Link to='/widgetedit'>New Widget</Link>
      </div>
    )
  }
})

export default WidgetList

./containers/WidgetEdit.js

import { connect } from 'tracker-react-redux'
import { Meteor } from 'meteor/meteor'
import WidgetEditComponent from '../components/WidgetEdit'
import { trackWidget, setWidget } from '../ducks/widgets'

const tracking = (track, props) => {
  track(trackWidget, {widgetId: props.params.widgetId})
}

const mapStateToProps = (state) => {
  return {
    widget: state.widgets.widget || {},
    isLoading: state.widgets.widget === null
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    commitWidget: (widgetId, name, callback) => {
      let isCreating = !widgetId

      if (isCreating) {
        Meteor.call('createWidget', name, (err, result) => {
          !err && dispatch(setWidget(result))
          callback(err, result)
        })
      } else {
        Meteor.call('updateWidget', widgetId, name, (err, result) => {
          !err && dispatch(setWidget(result))
          callback(err, result)
        })
      }
    },
    deleteWidget: (widgetId, callback) => {
      Meteor.call('deleteWidget', widgetId, (err, result) => {
        callback(err, result)
      })
    }
  }
}

export default connect(tracking, mapStateToProps, mapDispatchToProps)(WidgetEditComponent)

./components/WidgetEdit.js

import React from 'react'
import Spinner from 'react-spinner'
import { browserHistory } from 'react-router'
import { _ } from 'meteor/underscore'

const WidgetEdit = React.createClass({
  propTypes: {
    widget: React.PropTypes.object.isRequired,
    isLoading: React.PropTypes.bool.isRequired,
    commitWidget: React.PropTypes.func.isRequired,
    deleteWidget: React.PropTypes.func.isRequired
  },

  getInitialState () {
    return {
      err: null,
      widgetSame: true,
      committing: false
    }
  },

  shouldComponentUpdate (nextProps, nextState) {
    if (this.props.isLoading !== nextProps.isLoading) return true
    if (this.state.widgetSame !== nextState.widgetSame) return true
    if (this.state.committing !== nextState.committing) return true
    if (!_.isEqual(this.props.widget, nextProps.widget)) return true
    if (!_.isEqual(this.state.err, nextState.err)) return true
    return false
  },

  componentDidUpdate () {
    this.onAnyFieldChange()
  },

  isCreating () {
    return _.isEmpty(this.props.widget)
  },

  onSaveButtonClick () {
    var isCreating = this.isCreating()

    if (!this.state.widgetSame) {
      this.setState({committing: true})
      this.props.commitWidget(
        isCreating ? null : this.props.widget._id,
        this.widgetNameNode.value,
        (err, result) => {
          this.setState({committing: false})
          this.handleCommitCallback(err, result, isCreating)
        }
      )
    }
  },

  onDeleteButtonClick () {
    if (window.confirm('Are you sure?')) {
      this.setState({committing: true})
      this.props.deleteWidget(this.props.widget._id, (err, result) => {
        this.setState({committing: false})
        if (err) {
          this.setState({err: err})
        } else {
          browserHistory.push('/widgetlist')
        }
      })
    }
  },

  onAnyFieldChange () {
    this.setState({
      widgetSame: (
        this.widgetNameNode && (this.widgetNameNode.value === this.props.widget.name)
      )
    })
  },

  allFieldsSame () {
    return (
      this.state.widgetSame
    )
  },

  handleCommitCallback (err, result, wasCreating) {
    if (err) {
      return this.setState({err: err})
    }

    this.onAnyFieldChange()

    if (wasCreating) {
      browserHistory.push('/widgetlist')
    }
  },

  renderErrorMessage () {
    if (this.state.err) {
      return (
        <div>
          <pre>{JSON.stringify(this.state.err)}</pre>
          <button type='button' onClick={() => this.setState({err: null})}>OK</button>
        </div>
      )
    }
  },

  render () {
    const isCreating = this.isCreating()
    const isLoading = this.props.isLoading

    let heading
    let saveButton
    let deleteButton

    let saveButtonDisabled = (this.allFieldsSame() || this.state.committing)
    let deleteButtonDisabled = this.state.committing

    if (isLoading) {
      heading = '\u00a0'
    } else if (isCreating) {
      heading = 'New Widget'
      saveButton = <button type='button' onClick={this.onSaveButtonClick} disabled={saveButtonDisabled}>Create new widget</button>
    } else {
      heading = 'Edit Widget: ' + this.props.widget.name
      saveButton = <button type='button' onClick={this.onSaveButtonClick} disabled={saveButtonDisabled}>Update widget</button>
      deleteButton = <button type='button' onClick={this.onDeleteButtonClick} disabled={deleteButtonDisabled}>Delete widget</button>
    }

    return (
      <div>
        <h1>{heading}</h1>
        <form>
          <div className='form-group'>
            <label>Widget Name:</label>
            {isLoading ? <Spinner /> : (
              <input
                ref={(ref) => (this.widgetNameNode = ref)}
                onKeyUp={this.onAnyFieldChange}
                defaultValue={this.props.widget.name}
                type='text'
              />
            )}
          </div>
          <hr />
          {saveButton} {deleteButton}
        </form>
        {this.renderErrorMessage()}
      </div>
    )
  }
})

export default WidgetEdit

React Native

Special handling is necessary for the React Native Meteor library due to how it handles reactivity. The methods exposed by React Native Meteor are not truly reactive. Reactivity is accomplished by invalidating all active createContainer and getMeteorData functions with a single Dependency, invalidated for any and all DDP traffic received.

We've normalized around this by depending on this same DDP data dependency. The only thing you need to do differently is to add /native to then end of your import statement. For example:

import { connect } from 'tracker-react-redux/native'

License

MIT