Ghost
Ghost

Reputation: 407

React useState with ES6 classes

I want to create a form to save some form of data model in a database.

Let's say I have a class:

export default class Model {
    constructor(model = {}) {
        this.x = model.x || '';
        this.y = model.y || '';
    }
    save() {
        // code to save 'this' to database
    }

}

And a component:

import { React, useState } from 'react';
   
export const ModelForm = () => {
    const [model, setModel] = useState(new Model());

    const handleInputChange = (event) => {
        const target = event.target;

        setModel({ ...model, [target.name]: target.value });
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        const modelObject = new Model(model);
        modelObject.save();
    };

    return (
        <form onSubmit={handleSubmit}>
            <input type='text' name='x' value={model.x} onChange={handleInputChange} />
            <input type='text' name='y' value={model.y} onChange={handleInputChange} />
            <button type='submit'>Submit</button>
        </form>
    );
};

This thing is currently working, but I have a question.

Every time I do setModel, I have to destructure the model object and change the value accordingly. Since I destructure it, when I am submitting the form, the object in the component is no longer a Model object, but a simple javascript Object. So, as you can see, I have to create a new one to use save() on it.

Is my approach good or there is a better way to do it?

Also, is it a good practice to use ES6 classes in useState? Are there any downfalls?

Upvotes: 4

Views: 2917

Answers (3)

In my personal opinion, I don't think using ES6 classes in the state is a good idea. In fact, in the react useState hook documentation say that if you need to have two states on your component you should put two useState hooks.

// If there are too many states, you can use useReducer hook
const [state1, setState1] = useState();
const [state2, setState2] = useState();

I prefer use the useState hooks with primitives values, because as React works, when you change the state, if the value of the new state is equal to the old one, the component doesn't re-render because react do that comparison by itself, but when you use a object or any other non primitive value, even if the props of the object and values are the same, the reference to that object is going to be different, so it will re-render when it should not. Remember that when JavaScript compares non primitive values it compares its references in memory so:

const obj1 = {prop: 'a'};
const obj2 = {prop: 'a'};
console.log(obj1 === obj2); // This logs false because they are different objects and they references in memory are not equal
console.log(obj1 === obj1); // This logs true, because its reference is the same.
console.log(obj1.prop === obj2.prop); // This logs true because they are strings (primitive values)

Upvotes: 1

Bergi
Bergi

Reputation: 664444

I have to create a [new Model] to use save() on it.

That's certainly a viable approach. You wouldn't actually store a Model instance in your state, but only the options object for your constructor. Initialise it as simply

const [model, setModel] = useState({});

(and possibly rename it to modelArgs or modelOptions)

Since I destructure it, […] the object in the component [state] is no longer a Model object, but a simple javascript Object.

It's actually not destructuring, rather object literal spread syntax, but yes - you're creating a plain object in your setModel call.

To fix the problem, you'd need to pass a model instance there - a new one, since React state is designed around immutability. You can easily achieve that though:

export default class Model {
    constructor(model = {}) {
        this.x = model.x || '';
        this.y = model.y || '';
    }
    withUpdate({name, value}) {
        // if (!['x', 'y'].includes(name)) throw new RangeError(…)
        return new Model({...this, [name]: value});
        // or optimised (?):
        const clone = new Model(this);
        clone[name] = value;
        return clone;
    }
    save() {
        // code to save 'this' to database
    }

}
const [model, setModel] = useState(new Model());
const handleInputChange = (event) => {
    setModel(model.withUpdate(event.target));
};

Upvotes: 3

Zachiah
Zachiah

Reputation: 2627

I agree that it is very usefull to have es6 classes in your state sometimes. A decent solution is just to create a derived variable.

import { React, useState } from 'react';
   
export const ModelForm = () => {
    const [_model, setModel] = useState({});
    const model = new Model(_model)

    const handleInputChange = (event) => {
        const target = event.target;

        setModel({ ...model, [target.name]: target.value });
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        const modelObject = new Model(model);
        modelObject.save();
    };

    return (
        <form onSubmit={handleSubmit}>
            <input type='text' name='x' value={model.x} onChange={handleInputChange} />
            <input type='text' name='y' value={model.y} onChange={handleInputChange} />
            <button type='submit'>Submit</button>
        </form>
    );
};

Upvotes: 1

Related Questions