Meteor&React: Tipps for best practice: "edit dataset" with controlled components form

Hey,
as my first Meteor project I try to create a simple address book.

From the “ToDo-List” example I could abtract most of the things. Adding contacts, list the contacts, use React Router to access contact details via URL. :+1:

But I need some advise how to handle data in a “edit contact details”-form:
It seems like the reactivity-features doesn’t make much sense here.

  • When I use useTracker() I risk that data which I’m currently editing gets overidden.
  • When I save the query-result in an React-state I somehow create a loop, which React terminates.
  • When I remove useTracker and query data directly in the component function I loose the callback and if (!Meteor.subscribe('contacts').ready()) is not called again. I could put this in a while-loop but this would block the UI. Solve this with an async-task? Feels like I’m leaving the meteor/react eco-system with this.

Probably the solution is really simple. Any hints for example code are appreciated :slight_smile:

Here is what I got until now

import React, {useState} from 'react';
import {useTracker} from "meteor/react-meteor-data";
import {Contacts} from "../../db/Contacts";

export const ContactDetailForm = ({match}) => {
    
    let [localContact, setLocalContact] = useState();

    const {isLoading} = useTracker(() => {

        console.log("entering useTracker", match);
        if (!Meteor.user()) {
            console.log("user not logged in");
            return {isLoading: true};
        }

        if (!match) {
            console.log("no params provided!");
            return {isLoading: true};
        }

        if (!Meteor.subscribe('contacts').ready()) {
            console.log("subscription not ready now");
            return {isLoading: true};
        }

        const contact = Contacts.findOne({_id: match.params.id});

        setLocalContact(contact);

        return {isLoading: false};
    });

    const handleSubmit = e => {
        e.preventDefault();
        /* save edited contact to db*/

    };

    const changeHandler = (event) => {
        let fieldName = event.target.name;
        let fieldValue = event.target.value;
        setLocalContact({...localContact, [fieldName]: fieldValue});
    };

    return (
        <div className="container">
            {isLoading ? <div>Loading</div> : (<form onSubmit={handleSubmit}>
                    <label>First name</label>
                    <input onChange={changeHandler} type="text" value={localContact.firstName} id="inputFirstName" name="firstName"/>

                    <label>Last Name</label>
                    <input onChange={changeHandler} type="text" value={localContact.lastName} id="inputLastName" name="lastName"/>

                {/*all the other contact-fields....*/}

                    <button type="submit">save</button>
            </form>)}
        </div>)
};

Here are a couple simple Meteor+React example systems that might provide helpful code.

https://ics-software-engineering.github.io/meteor-application-template-react/

https://bowfolios.github.io/

I use these to teach my software engineering class.

Philip

1 Like

You should not call setLocalContact like that in the hook - that’ll run every single time the component is rendered. Instead return it from your useTracker - something like:

return {isLoaded: false, contact}

For the handler, you’d want to update your React Data store, instead of working on a local copy. If you really want a local copy (maybe for temporary edits) you can still manage that with local state, but don’t mix the two. When you are ready to submit, update the copy in the collection, and let Meteor’s reactivity update your copy. You’ll have to do some work to manage the two copies during render.

You could reverse the logic a bit:

import React, {useState} from 'react';
import {useTracker} from "meteor/react-meteor-data";
import {Contacts} from "../../db/Contacts";

export const ContactDetailForm = ({match}) => {
    
    const {isLoading, contact} = useTracker(() => {

        console.log("entering useTracker", match);
        if (!Meteor.user()) {
            console.log("user not logged in");
            return {isLoading: true};
        }

        if (!match) {
            console.log("no params provided!");
            return {isLoading: true};
        }

        if (!Meteor.subscribe('contacts').ready()) {
            console.log("subscription not ready now");
            return {isLoading: true};
        }

        const contact = Contacts.findOne({_id: match.params.id});

        return {isLoading: false, contact};
    });

    // set the default object during initialization
    let [localContact, setLocalContact] = useState(contact);

    const handleSubmit = e => {
        e.preventDefault();
        /* save edited contact to db*/
        
    };

    const changeHandler = (event) => {
        let fieldName = event.target.name;
        let fieldValue = event.target.value;
        setLocalContact({...localContact, [fieldName]: fieldValue});
    };

    return (
        <div className="container">
            {isLoading ? <div>Loading</div> : (<form onSubmit={handleSubmit}>
                    <label>First name</label>
                    <input onChange={changeHandler} type="text" value={localContact.firstName} id="inputFirstName" name="firstName"/>

                    <label>Last Name</label>
                    <input onChange={changeHandler} type="text" value={localContact.lastName} id="inputLastName" name="lastName"/>

                {/*all the other contact-fields....*/}

                    <button type="submit">save</button>
            </form>)}
        </div>)
};

Actually, that won’t work, because of the subscription step. Maybe just use deps to keep useTracker from running every render:

