Antd Notification appearing twice? - reactjs

I am using antd notification component as follows
import { notification } from "antd";
const Notification = ({ msg, type, clearMsg }) => {
return (
<div>
{notification[type]({
description: msg,
})}
{clearMsg()}
</div>
);
};
export default Notification;
And I am simply using it anywhere I need a notification to pop-up. For instance after API response failure:
const onSubmit = async (e) => {
try {
const res = await login(data);
if (res.message) {
setError({ state: true, msg: res.message });
}
} catch (err) {
console.log(err);
}
};
And then depending on error state, I am showing the Notification in the return body as following:
{ error.state ?
<Notification
type="error"
msg="some msg"
clearMsg={() => setError({state: false, msg: ""})} :
null
}
But simultaneously two pop-ups appear instead of just one, can anyone guide on why I am getting this behaviour?

Assign a unique identifier by using the key property. See: https://ant.design/components/notification/#API
const Notification = ({ key, msg, type, clearMsg }) => {
return (
<div>
{notification[type]({
key,
description: msg,
})}
{clearMsg()}
</div>
);
};

I resolved my problem by using notification.destroy(); before showing notification. An example below here:
const showNotification = () => {
notification.destroy();
notification.open({message: 'message here'});
}

Related

Pubnub message and presence handler are invoked multiple times

