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.