import React, {useState} from 'react';
import {useTracker} from "meteor/react-meteor-data";
import {Contacts} from "../../db/Contacts";

export const ContactDetailForm = ({match}) => {
    
    let [localContact, setLocalContact] = useState();

    const {isLoading} = useTracker(() => {

        console.log("entering useTracker", match);
        if (!Meteor.user()) {
            console.log("user not logged in");
            return {isLoading: true};
        }

        if (!match) {
            console.log("no params provided!");
            return {isLoading: true};
        }

        if (!Meteor.subscribe('contacts').ready()) {
            console.log("subscription not ready now");
            return {isLoading: true};
        }

        const contact = Contacts.findOne({_id: match.params.id});

        // make sure this doesn't run on every render by setting deps below
        setLocalContact(contact);

        return {isLoading: false};
    }, [match.params.id]); // set deps here

    const handleSubmit = e => {
        e.preventDefault();
        /* save edited contact to db*/

    };

    const changeHandler = (event) => {
        let fieldName = event.target.name;
        let fieldValue = event.target.value;
        setLocalContact({...localContact, [fieldName]: fieldValue});
    };

    return (
        <div className="container">
            {isLoading ? <div>Loading</div> : (<form onSubmit={handleSubmit}>
                    <label>First name</label>
                    <input onChange={changeHandler} type="text" value={localContact.firstName} id="inputFirstName" name="firstName"/>

                    <label>Last Name</label>
                    <input onChange={changeHandler} type="text" value={localContact.lastName} id="inputLastName" name="lastName"/>

                {/*all the other contact-fields....*/}

                    <button type="submit">save</button>
            </form>)}
        </div>)
};
1 Like

thank you! This is a pretty solid codebase to go the next steps from the Meteor-ToDo-List-example!

It looks like “Meteor-world” encourages developers to use functional componets instead of class components and hooks like useTracker() instead of withTracker(). Meanwhile a lot of example code (like yours) “still” use the former, what makes it difficult to smoothly dive into understanding meteor+react patterns. But I will try my best to abstract this from your code and use it as a learning resource :slight_smile:

You could use a functional component with withTracker - it’s really just preference. The Meteor guide still uses withTracker I think. I prefer hooks - but that’s more of a “React-world” encourages the adoption of hooks thing. :wink:

1 Like

I need to open this issue againI: The way provided by @captainn seems to be to be only applicable if there is some ID in the props, which can be used to keep useTracker from firing again and again. In case I want to use db-data without a correlating ID from the URL (e.g. a general settings page) , I ran into the same problem again. (I tried to pass an empty array as deps, but this did not work)

I think the best approach would be to avoid reactivity in the context of an “edit form”, because as soon, as the form is filled, update from the db are not desirable, because they would override the manual changes before they could be saved. So the correct question would be:

What’s the best way to query db-data without withTracker/useTracker on client side?

I guess [Collection].subscribe() still has to be called and I still have to wait for [Handler].ready() to return true.
Implement a method for the desired collection on the server which returns the data would be an option, but is this the preferred way?

Greetings

You should probably just avoid calling setSomeState methods inside useTracker.

Maybe do some logic in the render body, instead of the hook.

import React, {useState} from 'react';
import {useTracker} from "meteor/react-meteor-data";
import {Contacts} from "../../db/Contacts";

export const ContactDetailForm = ({match}) => {
    // initially set to null
    let [localContact, setLocalContact] = useState(null);

    const {isLoading, contact} = useTracker(() => {

        console.log("entering useTracker", match);
        if (!Meteor.user()) {
            console.log("user not logged in");
            return {isLoading: true};
        }

        if (!match) {
            console.log("no params provided!");
            return {isLoading: true};
        }

        if (!Meteor.subscribe('contacts').ready()) {
            console.log("subscription not ready now");
            return {isLoading: true};
        }

        const contact = Contacts.findOne({_id: match.params.id});

        return {isLoading: false, contact};
    }, [match.params.id]);

    // set your localContact, if needed
    if (!localContact) {
        // NOTE: this will not be reactive, but you probably don't want that anyway
        localContact = contact;
    }

    const handleSubmit = e => {
        e.preventDefault();
        /* save edited contact to db*/

    };

    const changeHandler = (event) => {
        let fieldName = event.target.name;
        let fieldValue = event.target.value;
        setLocalContact({...localContact, [fieldName]: fieldValue});
    };

    return (
        <div className="container">
            {isLoading ? <div>Loading</div> : (<form onSubmit={handleSubmit}>
                    <label>First name</label>
                    <input onChange={changeHandler} type="text" value={localContact.firstName} id="inputFirstName" name="firstName"/>

                    <label>Last Name</label>
                    <input onChange={changeHandler} type="text" value={localContact.lastName} id="inputLastName" name="lastName"/>

                {/*all the other contact-fields....*/}

                    <button type="submit">save</button>
            </form>)}
        </div>)
};

It’s a little hacky, but it gets it done.