1

The following is probably the neatest code that illustrates my problem I could think of. I am essentially trying to draw a table to the page that pulls exercise data from a database, sets that data to a state of an array of objects, and then when a row is clicked, render an 'edit' row below it that allows the user to change any of the array data inside one of those objects.

import React, {useEffect} from "react";

import { useState } from "react";
import { useNavigate } from "react-router";

export default function GetProgram() {
    // holds all n number of exercises from database
    const [exercises, setExercises] = useState([])
    // number that holds which exercises is currently being shown/edited
    const [editingExerciseIndex, setEditingExerciseIndex] = useState(-1)
    // once both effects have fetched the data
    const [loading, setLoading] = useState([true, true])
    // holds info about the program to map for exercise displaying
    const [program, setProgram] = useState({
        days: [],
        name : '',
        _id : null
    })

    useEffect(() => {
        async function getProgramInfo() {
            fetch(`http://localhost:5000/program`).then((res) => {
                res.json().then((body) => {
                    setProgram(body)
                    setLoading([false, loading[1]])
                })
            })
            .catch((err) => {
                console.log(`**ERR: ${err}`)
                return
            })
        }
        getProgramInfo()
    }, [])

    useEffect(() => {
        async function getExercises() {
            fetch(`http://localhost:5000/program/getmap`).then((res) =>{
                res.json().then((body) => {
                    
                    setExercises(body)
                    setLoading([loading[0], false])
                })
            })
        }
        getExercises()
    }, [])

    // onChange handler for edit fields
    function updateField(props) {

        var newExercise;

        if (props.reps) {
            const newReps = exercises[props.j].reps.map((r, i) => {
                if (i === props.set-1) {
                    return parseInt(props.reps,10) // user's input
                }
                return r // old value
            })
            newExercise = {...exercises[props.j], reps:newReps} // []
            console.log(newExercise)
        }
        else {
            const newWeight = exercises[props.j].weight.map((w, i) => {
                if (i === props.set-1) {
                    return parseInt(props.weight,10)
                }
                return w
            })
            newExercise = {...exercises[props.j], weight:newWeight}
        }
        console.log([...exercises, newExercise])

        setExercises(exercises.map((exercise, i) => {
            if (exercise.day === newExercise.day && exercise.position === newExercise.position) {
                return newExercise
            }
            return exercise
        }))
    }

    const EditFieldRow = (props) => {
        console.log("Rendering Edit Field Row")
        return (
            <tr>
                <td>{props.set}</td>
                <td><input key="reps-input" type="text" value={props.reps} onChange={(e) => updateField({j:props.j, set:props.set, reps:e.target.value})}/></td>
                <td><input key="weight-input" type="text" value={props.weight} onChange={(e) => updateField({j:props.j, set:props.set, weight:e.target.value})}/></td>
            </tr>
        )
    }

    const EditField = (props) => {
        return (
            <div>
                <form onSubmit={() => console.log("submitted")}>
                <table className="table table-bordered table-colored">
                    <thead>
                        <tr>
                            <th>Set</th>
                            <th>Reps</th>
                            <th>Weight</th>
                        </tr>
                    </thead>
                    <tbody>
                        {[...Array(props.exercise.sets).keys()].map((i) => {
                            return (
                                <EditFieldRow j={props.j} key={`fieldrow-${i}`} set={i+1} reps={props.exercise.reps[i]} weight={props.exercise.weight[i]}/>
                            )
                        })}
                    </tbody>
                </table>
                </form>

            </div>
        )
    }

    const PageContent = (props) => {
        return (
            <div className="container-fluid page-content program-page" >
                <h2 style={{textAlign:'center'}}>{program.rname??program.name} Program</h2>
                <div className="row">
                    {program.dayMap.map((day, i) => {
                        return (
                            <div className="col" key={`${day}-${i}`}>
                                <h4>{day}</h4>
                                <hr />
                                <table className="lift-table table table-bordered table-colored">
                                    <thead>
                                        <tr>
                                        <th>Name</th>
                                        <th>Sets</th>
                                        </tr>
                                    </thead>
                                    {exercises.map((exercise, j) => {
                                        if (exercise.day === i+1) {
                                            return (
                                                <tbody key={`${exercise.name}${i}${j}${day}`}>
                                                    <tr id={`exercise-row-${exercise.name.replaceAll(" ", "-")}`} className={`exercise-row`} 
                                                    onClick={() => {
                                                        setEditingExerciseIndex(j)
                                                    }}
                                                        key={`${exercise.name}-${i}-${day}`}
                                                    >
                                                        <td>{exercise.name}</td>
                                                        <td>{exercise.sets}</td>
                                                    </tr>
                                                    {editingExerciseIndex === j && <tr><td colSpan="2">
                                                         <EditField exercise={exercises[j]} j={j}/>
                                                    </td></tr>}
                                                </tbody>
                                            )
                                        }
                                    })}
                                </table>
                            </div>
                        )
                    })}
                </div>
            </div>
        )
    }

    if (program.dayMap) {
        return (
            <PageContent />
        )
    }
    return (
        <div></div>
    )
}

The exercises array would look something like this

