skip to Main Content

Using react-bootstrap, I’m creating a "todo" list and I want to have a line drawn through the text when I check a checkbox. I suspect that my problem lies in trying to use a ref within a map function. I also don’t think I’m using state correctly here, either.

I’m learning aspects of both react and bootstrap, and I’ve been a little mixed up on how to use a ref with an array. Looking at some solutions for this online, I haven’t found a way to use a ref properly in an array like this while using react-bootstrap 2.7.2, with bootstrap 5.2.3.

I’ve tried using both useRef() and useRef([]) to start. Using the code below, nothing happens with the text when I check the box.

import 'bootstrap/dist/css/bootstrap.min.css';
import React, { useRef, useState } from 'react';
import Form from 'react-bootstrap/Form';
import FormCheck from 'react-bootstrap/FormCheck';
import './App.css';

function App() {
  const [state, setState] = useState({ labelChecked: false });
  const labelRef = useRef();

  const handleClick = event => {
    labelRef.current = event.target;
    if (state.labelChecked === false) {
      labelRef.current.style.textDecorationLine = 'line-through';
    } else {
      labelRef.current.style.textDecorationLine = 'none';
    }
    setState({ labelChecked: !state.labelChecked });
  };

  return (
    <div className="App">
      <Form style={{ margin: 10 }}>
        {['Todo 1', 'Todo 2', 'Todo 3'].map((todo, index) => {
          return (
            <Form.Check type="checkbox" id={todo} key={index} ref={labelRef}>
              <FormCheck.Input type="checkbox" onChange={handleClick} />
              <FormCheck.Label>{todo}</FormCheck.Label>
            </Form.Check>
          );
        })}
      </Form>
    </div>
  );
}

export default App;

Thanks!

2

Answers


  1. Controlled component pattern

    A straight forward solution is to move your checked state to the item level and not use refs at all. Its good practice to use controlled components so that your strikethrough doesn’t decouple from your check box’s native DOM state.

    import "bootstrap/dist/css/bootstrap.min.css";
    import React, { useState } from "react";
    
    import Form from "react-bootstrap/Form";
    import FormCheck from "react-bootstrap/FormCheck";
    
    import "./App.css";
    
    const FormCheckBox = ({ item, index }) => {
      const [checked, setChecked] = useState(false);
      const styles = { textDecorationLine: checked ? "line-through" : "none" };
      const toggleCheck = () => setChecked(!checked);
      return (
        <Form.Check type="checkbox" id={item}>
          <FormCheck.Input type="checkbox" onChange={toggleCheck} value={checked} />
          <FormCheck.Label style={styles}>{item}</FormCheck.Label>
        </Form.Check>
      );
    };
    
    function App() {
      return (
        <div className="App">
          <Form style={{ margin: 10 }}>
            {["Todo 1", "Todo 2", "Todo 3"].map((todo, index) => {
              return <FormCheckBox  key={index} item={todo} index={index} />;
            })}
          </Form>
        </div>
      );
    }
    
    export default App;
    
    

    It’s a common pattern in React design systems to treat the mapped item as its own component with a ({item, index}) interface. This allows you to use hooks in each item.

    Controlled component pattern with a single state object

    In order to get the state of all checkboxes, you can then move your checked state up to App and maintain it as a map or array of current values.

    Login or Signup to reply.
  2. The issue with your current code is that you are using the same ref (labelRef) for all the checkboxes. When you update the labelRef.current value inside the handleClick function, it gets updated for all the checkboxes, resulting in unexpected behavior.

    To fix this, you can create an array of refs using useRef([]) and store a ref for each checkbox. Then, in the handleClick function, you can use the index of the clicked checkbox to update the corresponding ref.

    Here’s an updated version of your code that should work as expected:

    import 'bootstrap/dist/css/bootstrap.min.css';
    import React, { useRef, useState } from 'react';
    import Form from 'react-bootstrap/Form';
    import FormCheck from 'react-bootstrap/FormCheck';
    import './App.css';
    
    function App() {
      const [state, setState] = useState({ checkedItems: [] });
      const labelRefs = useRef([]);
    
      const handleClick = index => {
        const checkedItems = [...state.checkedItems];
        checkedItems[index] = !checkedItems[index];
        setState({ checkedItems });
    
        const labelRef = labelRefs.current[index];
        if (checkedItems[index]) {
          labelRef.style.textDecorationLine = 'line-through';
        } else {
          labelRef.style.textDecorationLine = 'none';
        }
      };
    
      return (
        <div className="App">
          <Form style={{ margin: 10 }}>
            {['Todo 1', 'Todo 2', 'Todo 3'].map((todo, index) => {
              return (
                <Form.Check type="checkbox" id={todo} key={index}>
                  <FormCheck.Input type="checkbox" onChange={() => handleClick(index)} />
                  <FormCheck.Label ref={el => (labelRefs.current[index] = el)}>{todo}</FormCheck.Label>
                </Form.Check>
              );
            })}
          </Form>
        </div>
      );
    }
    
    export default App;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search