I`m using Next.js for SSR in my app, also using Pubnub for real-time chat. So I have created my pubnub instance of chat like this in pages/index.js
const handleMessage = event => {
const { message, channel } = event;
addMessage(channel, message)
};
const handlePresence = event => {
const { state, channel, action } = event;
if (action === 'state-change') {
addPresence({
presence: { lastReadMessageTimestamp: state.lastReadMessageTimestamp },
id: channel,
})
}
};
useEffect(() => {
pubnub.addListener({
message: handleMessage,
presence: handlePresence,
});
pubnub.subscribe({
channelGroups: [channelGroup],
withPresence: true,
});
}, []);
return (
<PubNubProvider client={pubnub}>
<Fragment>
<section>
<MetaTags title={MAIN_SEO.title} description={MAIN_SEO.description} />
<Header />
<main>
{children}
</main>
</section>
</Fragment>
</PubNubProvider>
So I have 2 pages, where I should use pubnub, pages/chat.js and pages/messages.js. So while I start from messages page for example all is good, when I go from messages page to chat page, my handler for messages and presence invokes multiple times on next page(and I see for example 3 same messages in a thread), when for real I get only 1 message or only 1 presence. After reloading the page result is at should be(so I see only 1 message which came to me).
Fixed that issues, just added removeEventListener:
const handleMessage = event => {
const { message, channel } = event;
addMessage(channel, message)
};
const handlePresence = event => {
const { state, channel, action } = event;
if (action === 'state-change') {
addPresence({
presence: { lastReadMessageTimestamp: state.lastReadMessageTimestamp },
id: channel,
})
}
};
const pubnubListener = {
message: handleMessage,
presence: handlePresence,
}
const leaveApplication = () => {
pubnub.removeListener(pubnubListener);
pubnub.unsubscribeAll()
}
useEffect(() => {
pubnub.addListener(pubnubListener);
pubnub.subscribe({
channelGroups: [channelGroup],
withPresence: true,
});
return leaveApplication
}, []);

Unhandled Rejection (TypeError): Cannot read property 'id' of null

enter image description here*when i click add to cart. I get this in the terminal. Im stuck. please help. Unhandled Rejection (TypeError): Cannot read property 'id' of null. when i click add to cart. I get this in the terminal. Im stuck. please help. Unhandled Rejection (TypeError): Cannot read property 'id' of null
when i click add to cart. I get this in the terminal. Im stuck. please help. Unhandled Rejection (TypeError): Cannot read property 'id' of null.
PLEASE NOTE HAT I HAD TO CUT SOME OF THE CODE OFF
Shopcontext.js
class ShopProvider extends Component {
state = {
product: {},
products: [],
checkout: {},
isCartOpen: false,
isMenuOpen: false
}
componentDidMount() {
if (localStorage.checkout_id) {
this.fetchCheckout(localStorage.checkout_id)
} else {
this.createCheckout()
}
}
addItemToCheckout = async (variantId, quantity) => {
const lineItemsToAdd = [
{
variantId,
quantity: parseInt(quantity, 10)
}
]
const checkout = await client.checkout.addLineItems(this.state.checkout.id, lineItemsToAdd)
this.setState({ checkout: checkout })
console.log(checkout);
this.openCart();
}
fetchAllProducts = async () => {
const products = await client.product.fetchAll();
this.setState({ products: products });
};
fetchProductWithHandle = async (handle) => {
const product = await client.product.fetchByHandle(handle)
this.setState({ product: product})
}
render() {
return (
<ShopContext.Provider
value={{
...this.state,
fetchAllProducts: this.fetchAllProducts,
fetchProductWithHandle: this.fetchProductWithHandle,
addItemToCheckout: this.addItemToCheckout,
removeLineItem: this.removeLineItem,
closeCart: this.closeCart,
openCart: this.openCart,
closeMenu: this.closeMenu,
openMenu: this.openMenu
}}
>
{this.props.children}
</ShopContext.Provider>
);
}
}
const ShopConsumer = ShopContext.Consumer
export { ShopConsumer, ShopContext };
export default ShopProvider
**Product page**
const ProductPage = () => {
const { handle } = useParams();
const { fetchProductWithHandle, addItemToCheckout, product } = useContext(ShopContext)
useEffect(() => {
fetchProductWithHandle(handle)
}, [fetchProductWithHandle, handle])
if (!product.title) return <div>Loading....</div>
return (
<Box>
<Button
onClick={() => addItemToCheckout(product.variants[0].id, 1)}
>
Add To Cart
</Button>
</Box>
)
}
export default ProductPage;*
this is a general answer for this problem hope someone can benefit from it
so the problem when you encounter the error: Cannot read property '****' of null is that your trying to mape into a null object so first try to look to object and verify way it is null and verify that object container id or whatever it is,
sometimes the object takes time to render so that's why it gives this problem so how a avoid it is adding condition when I use the element like {object.id&&(<div>....</div)}
hope this answer can help someone :)

How can I pass event properties to a customEvent using React Testing Library?

I am trying to test a component using RTL that uses a customEvent to trigger a display. Given a component like this:
const Alerts = () => {
const [alerts, setAlerts] = useState([]);
const addAlert = (body, type = 'info', timeout = 7500) => {
const key = nextIndex++;
setAlerts([
...alerts,
{ key, body, type },
]);
// Set a timer to remove the alert.
setTimeout(() => {
setAlerts(alerts.filter((alert) => alert.key !== key));
}, timeout);
};
document.addEventListener(
'newalert',
({ details:
{
body = '',
type = '',
timeout = 1000
} = {}
}) => {
addAlert(body, type, timeout);
});
return (
<>
{alerts.map((alert) => <Alert {...alert} />)}
</>
);
};
I am trying to test it like this:
test('An alert is rendered on a newalert event', () => {
render(<Alerts />);
fireEvent(
document,
createEvent('newalert', document,
{
details: {
body: 'Hello World!',
type: 'info',
timeout: 1000,
}
})
);
expect(screen.getByText('Hello world'));
});
And it is firing the custom event as expected, however none of the properties (details, body, type, or timeout) is being passed to the event. What am I missing?
You need to pass the name of the event constructor as the fourth parameter, and it works!
createEvent(
'newalert',
document,
{
detail: {
body: 'Hello World!',
type: 'info',
timeout: 1000,
}
},
{ EventType: 'CustomEvent' }
)
);

useState referring to stale value

I have a keeper app where I am adding notes and storing them in database. When I make a post request to the server, I am trying to fetch the _id from database, which will eventually help me to later delete the note ( if needed).
Here is my jsx file
function CreateMessage(props) {
const [currentGuest, setCurrentGuest] = useState({
guestName: '',
guestMessage: '',
id:''
});
function handleMessages(event) {
const {name, value} = event.target;
setCurrentGuest(prevGuest => {
return {
...prevGuest,
[name] : value
};
});
}
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
return (
<div>
<form>
<input
name="guestName"
placeholder="Guest Name"
value={currentGuest.guestName}
onChange={handleMessages}
/>
<textarea
name="guestMessage"
placeholder="Write a Message"
rows="3"
value={currentGuest.guestMessage}
onChange={handleMessages}
/>
<button onClick={submitMessage}>Add</button>
</form>
</div>
);
}
The id is properly being fetched and displayed in ```console.log("The response is"+res.data._id"). But on first submit, the is always empty and stale id gets attached to the currentGuest object on next submit
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
In the above snippet, after getting the response you're correctly changing the state but the problem is with where you're checking the changed state(console.log(currentGuest)). You're basically logging before the state is changed.
You can use useEffect hook and log the state inside it. This runs every time the currentGuest Changes.
useEffect(() => {
console.log(currentGuest)
}, [currentGuest])
Update
You can use the modified currentGuest inside the useEffect hook:
useEffect(() => {
console.log(currentGuest)
if(currentGuest.id) {
props.onAdd(currentGuest);
// You can also reset the state here as follows
setCurrentGuest({
guestName: '',
guestMessage: '',
id:''
});
}
}, [currentGuest]) // You might need to add the necessary dependencies to this array.

Mock multiple fetch calls with state updates in ReactJS

I am having a ReactJS component which does two things:
- on ComponentDidMount it will retrieve a list of entries
- on Button click it will submit the select entry to a backend
The problem is that i need to mock both requests (made with fetch) in order to test it properly. In my current testcase i want to test a failure in the submit on the button click. However due some odd reason the setState is triggered however the update from that is received after i want to compare it.
Dumps i did for the test. First one is the state as listen in the test. The second is from the code itself where it is setting state().error to the error received from the call
FAIL react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console
console.log react/src/components/Authentication/DealerSelection.test.jsx:114
{ loading: true,
error: null,
options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
console.log react/src/components/Authentication/DealerSelection.jsx:52
set error to: my error
The actual test code:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
const element = mount(<DealerSelection />);
process.nextTick(() => {
jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
process.nextTick(() => {
console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
global.fetch.mockClear();
done();
});
});
});
This is the component that i actually use:
import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '#omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';
class DealerSelection extends Component {
state = {
loading: true,
error: null,
dealer: '',
options: []
}
componentDidMount() {
document.title = "Select dealer";
fetchBackend(
'/agent/account/dealerlist',
{},
this.onDealerListSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerListSuccessHandler = json => {
const options = json.data.map((item) => {
return {
key: item.key,
value: item.key,
text: item.value
};
});
this.setState({
loading: false,
options,
dealer: json.default
});
}
onFetchErrorHandler = err => {
if (err instanceof ClientError) {
err.response.json().then(data => {
this.setState({
error: data.error,
loading: false
});
});
} else {
console.log('set error to', err.message);
this.setState({
error: err.message,
loading: false
});
}
}
onSubmitHandler = () => {
const { dealer } = this.state;
this.setState({
loading: true,
error: null
});
fetchBackend(
'/agent/account/dealerPost',
{
dealer
},
this.onDealerSelectSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerSelectSuccessHandler = json => {
if (!json.error) {
window.location = json.redirect; // Refresh to return back to MVC
}
this.setState({
error: json.error
});
}
onChangeHandler = (event, key) => {
this.setState({
dealer: event.target.value
});
}
render() {
const { loading, error, dealer, options } = this.state;
const errorBanner = error ? <Banner type='error' text={error} /> : null;
return (
<div className='dealerselection'>
<Form>
<Header as="h1">Dealer selection</Header>
{ errorBanner }
<Select
label='My dealer'
fluid
defaultValue={dealer}
onChange={this.onChangeHandler}
maxHeight={5}
options={options}
/>
<Button
primary
fluid
onClick={this.onSubmitHandler}
loading={loading}
>Select dealer</Button>
</Form>
</div>
);
}
}
export default DealerSelection;
Interesting, this one took a little while to chase down.
Relevant parts from the Node.js doc on Event Loop, Timers, and process.nextTick():
process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.
...any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.
In other words, Node starts processing the nextTickQueue once the current operation is completed, and it will continue until the queue is empty before continuing with the event loop.
This means that if process.nextTick() is called while the nextTickQueue is processing, the callback is added to the queue and it will be processed before the event loop continues.
The doc warns:
This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.
...and as it turns out you can starve your Promise callbacks as well:
test('Promise and process.nextTick order', done => {
const order = [];
Promise.resolve().then(() => { order.push('2') });
process.nextTick(() => {
Promise.resolve().then(() => { order.push('7') });
order.push('3'); // this runs while processing the nextTickQueue...
process.nextTick(() => {
order.push('4'); // ...so all of these...
process.nextTick(() => {
order.push('5'); // ...get processed...
process.nextTick(() => {
order.push('6'); // ...before the event loop continues...
});
});
});
});
order.push('1');
setTimeout(() => {
expect(order).toEqual(['1','2','3','4','5','6','7']); // ...and 7 gets added last
done();
}, 0);
});
So in this case the nested process.nextTick() callback that logs element.state() ends up running before the Promise callbacks that would set state.error to 'my error'.
It is because of this that the doc recommends the following:
We recommend developers use setImmediate() in all cases because it's easier to reason about
If you change your process.nextTick calls to setImmediate (and create your fetch mocks as functions so Promise.reject() doesn't run immediately and cause an error) then your test should work as expected:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = () => Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
const element = mount(<DealerSelection />);
setImmediate(() => {
jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
setImmediate(() => {
console.log(element.state()); // state.error is 'my error'
global.fetch.mockClear();
done();
});
});
});
There are several asynchronous calls required to update the state, so your process.nextTick() isn't sufficient. To update the state, this needs to happen:
your test code clicks, and the event handler callback is queued
the event handler callback runs, runs fetch, gets a promise rejection, and runs the error handler
the error handler runs setState, which queues the state update (setState is asynchronous!)
your test code runs, checking the element's state
the state update runs
In short, you need to wait longer before asserting on the state.
A useful idiom to "wait" without nested process.nextTick() calls is to define a test helper
function wait() {
return new Promise((resolve) => setTimeout(resolve));
}
and then do
await wait();
as many times as required in your test code. Note that this requires you to define test functions as
test(async () => {
})
rather than
test(done => {
})

Resources