{
        "program" : "Full-body-3d",
        "name" : "Bench Press",
        "position" : 1,
        "day" : 1,
        "sets" : 3,
        "reps" : [
            6, 6, 6
        ],
        "ref" : "Bench",
        "weight" : [
            80, 80, 80
        ]
    },

    {
        "program" : "Full-body-3d",
        "name" : "Lat Pulldown",
        "position" : 2,
        "day" : 1,
        "sets" : 3,
        "reps" : [
            12, 12, 12
        ],
        "ref" : "Accessory",
        "weight" : [
            80, 80, 80
        ]
    },
...

Where

position - order of the exercise to perform
day - day that is mapped to said exercise (ex. 1 might represent "Push day")
sets - will always be the length of reps[] and weight[] (not strictly necessary)

the rest aren't extremely important

This is my App.js that routes all pages:

import React, { useEffect, useState } from "react";

// We use Route in order to define the different routes of our application
import { Route, Routes } from "react-router-dom";
import { useCookies } from 'react-cookie';

// We import all the components we need in our app
import NB from "./components/navbar";
import WorkoutCalendar from "./components/workoutCalendar";
import Edit from "./components/edit";
import Create from "./components/create";
import Settings from "./components/settings";
import Header from "./components/header";
import Progress from "./components/progress";
import DayInfo from "./components/dayInfoPage";
import Delete from "./components/deleteall";
import Todo from "./components/dev/todo";
import GetProgram from "./components/lift/programPage";
import AddLift from "./components/lift/create";
import Populate from "./components/dev/pop_db";
import PopulateColorThemes from "./components/dev/pop_colors";
import UserList from "./components/user/list";
import UserLogin from "./components/user/login";
import Rec from "./components/rec";

const App = () => {
    const [cookies, setCookie] = useCookies(['theme'])
    const [loading, setLoading] = useState(true)
    
    useEffect(() => {
        setCookie('ColorTheme', cookies.ColorTheme ?? 'Ender', {path:'/'})
        setLoading(false)
    },[])

    const [username, setUsername] = useState(''); 

    if (loading) {
        return (
            <div className="page" style={{backgroundColor:'grey'}}></div>
        )
    }
    else {
        return (
            <div className="page" data-theme={cookies.ColorTheme??'Ender'}>
                {/* navbar */}
                <NB />
    
                {/* header */}
                <Header username={username}/>
    
                {/* content */}
                <div className="page-content-area">
                    <Routes>
                        <Route exact path="/" element={<WorkoutCalendar />} />
                        <Route path="/edit/:id" element={<Edit />} />
                        <Route path="/record/create" element={<Create />} />
                        <Route path="/settings" element={<Settings />} />
                        <Route path="/record/:date" element={<DayInfo />} />
                        <Route path="/deleteall" element={<Delete />} />
                        <Route path="/dev/todo" element={<Todo />} />
    
                        <Route path="/program" element={<GetProgram />} />

                        <Route path="/lift/add" element={<AddLift />} />
                        <Route path="/program/populate" element={<Populate />} />
                        <Route path="/user" element={<UserList />} />
                        <Route path="/user/login" element={<UserLogin headerUsername={setUsername}/>} />
                        <Route path="/color/populate" element={<PopulateColorThemes />} />
                        <Route path="/record" element={<Rec />} />
                    </Routes>
                </div>
            </div>
        );
    }
    
};

export default App;

And finally index.js which renders the components

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import {CookiesProvider} from 'react-cookie'

import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap';


const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <CookiesProvider>
    <BrowserRouter>
      <App />
    </BrowserRouter>
    </CookiesProvider>
  </React.StrictMode>,
);


I cannot seem to find where I am going wrong despite reading tons of other posts with similar situations, but different causal issues. I've looked at In React ES6, why does the input field lose focus after typing a character? and it seems like its almost always caused by rendering a form in a function inside render(). I don't quite know where that would be happening or how to avoid it.

I tried refactoring all of the components into the smallest pieces possible, keeping them all clumped as one. I tried adding keys to every element that would be effected, using an individual state hook that would track only the selected exercise and edit/display based off that rather than directly editing the single exercises state array. I expected this to be a solved problem, since I cannot imagine I am the first person to want to do something like this with a data structure similar to mine

2 Answers2

0

I'm just learning react, but I had a similar issue like yours where I needed to edit an array of objects.

Basically, I defined the functions for editing the data INSIDE the react component I was writing, in your case, GetProgram. Just move them out of GetProgram.

function editField(/** Inject your data and functions here**/data, handleDataChanges, props) {
  return(
    <>
      {data.map((item, index) => (
        <div key={`itemList.${index}`}>
          <h1>{item.name}</h1>
          <input type="text" name="inputText" value={item.value} onChange={(event) => handleDataChanges(item, index, event)}/>
        </div>
      ))};
    </>
  );
}

export default function GetProgram() {
  ...
}

When react re-renders the component, it will also re-render your functions, so it messes up the focus on the input.

Juan
  • 45
  • 1
  • 5
0

In react-functional if you "change the state" of an element, the nested elements might not exist if the function rendering them does not exist. They are immutable. Class instance or memo could solve this issue. Another, probably more functional-y, is to control the focus via the autoFocus property.

Naturally, it highly depends on your patterns of your application which solution you choose, but I personally gravitate towards a mix of paradigms. Since you are already thinking of your input array a discrete object with individual focus states, it only makes sense to code it as such. Naturally, all other elements nesting it will also have to become ... somewhat "permanent". They can still be functional if they are not called while editing the field.

Hope it helps.

Akos
  • 1
  • 1