M
M
Mark2021-10-08 14:33:41
React
Mark, 2021-10-08 14:33:41

How to share responsibility in React and how to solve the problem with notifying the state of a Model change?

I'm learning React, JavaScript, doing a test project - Gym Journal.

9CMLxtS.png

Component Tree


vUA7PqN.png


Very fat Exercise , it:
- Fills itself with data from the form handleChange, handleSubmit)
- Receives data from the form about Set( handleWeightChange, handleRepsChange, toggleMaxReps)
- Manages the Collection Model Set( addSet, getSet, updateSet), and itself ( changeWeight, changeReps).
- Renders

Component data on Github: https://github.com/likont/workout-diary.loc/blob/m...
The code

import React from "react";
import generateUniqueId from "../utils/generateUniqueId";
import Set from "./Set";

class Exercise extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            id: generateUniqueId(),
            label: null,
            confirmed: false,
            sets: [],
        }

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);

        this.handleWeightChange = this.handleWeightChange.bind(this);
        this.handleRepsChange = this.handleRepsChange.bind(this);
        this.toggleMaxReps = this.toggleMaxReps.bind(this);
    }

    handleChange(event) {
        this.setState({label: event.target.value});
    }

    handleSubmit(event) {
        this.setState({
            confirmed: true,
            sets: [{id: generateUniqueId(), weight: null, reps: null, enableMaxReps: false}]
        })
        event.preventDefault();
    }

    handleWeightChange(event, id) {
        this.updateSet({
            id,
            updatedSet: {weight: event.target.value},
            listener: (sets) => this.addSetWhenPrevSetFilled(sets)
        });
    }

    handleRepsChange(event, id) {
        this.updateSet({
            id,
            updatedSet: {reps: event.target.value},
            listener: (sets) => this.addSetWhenPrevSetFilled(sets)
        });
    }

    toggleMaxReps(id) {
        let set = this.getSet(id);
        if (set) {
            set.enableMaxReps = !set.enableMaxReps;
            this.updateSet({id, updatedSet: set});
        }
    }

    addSetWhenPrevSetFilled(sets) {
        let lastSet = sets[sets.length - 1];
        let isNeedEmptySet = true;

        for (let key in lastSet) {
            if (lastSet[key] === null) {
                isNeedEmptySet = false;
            }
        }

        if (isNeedEmptySet) {
            this.addSet({weight: null, reps: null});
        }
    }

    addSet({weight, reps, enableMaxReps}) {
        enableMaxReps = enableMaxReps || false;
        this.setState({
            ...this.state,
            sets: [
                ...this.state.sets,
                {id: generateUniqueId(), weight, reps, enableMaxReps}
            ]
        });
    }

    getSet(id) {
        for (let set of this.state.sets) {
            if (set.id === id) {
                return set;
            }
        }

        return null;
    }

    updateSet({id, updatedSet, listener}) {
        updatedSet = updatedSet || {};
        this.setState({
            ...this.state,
            sets: this.state.sets.map((set) => {
                if (set.id === id) {
                    return {
                        ...set,
                        ...updatedSet
                    };
                }
                return set;
            })
        });

        if (typeof listener === "function") {
            setTimeout(() => {
                listener(this.state.sets);
            }, 1000)

        }
    }

    render() {
        return (
            this.state.confirmed ?
                <ExerciseView
                    id={this.state.id}
                    title={this.state.label}
                    sets={this.state.sets}
                    changeReps={this.handleRepsChange}
                    changeWeight={this.handleWeightChange}
                    toggleMaxReps={this.toggleMaxReps}
                /> :
                <ExerciseFormView onSubmit={(e) => this.handleSubmit(e)} onChange={(e) => this.handleChange(e)}/>
        );
    }

}

function ExerciseView({id, title, sets, changeWeight, toggleMaxReps, changeReps}) {
    return (
        <div className="col-12" key={id}>
            <h5>{title}</h5>
            <table className="table table-bordered">
                <tbody>
                {sets.map((set, index) =>
                    <Set
                        set={set}
                        changeWeight={(e) => changeWeight(e, set.id)}
                        changeReps={(e) => changeReps(e, set.id)}
                        toggleMaxReps={() => toggleMaxReps(set.id)}
                        number={index + 1}
                    />)}
                </tbody>
            </table>
        </div>
    );
}

function ExerciseFormView({onSubmit, onChange}) {
    return (
        <div className="col-12">
            <form onSubmit={onSubmit}>
                <div className="input-group mt-4 mb-4">
                    <select className="custom-select" id="select-exercise" onChange={onChange}
                            aria-label="Select workout type">
                        <option defaultValue>Выбрать...</option>
                        <option value="Жим лежа">Жим лежа</option>
                        <option value="Отжимания">Отжимания</option>
                        <option value="Тяга гантели">Тяга гантели</option>
                    </select>
                    <div className="input-group-append">
                        <button className="btn btn-outline-secondary" type="submit">Начать</button>
                    </div>
                </div>
            </form>
        </div>
    );
}

export default Exercise;



So what's the problem?
1. It looks like a violation of the Single Responsibility Principle.
2. I don't like that Set is a Plain Object, you have to remember what properties it contains.
3. Too long code.

How can the problem be solved?

What solution do I see?
1) Create classes for Models:
- SetCollection- manages an array of approaches ( SetCollection.add(), SetCollection.remove())
- Set- manages its data ( Set.create(), Set.changeMaxWeght(), etc.)

Problem : Class objects outside the Component State => React does not know about their changes => React does not render the page.
Is it possible to pass the closure with forceUpdate()to the class constructor SetCollection, Setand call it when the data changes.

2) Create componentSet, management of its form (functions) to carry out there.

3) Component = Controller. Passes data from the form to the Model, and if it changes itself, it notifies the component, and it re-renders.

But for sure, there is some standard that can solve my problem, but I could not find it.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
Y
Yury093, 2021-10-08
@Yury093

To manage the state, you need to use one of the state management libraries.
One of the most popular right now is Redux. There is also Mobx, for example. There are other new fashionable ones - read the comparison on the request "alternatives to Redux" or here's the article: https://habr.com/ru/company/ruvds/blog/566102/

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question