1

So I got this component:

export default function () {
    const [todos, setTodos] = useState([]);


    useEffect(() => {
        function populateTodos () {
            axios.get(`http://localhost:8000/api/all-todos`)
                .then(res => setTodos(res.data))
                .catch(err => console.log(err));
        }

        populateTodos();
    }, []);

    console.log(todos);

    return (
        <div>
            ...
        </div>
    );
}

I am using the useEffect hook to fetch all the todos from the database, and it works fine. The problem is that I don't know how to use useEffect to trigger a rerender anytime I make a modification to the todos array, like adding or deleting or updating. If I supply the useEffect's dependency array with the todos variable I get an infinite loop logging in the console. How can I use useEffect to trigger a rerender anytime the todos array gets updated?

norbitrial
  • 14,716
  • 7
  • 32
  • 59
code_dude
  • 951
  • 2
  • 15
  • 28
  • I think if you add `{todos.length}` for example between your `
    ` elements, it will show you first `0` then it rerender once `setTodos` is changing the value of `todos` state and representing the next length.
    – norbitrial Mar 16 '20 at 16:34
  • It doesn't work. Thanks. – code_dude Mar 16 '20 at 16:38
  • By any chance do you have any error on the console? – norbitrial Mar 16 '20 at 16:39
  • No. I have no error. – code_dude Mar 16 '20 at 16:41
  • I can,t put a variable inside the dependency array if I hadn't used that variable inside that useEffect – code_dude Mar 16 '20 at 16:42
  • 1
    Can you show what you mean by "make a modification to todos array"? Maybe the issue is there, and not in the code given. – Brian Thompson Mar 16 '20 at 16:42
  • Can you show the code that does the modifications? My guess is that you are not using `setTodos` for that? – Zoran Mar 16 '20 at 16:42
  • This is part of a bigger CRUD app, where by a modification I mean adding a new todo on the database. Then in the code above I fetch with axios all the todos from the database, at the /all-tasks endpoint. So anytime I add a new todo, the useEffect should rerender, hence displaying the new state(todos) – code_dude Mar 16 '20 at 16:45
  • You will need a custom hook - something like https://plnkr.co/edit/NuSBnLpPpoOmmEE6?preview , This uses localStorage but you can modify this to use an API as well – muddassir Mar 16 '20 at 17:00
  • Your first problem is that you need a way to inform this component that the todos have been changed at a database level. Are you doing this in another component, perhaps by sending AJAX requests to an endpoint? – GBWDev Mar 16 '20 at 17:02
  • Is there any place in your app that "knows" when the data in the DB been updated? – Sagiv b.g Mar 16 '20 at 17:02
  • To answer to GBWDev's and Sagiv b.g's questions. Yes, I have other components that does exactly that. That is not the proble. The database gets informed and updated accordongly. The thing is that if I write the dependency array like this: [todos], it works exactly how I want it to work, but it loops infinitely as well. – code_dude Mar 16 '20 at 17:07
  • Just to make it clerer. When I say it looping infinitely I mean that it keeps rerender on and on, printing that console.log every time. – code_dude Mar 16 '20 at 17:12
  • 1
    In that case, you'll need to centralize the state. There are several ways to do that: lifting the state to the parent component, using the context API, setting up a store management library (like Redux)... – Zoran Mar 16 '20 at 17:16
  • ZORAN, could you expand on your idea a bit? What is the reasoning behind your suggestion? – code_dude Mar 16 '20 at 17:29

2 Answers2

2

The problem is there is no logic inside the useEffect see this code bellow

    const [todos, setTodos] = useState([]);

    useEffect(() => {
        setTodos([1])
    }, [todos])

This will also give an infinite loop. but we are always giving the same value. The problem is when it gets updated the dependency is true so it again starts executing the useEffect(). You have to come up with some concrete logic like length is changed or you can take a new state which is like this bellow

    const [todos, setTodos] = useState([]);
    const [load, setLoad] = useState(false);

    useEffect(() => {
        function populateTodos () {
            axios.get(`http://localhost:8000/api/all-todos`)
                .then(res => setTodos(res.data))
                .catch(err => console.log(err));
        }

        populateTodos();
    }, [load])

    console.log(todos)
    return (
        <div>
            <button
            onClick={() => {
                todos.push(1)
                setLoad(!load)
            }}
            >cilck</button>
        </div>
    );
moshfiqrony
  • 4,303
  • 2
  • 20
  • 29
  • Md. Moshfiqur Rahman Rony, thanks for your response but your solution leads to the same issue. You are lying to the useEffect dependency that way by specifying a variable outside of its scope. To use load as dependency you have to use it first inside the useEffect. – code_dude Mar 16 '20 at 17:32
  • useEffect will only be called when the load is changed and changing load is in your control because it will not never ever needs to change inside useEffect. If you are still having the same issue can you create a codesandbox? here is my code working fine – moshfiqrony Mar 16 '20 at 17:43
  • Just change the load when you are deleting, adding and updating – moshfiqrony Mar 16 '20 at 17:44
0

You could lift the state by fetching todos in a parent component, then passing the result as a prop to a child component whose useEffect is dependent on it. Any CRUD operations must be called within the parent component in order to update the list (but you can trigger the modifications from the child components by passing those functions to the child).

Otherwise, this would also be a good application for Redux. You would be able to initialize the component with a Redux fetch action to populate a store with all CRUD operations done on any component also updating the store via API response modifying the reducer. Then you can use the store as a useEffect dependency to update your component's local todo state.

// State
const [todos, setTodos] = useState(null);
// Redux Stores 
const todoStore = useSelector(state => state.todoStore);
// Redux Dispatch
const dispatch = useDispatch();
// Lifecycle: initialize the component's todolist
useEffect(() => {
    if (!todos) {
        // your Redux Action to call the API fetch & modify the store
        dispatch(fetchTodos); 
    }
}, [dispatch, todos]
// Lifecycle: update the todolist based on changes in the store
// (in this case I assume the data is populated into todoStore.items)
useEffect(() => {
    if (todoStore.items.length > 0) {
        setTodos(todoStore);
    }
}, [todoStore.items]

return {
    <div>{ todos.map((todo, index) => <li key={'todo_'+index}>{todo}</li>) }</div>
}
bilwit
  • 799
  • 1
  • 5
  • 9