React context withTracker is awesome!

I had already settles on this connector pattern in my apps, where I put my connector junk (withTracker, redux, etc.) in a connectors folder, and then use those as reusable components throughout my app. It looked like this:

// UPDATE: I added some fixes for SSR, and to make sure to use userId 
// instead of user for iLoggedIn check, to avoid problems during startup, 
// like premature redirects
export const withAccount = withTracker((props) => {
  const user = Meteor.isServer ? null : Meteor.user()
  const userId = Meteor.isServer ? null : Meteor.userId()
  return { account: {
    user,
    userId,
    isLoggedIn: !!userId
  } }
})

Since withTracker returns a HoC, which accepts another component, I can then reuse the same code anywhere I needed the data it provides by wrapping my one off components.

const SideBarAccountInfo = withAccount((props) => <div>Account SideBar</div>)
const HeaderAccountInfo = withAccount((props) => <div>Account Header</div>)
// ...etc.

In many cases this is pretty good. I have some data types like a “pages and parts” collection, which can take a property for the document id, at whatever depth I need it, without doing a tone of prop drilling.

But sometimes, the data isn’t unique, as in the example above - it always is she same data, so it’s a tiny bit wasteful to pull that data out every time I need it. With React’s new context API, I can create a Provider and Consumer, and only pull the data once!

import { Meteor } from 'meteor/meteor'
import React from 'react'
import { withTracker } from 'meteor/react-meteor-data'

const AccountContext = React.createContext('account')

export const withAccount = withTracker((props) => {
  const user = Meteor.isServer ? null : Meteor.user()
  const userId = Meteor.isServer ? null : Meteor.userId()
  return { account: {
    user,
    userId,
    isLoggedIn: !!userId
  } }
})

function Provider (props) {
  return <AccountContext.Provider value={props.account}>
    {props.children}
  </AccountContext.Provider>
}

export const AccountProvider = withAccount(Provider)
export const AccountConsumer = AccountContext.Consumer

So now I can add the account data once using the provider, and use the consumer to pull out the data from that single source. Yay!

What do you think?

24 Likes

That is neat. It never occurred to me when I first saw it when 16.3 was released. For interested people here are the docs:

1 Like

Great stuff @captainn!
I was looking for something exactly like this, but for some reason I didn’t come up with this pretty elegant solution!

Sidenote / quick question to you React users:
How do you access the currently logged in user throughout your Meteor/React app?
Just use Meteor.user() whenever you need it? Or do you have some other approach?

You can use Meteor.user() whenever you need it, but it only return the user id. We may need more than that.
So in a very root component, you can subscribe user information and store it in props or context, then in child component you can pull it out without doing some meteor magics.

Aren’t you mixing up Meteor.user() with Meteor.userId() right here?

Oh, I was wrong :smile:. It’s Meteor.userId(). But if you want to track more data which not provided by Meteor.user()

Superbe solution. Good job!

I discovered something interesting, which is that when the page first renders, Meteor.user() returns undefined, instead of either an object when you are logged in, or null when you aren’t logged in. I suspect it’s waiting for some data from the server. I changed the isLoggedIn check to use Meteor.userId() which does always return an ID even during startup.

6 Likes

I am torn between using the new Context API vs. Meteor’s Session

The Context API will re-render all the child of the consumer whenever the value changes while with Session + withTracker() you can control the renders

2 Likes

Yes, it takes a while for the subscription to kick in. I use Meteor.userId() in combination with Meteor.loggingIn().

1 Like

I only ever use Meteor’s reactive data sources (including Meteor.user() and Meteor.userId()) from inside a withTracker component, to make sure if the state changes, it’ll flow through my app

Using Context isn’t really about where the data comes from - it’s just a way to make the source more of a single location (the Provider) then use the Context to grab that, instead of multiple nested copies of withTracker.

You could always put your Session inside of a withTracker’d Context. :slight_smile:

I have found that for a lot of cases I don’t need the complexity of a Context, sticking with just a one off withTracker here an there is perfectly fine. I try not to favor one tool too much - right tool for the job and all that.

2 Likes

I don’t see any use of Context Api that you cannot do with Meteor’s Session. Prior to the Context Api, I use Session so that:

  1. There can be a single source of data where I call Session.set()
  2. I don’t need to pass the data through a tree of components to reach a particular child

That is what the context api also provided us.

The only difference is that with session, there is better control of renders while with Context, we are forced to re-render the complete subtree inside the Consumer

How are you wiring in your session though? Are you using it directly inside of render methods, or are you using withTracker?

Inside withTracker. And then use shouldComponentUpdate or getDerivedStateFromProps to figure out if a re-render is required.

The nice thing about @captainn’s solution is that you only get one autorun, which limits the reactivity to one place. If you make a higher order component (called withUserAccount are whatever) that reads from the context, you can still get the benefits of limiting re-renders with shouldComponentUpdate.

Maybe I’m missing something and would love to learn. Can you give an example of using AccountConsumer component wherein we can control the re-render of its child component?

It would be exactly the same as using withTracker directly. Here’s a super simple example:

This uses a context Provider:

class Feed extends Component {
  render () {
    return <ContentProvider>
      <ContentConsumer>
        {(content) => <ContentCard content={content} key={content._id} />}
      </ContentConsumer>
    </ContentProvider>
  }
}

This uses withTracker directly:

class Feed extends Component {
  render () {
    const {content} = this.props
    return  content.map((content => <ContentCard content={content} key={content._id} />
  }
}

const FeedContainer = withTracker((props) => { ... })(Feed)

Using a Consumer directly inside a Provider like that may seem like overkill, but this is just for example. You could for example, wrap your whole App with a User Account Provider (or whatever your Session state holds), then just use the Consumer anywhere you need that state. The alternative would be to use withTracker each and every place you need to access that state, which would create a lot of instances of computations.

Normally, I actually abstract the withTracker thing away:

const withContent = withTracker((props) => { ... })

Then I can just use the single with{Thing} connector everywhere:

const ComponentContainer = withContent(Feed)

That’s okay, works pretty well. I like the composition of the Context API a little bit better though.

The point is that ContentCard, however it’s implemented will be re-rendered exactly the same way in both cases.

Oh, another benefit of working this way, is you can put some additional logic in your Provider. In my app my withTracker container class, does a query from a local Ground DB source, to populate the data. If there’s data on startup, it uses that, runs it through the context API, gets used by the Consumer. My Provider has a function which runs mount, to grab data from Meteor’s Mongo server over a method (it’s way faster than grabbing data over DDP), then stuff that into GroundDB storage. Once the data has been updated in GroundDB, withTracker does it’s magic and reflows the data. This would have been more challenging without access to the full lifecycle methods.

I’m actually working with a bunch of interns, and honestly the container model - pure functional programming, is harder for them to pick up. Components they get.

Thanks for that.

Although, that was my issue. I don’t want ContentCard (and whatever child components it might have) to re-render in some cases. I actually use the context api but since I cannot control consumer child re-render, I left it on cases when I know that the child component of the consumer must always re-render when the provider value changes. For those I need render control, I moved back to using session until I can find a way to control renders within Consumer child components

withTracker(() => {
   return { user: Meteor.user() };
})

Will create an HOC that does the same thing as a HOC using context like

WrappedComponent => ({ ...props }) => (
   <AccountConsumer>
      {({ user }) => (
         <WrappedComponent user={user} {...props} />
      )}
   </AccountConsumer>
)

The only difference is that if you use the first solution, an autorun is created for each component that uss it, whereas with the second solution, there is only a single autorun created for the context. The second solution also has the benefit of the reactivity happening in spot: things are easier to debug, changes easier to make, reactivity is more predictable, etc. Both solutions have the same behaviour in regards to re-renders (as in, any change to Meteor.user() would cause both to rerun).