简体   繁体   中英

How do I use an updated value in useEffect

I'm new to Javascript and I'm currently experimenting with the Demo application out of the Docker getting-started tutorial . The application is a simple Todo list where you can add items and remove them.

I'm trying to update the list on every instance of the page without having to reload the page. I've managed to edit the node express server so that it sends updates via Server-sent events.

The problem: The frontend uses React. The data of the currently displayer items is contained in the ìtems array. onNewItem adds items to that array. However when onNewItem is called from onmessage the items array is null even though it's not null when onNewItem is called from other React components. How can I access the initialized version of the items array? (It gets initialized by the 1. useEffect which fetches items from the server)

Below is a part of the code

function TodoListCard() {
const [items, setItems] = React.useState(null);

const [ listening, setListening ] = React.useState(false);

React.useEffect(() => {
    fetch('/items')
        .then(r => r.json())
        .then(setItems);
}, []);

React.useEffect( () => {
    if (!listening) {
    const events = new EventSource('/events/subscribe');
    events.onmessage = (event) => {
        const parsedData = JSON.parse(event.data);
        switch (parsedData.type) {
            case "add":
                var newItem = {id: parsedData.id, name: parsedData.name, completed: parsedData.completed};
                onNewItem(newItem);
                break;
            default:
                break;
        }
    };

    setListening(true);
    }
}, [listening]);

const onNewItem = React.useCallback(
    newItem => {
        if (items.some(e => e.id === newItem.id)){return;}
        setItems([...items, newItem]);
    },
    [items],
);

Let's start of by why things are going wrong. The issue is that when you call onNewItem(newItem) you are using an outdated reference to the onNewItem . For this reason items within the function will still be set to the initial value.

You partially solved this by providing an dependency array to React.useCallback . This will update onNewItem when a new value of items is available. However since React.useEffect does not list onNewItem as a dependency it keeps using the old version of onNewItem .

With this being said you might consider adding onNewItem , to the dependency array of React.useEffect . Although this is the correct action, just adding this to dependency array is not enough.

What is the problem you get when you add onNewItem to the depency array of React.useEffect ? There is no cleanup function, so you will subscribe to the channel multiple times with different onmessage handlers (different versions of onNewItem ).

So taking all the above into account a solution might look something like this:

function TodoListCard() {
  const [items, setItems] = React.useState(null);
  const [events, setEvents] = React.useState(null);

  React.useEffect(() => {
    const pEvents = fetch('/items')
      .then(r => r.json())
      .then(setItems)
      .then(() => new EventSource('/events/subscribe'));

    pEvents.then(setEvents);

    return () => pEvents.then(events => events.close());
  }, []);

  React.useEffect(() => {
    if (!events) return;
    events.onmessage = (event) => {
      const parsedData = JSON.parse(event.data);
      switch (parsedData.type) {
        case "add":
          var newItem = {
            id: parsedData.id,
            name: parsedData.name,
            completed: parsedData.completed
          };
          onNewItem(newItem);
          break;
        default:
          break;
      }
    };
  }, [events, onNewItem]);

  const onNewItem = React.useCallback(newItem => {
    const isPresent = items.some(item => item.id === newItem.id);
    if (isPresent) return;
    setItems([...items, newItem]);
  }, [items]);

  return (
    // ...
  );
}

I've moved the EventSource creation inside the first React.useEffect since that only needs to happen once the component is mounted (and needs to close the connection on unmount). An empty dependency array will only call the function on mount, and calls the cleanup function on unmount.

The second React.useEffect now has the dependency array [events, onNewItem] , because when events is set the onmessage handler needs to be attached. And if the onNewItem callback updates to a new version you should attach it as the new onmessage handler (replacing the old handler). This doesn't need a cleanup function anymore since, opening and closing events is already handled.

Although the above should do the job. If managing a specific state is becoming more complicated it might be better to opt for useReducer instead of useState .

function reducer(items, action) {
  switch (action.type) {
    case "add":
      const isPresent = items.some(item => item.id == action.item.id);
      if (isPresent) return items;
      return [...items, action.item];

    case "replace all":
      return action.items;

    case "complete": // <- unused example case
      return items.map(item => {
        if (item.id != action.id) return item;
        return {...item, completed: true};
      });

    // ...

    default: // silently ignore unsupported operations
      return items;
  }
}

function TodoListCard() {
  const [items, dispatch] = React.useReducer(reducer, null);

  React.useEffect(() => {
    const pEvents = fetch('/items')
      .then(r => r.json())
      .then(items => dispatch({type: "replace all", items}))
      .then(() => new EventSource('/events/subscribe'));

    pEvents.then(events => {
      events.onmessage = (event) => {
        const {type, ...item} = JSON.parse(event.data);
        dispatch({type, item});
      };
    });

    return () => pEvents.then(events => events.close());
  }, []);

  // if you still need onNewItem for your render:
  const onNewItem = React.useCallback(item => {
    dispatch({type: "add", item});
  }, []);

  return (
    // ...
  );
}

The above extracts all the items management logic into a "reducer" function. The dispatch function returned by useReducer is guaranteed to be stable by React, so you can omit it from dependency arrays (but you don't have to).

The error that you have done is you are not getting any data from your api.The setItems in your first useEffect won't work.

Wrong Way:

React.useEffect(() => {
    fetch('/items')
        .then(r => r.json())
        .then(setItems);
}, []);

Right Way:

useEffect(() => {
      fetch('/items')
          .then(r => r.json())
          .then((result) => {
            setItems(result.items)
          });
  }, []);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM