0

To-Do List

I have a parent with a useState called tasks (array).
And for each of those items (task) in the array, I am displaying a component with the item data and a delete button.

Now each component(child) has a delete button. But the array with the tasks is in the parent class so I can't delete the task or that component as the button is inside the component (child).

Any solutions for this problem?

Note: I have tried to find a solution but I can't seem to find it. SO just tell me what to do, the whole code is here.

The parts that need help are commented

Parent:

import React, { useState, useEffect } from "react";
import {
  Keyboard,
  KeyboardAvoidingView,
  Platform,
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from "react-native";
import Task from "./components/Task";

export default function App() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState();

  const AddTask = () => {
    Keyboard.dismiss();
    if (newTask == null) return;
    setTasks([...tasks, { name: newTask }]);
    setNewTask(null);
  };

  const removeTodo = (index) => {
    let CopyLi = [...tasks];
    CopyLi.splice(index, 1);
    setTasks(CopyLi);
  };

  return (
    <View style={styles.container}>
      <View style={styles.taskWrapper}>
        <Text style={styles.sectionTitle}>Today's task</Text>

        <ScrollView style={styles.items}>

          {/* Here is the Rendering of Child Component */}

          {tasks.map((item, index) => {
            return <Task key={index} text={item.name} />;
          })}
        </ScrollView>
      </View>

      <KeyboardAvoidingView
        behavior={Platform.OS === "ios" ? "padding" : "height"}
        style={styles.inputWrapper}
      >
        <TextInput
          style={styles.input}
          placeholder={"Add a new Task"}
          value={newTask}
          onChangeText={(text) => setNewTask(text)}
        />

        <TouchableOpacity onPress={() => AddTask()}>
          <View style={styles.btnWrapper}>
            <Text style={styles.btnIcon}>+</Text>
          </View>
        </TouchableOpacity>
      </KeyboardAvoidingView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#e8eaed",
  },
  taskWrapper: {
    paddingTop: 80,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: "bold",
    marginHorizontal: 20,
  },
  items: {
    marginTop: 30,
    height: "78%",
  },
  inputWrapper: {
    width: "100%",
    flexDirection: "row",
    justifyContent: "space-around",
    alignItems: "center",
    height: 100,
  },
  input: {
    paddingVertical: 15,
    paddingHorizontal: 20,
    backgroundColor: "#fff",
    borderRadius: 60,
    borderColor: "#C0C0C0",
    borderWidth: 1,
    width: "70%",
  },
  btnWrapper: {
    width: 60,
    height: 60,
    backgroundColor: "#fff",
    borderRadius: 60,
    borderColor: "#C0C0C0",
    borderWidth: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  btnIcon: {
    fontSize: 30,
    transform: [{ translateY: -2 }],
    color: "#A4A4A4",
  },
});

Child:

import React, { useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  CheckBox,
  TouchableOpacity,
} from "react-native";

const Task = (props) => {
  const [toggle, setToggle] = useState(false);

  return (
    <View style={styles.item}>
      <View style={styles.itemLeft}>
        <CheckBox
          value={toggle}
          onValueChange={setToggle}
          style={[{ textDecorationLine: "line-through" }, styles.square]}
        />
        <Text
          style={[styles.itemText, toggle ? styles.checked : styles.unchecked]}
        >
          {props.text}
        </Text>
      </View>
      <TouchableOpacity style={styles.itemRight}></TouchableOpacity>

      {/*Here is the Button/TouchableOpacity to delete this component */}

    </View>
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: "#fff",
    padding: 15,
    borderRadius: 10,
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    marginBottom: 20,
    width: "90%",
    alignSelf: "center",
  },
  itemLeft: {
    flexDirection: "row",
    alignItems: "center",
    flexWrap: "wrap",
  },
  square: {
    marginRight: 10,
    transform: [{ scaleX: 1.2 }, { scaleY: 1.2 }],
  },
  itemText: {
    maxWidth: "80%",
    fontSize: 16,
  },
  checked: { textDecorationLine: "line-through" },
  unchecked: { textDecorationLine: "none" },
  itemRight: {
    width: 15,
    height: 15,
    borderColor: "#f94355",
    borderWidth: 2,
    borderRadius: 10,
    backgroundColor: "#ff2848",
  },
});

