Taylor
Taylor

Reputation: 49

Why isn't my useState hook working in this React component?

I've been stuck on this all day, and I'd appreciate some help. Thanks in advance.

At first I was writing it like this, but I'd get a type error: todos.map is not a function.

function toggleState() {
  setTodos(state => ({ ...state, isComplete: !state.isComplete }))
}

Finally I realized that error was because it was returning todos as an object, so I tried this:

function toggleState() {
        setKeywords(state => [{ ...state, isUsed: !state.isUsed }])
    }

Now I'm not getting the type error, but it's still not working as expected. Here's the state before toggleState:

[
  {
    "name": "State",
    "value": [
      {
        "todo": "Learn React",
        "id": "91bad41d-1561-425a-9e77-960f731d058a",
        "isComplete": false
      }
    ]

and here's state after:

[
  {
    "name": "State",
    "value": [
      {
        "0": {
          "todo": "Learn React",
          "id": "91bad41d-1561-425a-9e77-960f731d058a",
          "isComplete": false
        },
        "isComplete": true
      }
    ]

Here's the rest of my code:

import React, { useState, useEffect } from 'react'
import { uuid } from 'uuidv4'
import { Form, FormGroup, Input, Button } from 'reactstrap'

function Example(props) {
    const [todos, setTodos] = useState([])

    // Run when component first renders
    useEffect(() => {
        console.log('useEffect component first rendered')
        if (localStorage.getItem('todoData')) {
            setTodos(JSON.parse(localStorage.getItem('todoData')))
        }
    }, [])

    // Run when todos state changes
    useEffect(() => {
        console.log('useEffect todos changed')
        localStorage.setItem('todoData', JSON.stringify(todos))
    }, [todos])

    const [formInput, setFormInput] = useState()

    function handleChange(e) {
        setFormInput(e.target.value)
    }

    function handleSubmit(e) {
        e.preventDefault()
        setTodos(prev => prev.concat({ todo: formInput, id: uuid(), isComplete: false }))
        setFormInput('')
    }

    function toggleState() {
        setTodos(state => [{ ...state, isComplete: !state.isComplete }])
    }

    return (
        <div className='text-center'>
            <div className='mb-2 border text-center' style={{ height: '300px', overflowY: 'scroll' }}>
                {todos.map(todo => (
                    <p className={todo.isUsed ? 'text-success my-1' : 'text-danger my-1'} key={todo.id}>
                        {todo.todo}
                    </p>
                ))}
            </div>
            <Form onSubmit={handleSubmit}>
                <FormGroup>
                    <Input onChange={handleChange} type='text' name='text' id='todoForm' placeholder='Enter a todo' value={formInput || ''} />
                    <Button>Set Todo</Button>
                </FormGroup>
            </Form>
            <Button onClick={toggleState}>Toggle isComplete</Button>
        </div>
    )
}

export default Example

Upvotes: 0

Views: 1524

Answers (2)

user2340824
user2340824

Reputation: 2152

So in your specific case, where you just want to toggle the isComplete on the first item, could be achieved like this:

  function toggleState() {
    setTodos(([firstItem, ...remainder]) => {
      return [
        {
          ...firstItem,
          isComplete: !firstItem.isComplete
        },
        ...remainder
      ];
    });
  }

Where we use Destructuring assignment to get the FirstItem and manipulate that, and spread the reminder back into the state.

Upvotes: 1

codingwithmanny
codingwithmanny

Reputation: 1184

The method that I end up using, and I've seen other developers do, is to copy the object or state first, do modifications to it, and then set the new state with the modified state.

I also noticed you need to provide an index for the todos to be able to toggle them, so I added that functionality.

Take a look at a working example, click "Run code snippet" below.

// main.js

// IGNORE THIS BECAUSE THIS IS JUST TO USE REACT IN STACK OVERFLOW
const { useEffect, useState } = React;

// ---- CODE STARTS HERE -----
const Example = (props) => {
    const [todos, setTodos] = useState([]);
    const [formInput, setFormInput] = useState('');

    // Run when component first renders
    useEffect(() => {
        /*
        // Uncomment - Just doesn't work in Stack Overflow
        if (localStorage && localStorage.getItem('todoData')) {
            setTodos(JSON.parse(localStorage.getItem('todoData')));
        }
        */
    }, []);
    
    // Hooks
    const handleChange = event => {
      setFormInput(event.target.value);
    };
    
    const handleSubmit = event => {
      const newTodosState = [...todos ]; // make copy
      newTodosState.push({ todo: formInput, isComplete: false });
      setTodos(newTodosState);
      
      // Add functionality to update localStorage
      // ex:
      // localStorage.setItem('todoData', newTodosState);
      
      // Reset form
      setFormInput('');
      
      event.preventDefault();
    };
    
    const toggleTodoState = index => event => {
      const newTodosState = [...todos ]; // make copy
      newTodosState[index].isComplete = !newTodosState[index].isComplete;
      setTodos(newTodosState);
      
      // Add functionality to update localStorage
    };
    
    const handleDelete = index => event => {
      const newTodosState = [...todos.slice(0, index), ...todos.slice(index + 1) ];
      setTodos(newTodosState);
      
      // Add functionality to update localStorage
    }
    
    // Render
    return (<div>
      <h3>Todos</h3>
      <ul>
        {todos.map((item, index) => <li key={`todo-${index}`}>{item.todo} - <input type="checkbox" checked={item.isComplete} onClick={toggleTodoState(index)} /> - <button onClick={handleDelete(index)}>Delete</button></li>)}
      </ul>
      <hr />
      <form onSubmit={handleSubmit}>
        <input type="text" value={formInput} onChange={handleChange} placeholder="Enter todo name" />
        <button type="submit">Add</button>
      </form>
    </div>);
};

ReactDOM.render(<Example />, document.querySelector('#root'));
<body>
<div id="root"></div>

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<script type="text/babel" src="main.js"></script>
</body>

Upvotes: 1

Related Questions