React Router, refresh, and reactivity


#1

I am attempting to transition from Blaze/FlowRouter to React/React Router, and built a simple application called meteor-application-template-react to help me understand how Meteor, React, React Router, and Semantic UI all work together.

My problem occurs when I navigate to a page where the URL contains docID information: things work correctly when I navigate to the page using a React Router Link, but if I then refresh, the page displays but without the data.

For example, say I am on this page, which lists data in my sample app:

I can click the Edit button to go to a page to Edit some data:

So far so good. But now, if I hit the browser’s refresh button, the page displays but without the data:

I have learned about the client vs. server side routing, and since I am using HashRouter (and since the page displays), I assume there is something wrong with the reactive data binding? But if so, why does this only occur on the refresh and not the original page display?

Clearly there is something I don’t understand about React, React Router, and/or Meteor. Can someone please explain why this is occurring?

It’s probably helpful to take a quick look at the source code for the EditStuff.jsx page.

The router stuff is in App.jsx.

Thanks in advance!


#2

Try adding a .ready() to your subscribe call.

Meteor.subscribe('Stuff').ready();

#3

Unfortunately, that doesn’t fix it.


#4

In your withTracker add the following :

export default withTracker(({ match }) => {
  // Get the documentID from the URL field. See imports/ui/layouts/App.jsx for the route containing :_id.
  const documentId = match.params._id;
  let stuffSub = Meteor.subscribe('Stuff');
  return {
    doc: Stuff.findOne(documentId),
    ready: stuffSub.ready()
  };
})(EditStuff);

then in your render :

  render() {
    const { name, quantity } = this.state;
   if (!this.props.ready) {
       return ( <div>Loading</div>)
   } else {
    return (
        <Container text>
          <Segment.Group>
            <Segment attached='top' inverted color='grey'>Edit Stuff</Segment>
            <Segment>
              <Form onSubmit={this.handleSubmit}>
                <Form.Input required label='Name' name='name' value={name} onChange={this.handleChange}/>
                <Form.Input required label='Quantity' name='quantity' value={quantity} onChange={this.handleChange}/>
                <Button type='submit'>Submit</Button>
              </Form>
            </Segment>
          </Segment.Group>
        </Container>
    );
  }
}

#5

Or actually you may have to add the following :

componentDidUpdate(prevProps, prevState) {
 if(prevProps != this.props) {
   this.setState(() => ({
    name : this.props.doc && this.props.doc.name,
    quantity : this.props.doc && this.props.doc.quantity,
    _id : this.props.doc && this.props.doc._id
   }))
 }
}

#6

Thanks @ivo for your improvements!

Checking to see if subscriptions are ready is (of course) a good idea, but doesn’t fix my problem.

Adding the componentDidUpdate lifecycle method did fix the problem.

I’m still fuzzy on when adding this method is required. For example, my “ListStuff” page which subscribes to a collection will refresh just fine without the componentDidUpdate method, while the EditStuff page requires componentDidUpdate in order to refresh correctly.

The only difference I can see between these two pages is that in the EditStuff page, the withTracker HOC is passed the match parameter from React Router and uses it to get the docID. From this, I am guessing that you need to use componentDidUpdate only when you need to set the internal state of a component from a value obtained from the current URL using React Router.

Here is the updated EditStuff.jsx page with both the subscriptions ready and the componentDidUpdate enhancements added.

Thanks for the help! Hopefully this sample code will help out others who run across this issue.


#7

@philipmjohnson you can change the component from @ivo above to get name and quantity from props instead of state because you’re using withTracker to pass reactive data into your component. withTracker automatically re-renders your component with new props when they change!

you will have to make some other changes with your handlers from this.handler to either a func you pass in via props, or something else, but using functional components ends up making things simpler in the long run.

const EditStuff = ({ doc, ready }) => !ready ? (
  <div>Loading...</div>
) : (
  <Container text>
    ...
      <Form.Input required label='Name' name='name' value={doc.name} />
      <Form.Input required label='Quantity' name='quantity' value={doc.quantity} />
    ...
  </Container>
);

export default withTracker(({ match }) => {
  const documentId = match.params._id;
  const sub = Meteor.subscribe('Stuff');
  return {
    doc: Stuff.findOne(documentId),
    ready: sub.ready(),
  };
})(EditStuff);

#8

@rkstar That is an intriguing idea, but name and quantity turn out to be form field values. All of the examples I’ve seen in React for manipulating forms involve the use of component state to acquire form field values from user input. So, while I could probably pass in the name and quantity values as properties to EditStuff, I currently only know to use internal state to gather the user’s input as they fill out the form.

Is there example code you can point me to that illustrates your alternative approach in the case where the properties being passed to a component are form field values that the user will then update? That would resolve my confusion.

Thanks for your insights. I’m quite new to React + Meteor, but it’s definitely interesting and enjoyable to play around with.


#9

well, your handleChange func would take care of the input and your onSubmit button action would submit the form, right?

there’s some massaging you would have to do here, but not a ton.

inside your component file, but above const EditStuff = () => ...

const formData = {
  name: '',
  quantity: 0,
};
function handleOnChange(e) {
  formData[e.target.name] = e.target.value;
}

function onSubmit(e){
  // 
  // send formData to your endpoint...
  //
}

const EditStuff = () => (
  <Form.Input ..... onChange={handleOnChange} />
  <Button ... onClick={submitForm}>Submit</Button>
);

#10

you could also look at recompose for help with that.


#11

@rkstar I’ve tried to implement your suggestions but can’t quite get there. I’m definitely interested in understanding this alternative. I know this is a lot to ask, but if you have some time in the future and would be willing to make a fork of https://github.com/ics-software-engineering/meteor-application-template-react to illustrate how your approach works, I would really appreciate it. If not, no worries, I’ll keep fiddling around with it on my own.


#12

So I’ve finally gotten a chance to clone the repo and have a look at how everything works in more detail than on my phone. Is there a reason that you are using state in the render function instead of props directly? This simple change fixes your issue very easily.

Also as a quick tip, working with forms like this in react is greatly simplified using the uniforms package.

Hope this helps.


#13

Never mind, I see now that it due to saving data to the state for form submission :stuck_out_tongue:


#14

Pup is an excellent meteor + react + react router boilerplate. You might find answers there.


#15

Just FYI, I’ve started using React Uniforms to handle form processing which handles this issue quite nicely.

Here’s the revised EditStuff.jsx component. No need for componentDidUpdate(), code is very simple.