Betsy
Betsy

Reputation: 93

Focus on input is lost after state change

My input box is losing focus everytime I rerender and update state. I'm wondering how to retain the focus on the textbox until the user clicks away.

My application takes an input for material cost of each product and calculates the total of that product. I would like the component to be rerendered to show the updated total after the input is updated. It seems that the total is calculated and the state is updating every time the material cost is changed, but the user has to manually reselect the input box for every time because the focus is gone.

Here's a screenshot of my application (some code for elements in the table were removed for simplicity)

enter image description here I'm not sure this is necessary for the problem, but this is what the data looks like. In this example I am saving the first element of the array.

export let data = 
    {
        name: "Name",
        description:
            "",
        products: [
            {
                id: 1,
                name: "Name 1",
                material: 1.05,
                time: 25,
                total: 0,
            },
            {
                id: 2,
                name: "Name 2",
                material: 3,
                time: 252,
                total: 0,
            },
        ],
    }

This is the parent component. It has the main render and the setTotal and setMaterial functions. The render maps through each of the productData.products so the child component can render them.

The setTotal and setMaterial functions are almost identical (I'm sure there's a way to refactor that). The functions are both called after the user types a number within the element.


function CompareCard({ type, labor }) {

    const [productData, setProductData] = useState(data);

    const setTotal = (id, total) => {
        const productPrevious = productData.products.find(function (rec) {
            return rec.id === id;
        });

        const productUpdated = {
            ...productPrevious,
            total: total,
        };

        const productNew = productData.products.map(function (rec) {
            return rec.id === id ? productUpdated : rec;
        });

        const productFullNew = {
            ...productData,
            products: productNew,
        };0

        //calculate new final totals
        let newFinalTotal = 0;
        let newFinalMaterial = 0;
        let newFinalTime = 0;
        productFullNew.products.map((product) => {
            newFinalTotal += product.total;
            newFinalMaterial += product.material;
            newFinalTime += product.time;
        });

        productFullNew.finalTotal.total = newFinalTotal;
        productFullNew.finalTotal.material = newFinalMaterial;
        productFullNew.finalTotal.time = newFinalTime;

        setProductData(productFullNew);
    };

    const setMaterial = (id, materialCost) => {
        const productPrevious = productData.products.find(function (rec) {
            return rec.id === id;
        });

        const productUpdated = {
            ...productPrevious,
            material: materialCost,
        };

        const productNew = productData.products.map(function (rec) {
            return rec.id === id ? productUpdated : rec;
        });

        const productFullNew = {
            ...productData,
            products: productNew,
        };

        setProductData(productFullNew);
    };
    return (
        <div className="Card">
            <h3> {productData.name} </h3>
            <p>{productData.description}</p>
            <table className="table">
                <thead>
                    <tr>
                        
                        <th scope="col">
                            <p>
                                <b> Material </b>
                            </p>
                        </th>
                        
                        <th scope="col">
                            <p>
                                <b> Labor </b>
                            </p>
                        </th>
                        <th scope="col">
                            <p>
                                <b> Total</b>
                            </p>
                        </th>
                    </tr>
                </thead>
                <tbody id={`${productData.name}`}>
                    {productData.products.map((product) => {
                        return (
                            <Products
                                key={product.id}
                                product={product}
                                labor={3}
                                setMaterial={setMaterial}
                                setTotal={setTotal}
                            />
                        );
                    })}
                </tbody>
            </table>
        </div>
    );
}

export default CompareCard;

This is the child element. It handles each product individually.

The element lives here.

import React, { useState, useEffect } from "react";
function Products({ product, labor, setMaterial, setTotal }) {
    function setMaterialState(newMaterial) {
        product.material = newMaterial;
    }

    function calcTotal() {
        product.total = labor + product.material;
    }
    

    if (product.total === 0) {
        calcTotal();
    }

    useEffect(() => {
        setTotal(product.id, product.total);
    }, [product.total]);

    useEffect(() => {
        setMaterial(product.id, product.material);

        //setTotal(product.id, product.material + labor);
    }, [product.material]);

    function ProductMaterial() {
        const [name, setName] = useState("");

        function handleChange(e) {
            setName(parseFloat(e.target.value));
            setMaterialState(parseFloat(e.target.value));
            setMaterial(product.id, product.material);
            calcTotal();
            setTotal(product.id, product.total);
        }

        return (
            <input
                type="number"
                name="firstName"
                onChange={handleChange}
                value={product.material}
            />
        );
    }

    return (
        <tr key={product.name}>
            
            <td>
                <ProductMaterial product={product} />{" "}
    
            <td id={`total-${product.id}`}>{product.total}</td>
        </tr>
    );
}

export default Products;

Upvotes: 1

Views: 1285

Answers (1)

Betsy
Betsy

Reputation: 93

I found a similar stackoverflow question answered by Sergi Juanati that worked for me In React ES6, why does the input field lose focus after typing a character?

I switched my function that holds the input return statement to this syntax:

const Products = ({ product, labor, setMaterial, setTotal }) => {};

and the call to the function to this:

{ProductMaterial({ product: product })}

Upvotes: 2

Related Questions