Search code examples
reactjsreact-hooksreact-bootstrap

How can I strike through text when checking a checkbox?


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!


Solution

  • 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.