export default Task;
Naman
  • 284
  • 1
  • 3
  • 17
  • 2
    Does this answer your question? [How to pass data from child component to its parent in ReactJS?](https://stackoverflow.com/questions/38394015/how-to-pass-data-from-child-component-to-its-parent-in-reactjs) – wowandy Oct 12 '21 at 17:22

3 Answers3

0

Firstly, in Parent component create remove function, and passed to child component, then use remove function in child component and pass index. For example, your code look like this:

import React from 'react';

function Todo({ todo, index, removeTodo }) {
    return (
        <div className="todo">
            <span>{todo.text}</span>
            <button onClick={() => removeTodo(index)}>Delete</button>
        </div>
    );
};

function App() {
    const [todos, setTodos] = React.useState([
        { text: "Learn about React" },
        { text: "Meet friend for lunch" },
        { text: "Build really cool todo app" }
    ]);

    const removeTodo = (index) => {

        let todosArrCopy = [...todos];

        todosArrCopy.splice(index,1);

        setTodos(todosArrCopy);
    }

    return (
        <div className="app">
            <div className="todo-list">
                {todos.map((todo, index) => (
                    <Todo
                        key={index}
                        index={index}
                        todo={todo}
                        removeTodo={removeTodo}
                    />
                ))}
            </div>
        </div>  
    );
}

export default App;
Tabish Adnan
  • 104
  • 4
0

You must declare the method called by the child component delete button in the parent, and pass it to the child component as a prop. The main app.js file should be something like:

import './App.css';
import ChildComponent from './ChildComponent';
import { useState } from 'react';

const App = () => {
  const [todoList, setTodoList] = useState(["task 0","task 1","task 2","task 3","task 4"])
  
  const deleteTask = (tobedeleted) => {
    let todos = todoList;
    setTodoList(todos.filter((todo) => {return todo !== tobedeleted}))

}
  
return (
    <div className="App">
      <ul>
        {todoList.map(
          function(todo, idx) {
            return (<li key={idx}>{todo} <ChildComponent handleDelete={() => deleteTask(todo)} /></li>)
          }
        )}
      </ul>
    </div>
  );
}

export default App;

While your child component would be:

const ChildComponent = (props) => {
    return (
        <button  
            onClick={props.handleDelete}
        >
            Delete Task
        </button>
    );
}

export default ChildComponent;
pls78
  • 11
  • 4
  • A good attempt but its not working. The function got called as soon as the component is made. – Naman Oct 13 '21 at 07:50
  • You are right! I modified the code in order to make it run correctly. Basically I was passing the return value of the deleteTask function to the child component and not its reference! – pls78 Oct 13 '21 at 11:41
0

Traditionally you would pass the function into the child component as a prop on the child.

However, you need to consider the implications of the deletion and how it impacts other items in the list. In order to stop unnecessary re-renders of your child tasks whenever a task is deleted, you need to introduce useCallback and React.memo.

React.memo is a higher order component.

If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

useCallback returns a memoized callback.

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

Without these, you will be passing in a new function to the child every time state changes in your parent and this will result in all the children rendering again. This may be undesirable with larger lists.

Demo:

const {
  useState,
  memo,
  useCallback
} = React;

// we use memo here to return a optimized version of our higher order component. 
// We compare the task values between renders to determine if we should render again.
const Task = memo(({task, deleteTask}) => {
  const handleClick = (e) => {
    const {value} = e.currentTarget;
    deleteTask(value);
  }
  
  return ( 
    <button value={task} onClick={handleClick}>{task} <br/>({Date.now()})</button>
  )
}, (prevProps, nextProps) => prevProps.task.toUpperCase() === nextProps.task.toUpperCase());

// dummy data
const data = Array(20).fill(null).map((i, index) => `task ${(index + 1).toString()}`);

function App() {
  const [tasks, setTasks] = useState(data);
  
  // wrap the function in useCallback to memoized version of the callback
  const deleteTask = useCallback((task) => {
    setTasks(prevState => prevState.filter((x) => x !== task));
  }, [setTasks]);

  return ( 
    <div> 
      {tasks.map((task) => (
        <Task key={task} task={task} deleteTask={deleteTask} />
      ))} 
    </div>
  );
}

ReactDOM.render( <
  App / > ,
  document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>

<div id="app"></div>
Samuel Goldenbaum
  • 18,391
  • 17
  • 66
  • 104