I have a screen that receives notifications from the Spring Boot backend, and I show them in a bell. When deleting a notification it deletes it well, but when another new notification arrives it loads the ones that I had already deleted.
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';
// core components
const HeaderNotificacions = () => {
const [chipData, setChipData] = useState([]); //Hook where I load the notifications that come from the backend >
const historyAlerts = localStorage.getItem('notys')
? JSON.parse(localStorage.getItem('notys'))
: []; if (chipData.length === 0 && historyAlerts.length !== 0) { //I get the notifcations when I reload the browser
setChipData(historyAlerts); }
useEffect(() => {
var sock = new SockJS(
`${process.env.REACT_APP_WEB_SOCKET}mocaConsola/api/notifications`
);
let stompClient = Stomp.over(sock);
sock.onopen = function () {
/* console.log('open'); */
};
stompClient.connect({}, function (frame) {
stompClient.subscribe('/ws/alertNotification', function (greeting) {
if (stompClient !== null) {
stompClient.disconnect();
}
setChipData([
...chipData,
{
key: greeting.headers['message-id'],
label: JSON.parse(greeting.body).content,
},
]);
});
}); }, [chipData]);
localStorage.setItem('notys', JSON.stringify(chipData));
const handleDelete = (chipToDelete) => () => {
const historyAlerts = localStorage.getItem('notys') //function to delete a notification
? JSON.parse(localStorage.getItem('notys'))
: [];
setChipData((chips) =>
chips.filter((chip) => chip.key !== chipToDelete.key)
);
const local = historyAlerts.filter((chip) => chip.key !== chipToDelete.key);
localStorage.setItem('notys', JSON.stringify(local)); };
One of the problems could be that you do not disconnect from the socket, so first subscription (having initial value of chipData in closure) brings it back. Unsubscribing on effect's clean up could help, similar to:
useEffect(() => {
/* your code */
> stompClient.connect({}, function (frame) {
> subscription = stompClient.subscribe('/ws/alertNotification', function (greeting) {
> if (stompClient !== null) {
> stompClient.disconnect();
> }
>
> setChipData([
> ...chipData,
> {
> key: greeting.headers['message-id'],
> label: JSON.parse(greeting.body).content,
> },
> ]);
> });
> });
return () => subscription && subscription.unsubscribe();
}, [chipData]);
Also for performance considerations we can skip recreation of the connection/subscription each time we update chipData. We can use callback version of setChipData's argument which refer to latest value of state.
setChipData(prevData => [
> ...prevData,
> {
> key: greeting.headers['message-id'],
> label: JSON.parse(greeting.body).content,
> },
> ]);
so we can replace [chipData] to [] as second argument of useEffect and open connection only once per component load.
Related
I have two different endpoints, one that is called with getProjectMapping and one with getStaffing. The getProjectMapping query must be run first in order to set the project variable, which will then be used to make the getStaffing request. But I get the following error:
Uncaught TypeError: project is null
I get that error in the getStaffing request, although before activating it I check that the project is not null. Does anyone know what is wrong?
const Staffing = () => {
const { tokenApi } = useContext(LoginContext);
const [project, setProject] = useState(null);
const {
data: projectMapping,
isLoading: projectMappingIsLoading,
isFetching,
} = useQuery("ProjectMapping", () => getProjectMapping(tokenApi), {
onSuccess: () => {
if (projectMapping != null && projectMapping.length !== 0) {
setProject(projectMapping[0]);
}
},
});
const { data, isLoading } = useQuery(
[project.value, "Staffing"],
() => getStaffing(project.value, tokenApi),
{
enabled: !isFetching && project != null,
dependencies: [project],
}
);
}
This isn't how you structure dependent queries.. Instead of setting state you should derive it. If you have dependent queries it might also make sense to wrap them in a custom hook
e.g.
const useProjectStaffing = (tokenApi) => {
const {
data: [project] = [],
isLoading: projectMappingIsLoading,
} = useQuery("ProjectMapping", () => getProjectMapping(tokenApi), {
},
});
const projectValue = project && project.value
return useQuery(
[projectValue, "Staffing"],
() => getStaffing(projectValue, tokenApi),
{ enabled: !!projectValue }
);
}
const Staffing = () => {
const { tokenApi } = useContext(LoginContext);
const {isLoading, data: staffing} = useProjectStaffing(tokenApi);
// ... do stuff with the staffing data when it comes back.
I have a markets screen where I am using sockets to update the prices of cryptocurrencies in real time. The screen contains an infinite scroller, so when the user scrolls, more cryptocurrencies load and the coins being observed by the socket changes as well. However I am noticing as the coins list is increasing, the app becomes really slow and I cannot navigate to other screens or click anywhere quickly.
I have seen a few apps achieve this infinite-scroll-live-prices logic such as CoinGecko & CoinMarketCap.
Snippet of the relevant code:
const updatePriceOfCoins = (newPrices = {}, coins = []) => {
const updatedCoins = [...coins];
let wasUpdated = false;
for (let i = 0; i < updatedCoins.length; i++) {
let coin = updatedCoins[i];
if (newPrices[coin.id] !== undefined) {
updatedCoins[i] = { ...coin, priceUsd: newPrices[coin.id] };
wasUpdated = true;
}
}
return { wasUpdated, coins: updatedCoins };
};
const MarketsScreen = ({
markets,
getMarkets,
isLoading,
isLoadingMore,
perPage,
getMoreMarkets,
hasMore,
updateMarkets
}) => {
const socket = useLivePrices(markets);
const marketsRef = useRef(markets);
useEffect(() => {
marketsRef.current = markets;
}, [markets]);
const onNewPrices = (newPrices) => {
const { wasUpdated, coins: updatedMarkets } = updatePriceOfCoins(newPrices, marketsRef.current);
if (wasUpdated) {
updateMarkets(updatedMarkets);
}
};
useEffect(() => {
getMarkets();
}, []);
useEffect(() => {
if (socket !== null) {
socket.on("new prices", onNewPrices);
}
return () => {
if (socket !== null) {
socket.off("new prices");
}
};
}, [socket]);
return (
<FlatList
data={data}
renderItem={renderDataItem}
showsVerticalScrollIndicator={false}
onEndReached={getMoreMarkets}
onEndReachedThreshold={0.5}
/>
);
};
useLivePrices hook
const useLivePrices = (coinsToWatch = []) => {
const [socket, setSocket] = useState(null);
const prevCommaSepCoins = useRef("");
useEffect(() => {
//Only initialize socket once then everytime coinsToWatch is different
//update the coins observed
if (coinsToWatch.length > 0) {
if (socket === null) {
const commaSepCoins = coinsToCommaSepIDs(coinsToWatch);
setSocket(connectToLivePricesSocket(commaSepCoins));
prevCommaSepCoins.current = commaSepCoins;
} else {
const newCommaSepCoins = coinsToCommaSepIDs(coinsToWatch);
if (prevCommaSepCoins.current !== newCommaSepCoins) {
socket.emit("update coins", newCommaSepCoins);
prevCommaSepCoins.current = newCommaSepCoins;
}
}
}
}, [coinsToWatch]);
useEffect(() => {
let unsubFocus = () => {};
let unsubBlur = () => {};
if (socket !== null) {
//pause and resume prices based on if screen is in focus
unsubFocus = navigation.addListener("focus", resumePrices);
unsubBlur = navigation.addListener("blur", pausePrices);
}
return () => {
if (socket !== null) {
socket.disconnect();
unsubFocus();
unsubBlur();
}
};
}, [socket]);
return socket;
};
I want to achieve the infinite-scroll-live-prices but not sure how to optimize the performance anymore.
I tried optimizing the performance by reducing the number of renders when price updates. I have also tried to pause and resume the socket based on if the screen is focused so that state updates are not happening while the screen is not focused.
I am trying to implement feature to jump 15 seconds forward or backward in video.
I am facing hard time to set the update and set the current time.
const videoNode = useRef(null);
const [currentTime, setCurrentTime] = useState(null);
const handleTimeJump = (type) => {
const player = videojs(videoNode.current);
console.log(player)
if (player) {
type === 'inc' && setCurrentTime(player.currentTime() + 15);
player.currentTime() > 15 ? setCurrentTime(player.currentTime() - 15) : setCurrentTime(0)
}
};
useEffect(() => {
const player = videojs(
videoNode.current,
videoJsOptions,
function onPlayerReady() {
console.log('onPlayerReady');
player.on('timeupdate', () => {
setCurrentTime(player.currentTime());
});
},
);
if (!videoJsOptions.sources[0].src) {
console.log('no source found');
}
return () => {
if (player) {
player.dispose();
}
};
}, []);
useEffect(() => {
const player = videojs(videoNode.current)
player.currentTime(currentTime)
}, [currentTime])
handleTimeJump is called after clicking a button.
onClick={() => handleTimeJump('inc')}
Look I haven't tested if it works but looks like it should be player.setCurrentTime(currentTime) instead of player.currentTime(currentTime)
If that works then they should have thrown an error when calling currentTime with an argument because it is not supposed to take an argument (*cough* or you could use a statically typed language *cough*)
Also the currentTime state is already in the videojs-land there's no need to create another in React-land and keep them in sync. You're dispatching a react update EVERY SECOND. Here's a higly recommended and unsolicited refactor (keeping diff as less as possible):
const videoNode = useRef(null);
const playerRef = useRef(null);
const player = playerRef.current;
const handleTimeJump = (type) => {
if (player) {
type === 'inc' && player.setCurrentTime(player.currentTime() + 15);
player.currentTime() > 15 ? player.setCurrentTime(player.currentTime() - 15) : player.setCurrentTime(0)
}
};
useEffect(() => {
playerRef.current = videojs(
videoNode.current,
videoJsOptions
);
if (!videoJsOptions.sources[0].src) {
console.log('no source found');
}
return () => {
if (player) {
player.dispose();
}
};
}, []);
suppose we try to connect web socket. web socket server sends some data for fetching client status which represents the online or offline. I tried to store these data into redux (works fine), but I need to change these statuses instantly with overriding the existing objects. I need some functionality for override my redux store. I get so far with the snippet below.
but:
this code push objects to my redux store not overriding
const [status, set_status] = useState([]);
useEffect(() => {
const socket = io(ws_api, {
query: `token=${localStorage.getItem("token")}`,
});
socket.on("message", (data) => {
status.map((item) => {
if (item.application === data.application) {
item["status"] = data.status;
} else {
set_status((status) => [...status, data]);
}
});
});
}, []);
useEffect(() => {
get_client_status(status); // redux action
}, [status]);
the data structure which is coming from the socket on message
{
application: "5ede25f4d3fde1c8a70f0a38"
client: "5ede25f4d3fde1c8a70f0a36"
status: "offline"
}
First search current state for any existing data elements, if found then update it, otherwise add to the array.
Using array::findIndex
const messageHandler = data => {
const dataIndex = status.findIndex(
item => item.application === data.application
);
if (dataIndex !== -1) {
set_status(status =>
status.map(item => {
return item.application === data.application
? { ...item, status: data.status }
: item;
})
);
} else {
set_status(status => [...status, data]);
}
});
Using array::find
const messageHandler = data => {
const found = status.find(item => item.application === data.application);
if (found) {
set_status(status =>
status.map(item => {
return item.application === data.application
? { ...item, status: data.status }
: item;
})
);
} else {
set_status(status => [...status, data]);
}
});
Edit: define this callback outside the effect
useEffect(() => {
socket.on("message", messageHandler);
}, []);
I'm testing out logout function on our react app and I'm having trouble testing the localStorage. What I want to do is check if (value === 'logout') in my unit test so I can have a expect assertion that will check if localStorage has the item i'm testing for, this is where I'm having trouble. How can I assert that if (value === 'logout') then equals localStorage.removeItem('id_token')
Here is the .js snippet I'm testing for
_handleSelectItem = (event, value) => {
if (value === 'logout') {
const sharedLogic = () => {
localStorage.removeItem('id_token')
window.location = '/' // TODO: find a better way to reset the relay cache
}
const onSuccess = (response) => {
sharedLogic()
}
const onFailure = (transaction) => {
var error = transaction.getError() || new Error('Mutation failed.')
console.error(error)
sharedLogic()
}
this.props.relay.commitUpdate(
new SignOutUserMutation({
viewer: this.props.viewer
}
), {onSuccess, onFailure})
}
}
Here is the .spec.js I wrote that didn't quite do the job that I wanted
if(!global.localStorage) {global.localStorage = {} }
// if(value === 'logout')
describe('(Component) AccountManager | Logout ', () => {
let _component
let _props
let value = 'logout'
// if(value === 'logout')
beforeEach(() => {
_props = {
viewer: {
email: 'joe#example.com'
}
}
_component = shallowRenderWithProps(_props)
sinon.spy(global.localStorage, 'getItem')
sinon.spy(global.localStorage, 'setItem')
sinon.spy(global.localStorage, 'clear')
})
afterEach(() => {
global.localStorage.getItem.restore()
global.localStorage.setItem.restore()
global.localStorage.clear.restore()
})
it('Should check if Logout work correctly', () => {
if(value === 'logout');
console.log(global.localStorage)
expect(global.localStorage.getItem.withArgs('id_token').calledOnce).is.true;
})
Note the test above passes but does not clear the CodeCov error that indicated I should test this. I'm just getting started with react so I appreciate the helping me learn more
if if (typeof(Storage) !== "undefined" && localStorage.getItem(value) === 'logout') {
//[do your stuff here]
}