How to update multiple time a discord.js's interaction using setTimeout? - node.js

I'm currently working on a discordjs v13.6 bot in typescript who post an embed object with the date of day when using /day interaction command.
In a nutshell: I make /day and an embed is posted by my bot with the current date, weather, etc...
It's work fine and I added 3 buttons under the embed:
"reload" button: it will simply update the embed (with current weather forecast).
"previous" button: to update embed's interaction to the previous day.
"next" button: to update embed's interaction to the next day.
My code work as it should like this for the 3 buttons attached to my embed:
import { MessageActionRow, MessageButton } from 'discord.js';
// All customId property are formated like: `{method}#{date}`, ie: `reload#2022-03-10` is the button who'll reload the embed to date 03/10/2022.
export const createNavigationButtons = (date) => (
new MessageActionRow().addComponents([
new MessageButton()
.setCustomId(`prev#${date}`)
.setStyle('SECONDARY')
.setEmoji("946186554517389332"),
new MessageButton()
.setCustomId(`reload#${date}`)
.setStyle('SUCCESS')
.setEmoji("946190012154794055"),
new MessageButton()
.setCustomId(`next#${date}`)
.setStyle('SECONDARY')
.setEmoji("946186699745161296")
])
)
For the logic:
import { ButtonInteraction } from 'discord.js';
import { createEmbed } from "../utils/embed";
import { createNavigationButtons } from "../utils/buttons";
import * as year from "../../resources/year.json";
import * as moment from 'moment';
import { setTimeout as wait } from 'node:timers/promises';
// This function is called in the on('interactionCreate') event, when interaction.isButton() is true
export const button = async (interaction: ButtonInteraction): Promise<void> => {
const [action, date]: string[] = interaction.customId?.split('#');
await interaction.deferUpdate();
await wait(1000);
const newDate: string = {
prev: moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
next: moment(date, "YYYY-MM-DD").add( 1, "day").format("YYYY-MM-DD"),
reload: date
}[action];
await interaction.editReply({
embeds: [await createEmbed(year[newDate])],
components: [createNavigationButtons(newDate)]
});
}
It works just as I wished. BUT, everybody can use theses buttons (and I don't want to send /day's answer as ephemeral, I want everybody to see the response). So, if we use /day 2022-03-10 the embed for March 10, 2022. but if the author or someone else (I don't mind) use the button, the embed will be updated with another date (and that's fine by me !). But I want to roll back my embed to the original date few seconds / minutes after the button is pressed.
I tried somes primitive way like the setTimeout like this:
export const button = async (interaction: ButtonInteraction, config: any): Promise<void> => {
// In this test, button's customId are formated like {method}#{date}#{date's origin} (where 'origin' is the original requested's date, optional)
const [action, date, origin]: string[] = interaction.customId?.split('#');
await interaction.deferUpdate();
await wait(1000);
const newDate: string = {
prev: moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
next: moment(date, "YYYY-MM-DD").add( 1, "day").format("YYYY-MM-DD"),
reload: date
}[action];
// Here is my setTimeout who is supposed to recursively recall this function with a "reload" and the original date
setTimeout(async () => {
interaction.customId = `reload#${origin ?? date}`;
console.log(interaction.customId);
await button(interaction, config);
}, 5000);
await interaction.editReply({
embeds: [await createEmbed(year[newDate])],
components: [createNavigationButtons(newDate)]
});
};
When I press my buttons with this, it's correctly updated but 5sec (setTimeout's value) after it end up with an error saying:
reload#2022-03-10
/home/toto/tata/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:180
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
^
Error [INTERACTION_ALREADY_REPLIED]: The reply to this interaction has already been sent or deferred.
at ButtonInteraction.deferUpdate (/home/toto/tata/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:180:46)
at button (/home/toto/tata/src/services/button.ts:12:21)
at Timeout._onTimeout (/home/toto/tata/src/services/button.ts:28:21)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7) {
[Symbol(code)]: 'INTERACTION_ALREADY_REPLIED'
}
I understand that it seems I can't reupdate my interaction with the same token like that, so how shoul I achieve my goal ? may be the setTimeout isn't a propper solution (but it was quite simple to implemant so I tried it first). Any Ideas ?

I successfully reached my objective like this:
// All customId property are formated like: `{method}#{date}#{origin}`, ie: `reload#2022-03-10` is the button who'll reload the embed to date 03/10/2022.
export const createNavigationButtons = (date: string, mode?: boolean) => (
new MessageActionRow().addComponents([
new MessageButton()
.setCustomId(`prev#${date}${!mode ? `#${date}` : ''}`)
.setStyle('SECONDARY')
.setEmoji("946186554517389332"),
new MessageButton()
.setCustomId(`reload#${date}`)
.setStyle('SUCCESS')
.setEmoji("946190012154794055"),
new MessageButton()
.setCustomId(`remind#${date}`)
.setStyle('PRIMARY')
.setEmoji("946192601806155806"),
new MessageButton()
.setCustomId(`next#${date}${!mode ? `#${date}` : ''}`)
.setStyle('SECONDARY')
.setEmoji("946186699745161296")
])
);
export const createButtons = (date, mode?: boolean) => ({
components: [ createNavigationButtons(date, mode) ]
});
export const button = async (interaction: ButtonInteraction, config: any): Promise<void> => {
const [action, date, origin]: string[] = interaction.customId?.split('#');
const newDate: string = {
prev: moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
next: moment(date, "YYYY-MM-DD").add( 1, "day").format("YYYY-MM-DD"),
reload: date
}[action];
origin && !interaction.deferred && setTimeout(async () => {
await interaction.editReply({
embeds: [await createEmbed(year[origin], config.server)],
...createButtons(origin)
});
}, 120000);
!interaction.deferred && await interaction.deferUpdate();
await interaction.editReply({
embeds: [await createEmbed(year[newDate], config.server)],
...createButtons(newDate, true)
});
};
In a nutshell, when I first create the embed I place a #{origin} (who's a date), and when I navigate and update my embed with my buttons, I don't send origin (only {action}#{date}, not {action}#{date}#origin by passing true to my createButtons method.

Related

How can I set a button as disabled after the 15s has passed without any interaction?? Discord v14

const config = require('../../botconfig');
module.exports = {
name: 'invite',
description: 'Crimson and Server invites',
run: async (client, interaction, args) => {
try {
const inviteEmbed = new EmbedBuilder()
.setDescription('**__Invite • Support__**\n\n<a:Arrow:735141069033046106> Want to invite Crimson to your server? Feel free to click on the **"Invite"** button.\n\n<a:Arrow:735141069033046106> Need additional help with Crimson? Come join us in our humble abode by clicking on the **"Support"** button.')
.setColor('#EE1C25')
.setFooter({ text: `Command Requested by: ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() })
.setTimestamp()
.setThumbnail(client.user.displayAvatarURL());
let botInvite = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setURL(`https://discord.com/`)
.setLabel('Invite');
let support = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setURL('https://discord.gg/')
.setLabel('Support');
let del = new ButtonBuilder()
.setLabel(`Close`)
.setCustomId(`delete`)
.setEmoji(`❌`)
.setStyle(ButtonStyle.Danger);
const inviteMsg = await interaction.reply({ embeds: [inviteEmbed], components: [new ActionRowBuilder().addComponents(botInvite, support, del)], fetchReply: true });
const collector = inviteMsg.createMessageComponentCollector({ componentType: ComponentType.Button, time: 15000 });
collector.on('collect', async i => {
if (i.user.id === interaction.user.id) {
console.log(`${i.user.tag} clicked on the ${i.customId} button.`);
} else {
await i.reply({ content: `This button is not for you!`, ephemeral: true });
}
if (i.id === 'delete') {
inviteMsg.delete();
interaction.delete();
await i.reply.defer();
}
});
collector.on('end', collected => {
console.log(`Collected ${collected.size} interactions.`);
});
} catch (err) {
console.log(err);
return interaction.reply(`\`${err}\`.`);
}
}
};
I’ve been trying to mess around with it to see if i can but I’m running out of options to try 😂
I don’t know if I can do the same thing as the embed and set it as disabled through the components thing but not sure if that’s the correct way to do it.
You can do that easily by disabling the buttons once your collector ends.
collector.on('end', collected => {
botInvite.setDisabled(true);
support.setDisabled(true);
del.setDisabled(true);
// edit the message with the components disabled
inviteMsg.edit({embeds: [inviteEmbed], components: [new ActionRowBuilder().addComponents(botInvite, support, del)]});
console.log(`Collected ${collected.size} interactions.`);
});
If you have multiple buttons that need to be disabled, this can get a little annoying to add in your code.
What you can do is creating a row with your components, adding it to your message, and then looping through the buttons.
const row = new ActionRowBuilder().addComponents(botInvite, support, del);
const inviteMsg = await interaction.reply({ embeds: [inviteEmbed], components: [row], fetchReply: true });
// when your collector ends:
collector.on('end', collected => {
row.components.forEach(c => c.setDisabled(true));
// edit message
inviteMsg.edit({embeds: [inviteEmbed], components: [row]});
console.log(`Collected ${collected.size} interactions.`);
});

Saving and Reading UserState in Botframework v4

Hello I'm having a hard time dealing with UserStates in MSBF
Here's the setup of the dialogBot.ts
export class DialogBot extends ActivityHandler {
private conversationState: BotState;
private userState: BotState;
private dialog: Dialog;
private dialogState: StatePropertyAccessor<DialogState>;
/**
*
* #param {BotState} conversationState
* #param {BotState} userState
* #param {Dialog} dialog
*/
constructor(
conversationState: BotState,
userState: BotState,
dialog: Dialog
) {
super();
if (!conversationState) {
throw new Error(
'[DialogBot]: Missing parameter. conversationState is required'
);
}
if (!userState) {
throw new Error('[DialogBot]: Missing parameter. userState is required');
}
if (!dialog) {
throw new Error('[DialogBot]: Missing parameter. dialog is required');
}
this.conversationState = conversationState as ConversationState;
this.userState = userState as UserState;
this.dialog = dialog;
this.dialogState =
this.conversationState.createProperty<DialogState>('DialogState');
this.onMessage(async (context, next) => {
console.log('Running dialog with Message Activity.');
// Run the Dialog with the new message Activity.
await (this.dialog as MainDialog).run(context, this.dialogState);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
this.onDialog(async (context, next) => {
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
}
In the MainDialog.ts I'm fetching a user from the database based on the userID passed on and if it fetches anything it should be saved in the UserState.
mainDialog.ts
export class MainDialog extends CancelAndHelpDialog {
private userProfileAccessor: StatePropertyAccessor<any>;
userState: UserState;
constructor(
bookingDialog: BookingDialog,
userState: UserState,
conversationState: ConversationState
) {
super('MainDialog');
// DECLARE DIALOGS HERE
const createJobOrderDialog = new CreateJobOrderDialog(
'createJobOrderDialog'
);
const checkJobOrderStatusDialog = new CheckJobOrderStatusDialog(
'checkJobOrderStatusDialog'
);
const accountSetupDialog = new AccountSetupDialog(
'accountSetupDialog',
userState
);
this.userProfileAccessor = userState.createProperty('userProfile');
this.userState = userState;
// Define the main dialog and its related components.
// This is a sample "book a flight" dialog.
this.addDialog(new TextPrompt('TextPrompt'));
this.addDialog(bookingDialog);
this.addDialog(createJobOrderDialog);
this.addDialog(checkJobOrderStatusDialog);
this.addDialog(accountSetupDialog);
this.addDialog(
new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.accountSetupStep.bind(this),
this.introStep.bind(this),
this.actStep.bind(this),
this.finalStep.bind(this)
])
);
this.initialDialogId = MAIN_WATERFALL_DIALOG;
}
/**
* The run method handles the incoming activity (in the form of a DialogContext) and passes it through the dialog system.
* If no dialog is active, it will start the default dialog.
* #param {TurnContext} context
*/
public async run(
context: TurnContext,
accessor: StatePropertyAccessor<DialogState>
) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(context);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
private async accountSetupStep(
stepContext: WaterfallStepContext
): Promise<DialogTurnResult> {
const userProfile = await this.userProfileAccessor.get(
stepContext.context,
{}
);
stepContext.context.activity.from.id = '*******************';
userProfile.isHandover = false;
await this.userProfileAccessor.set(stepContext.context, userProfile);
// await this.userState.saveChanges(stepContext.context, true);
const result = await userService.getUser(
stepContext.context.activity.from.id
);
console.log(result);
if (Object.keys(result).length === 0) {
return await stepContext.beginDialog('accountSetupDialog');
} else {
userProfile.user = result;
await this.userProfileAccessor.set(stepContext.context, userProfile);
// await this.userState.saveChanges(stepContext.context, true);
return await stepContext.next();
}
}
private async introStep(
stepContext: WaterfallStepContext
): Promise<DialogTurnResult> {
const userProfile = await this.userProfileAccessor.get(
stepContext.context,
{}
);
console.log('INTRO STEP USERPROFILE', userProfile);
await stepContext.context.sendActivities([
{
type: 'message',
text: `Hi ${userProfile.user.first_name}, welcome to Podmachine. Let us take care of the dirty stuff so you can sound like a Pro!`
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 },
{
type: 'message',
text: 'To start, you need to submit a job order.'
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 },
{
type: 'message',
text: `So what's a job order? It's basically sending a request to edit (1) one raw episode audio file to Podmachine team. We'll handle the rest. `
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 },
{
type: 'message',
text: `Since you're part of the early access users (Yay!), you're entitled to (1) one free job order / edit. Go ahead and click "Create New Job order."`
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 }
]);
const messageText = (stepContext.options as any).restartMsg
? (stepContext.options as any).restartMsg
: `Please take note that once you submit your job order, Podmachine team will review it first. Make sure all the details you put in your job order are correct. It will be our basis when we do the edits. Thank you!`;
const promptMessage = MessageFactory.suggestedActions(
[
'Create New Job Order',
'Check Status',
'Chat with Team',
'Subscribe Now'
],
messageText
);
return await stepContext.prompt('TextPrompt', {
prompt: promptMessage
});
}
/**
* Second step in the waterall. This will use LUIS to attempt to extract the origin, destination and travel dates.
* Then, it hands off to the bookingDialog child dialog to collect any remaining details.
*/
private async actStep(
stepContext: WaterfallStepContext
): Promise<DialogTurnResult> {
// const bookingDetails = new BookingDetails();
const userProfile = await this.userProfileAccessor.get(stepContext.context, {});
console.log('USER PROFILE ACT STEP', userProfile);
switch (stepContext.result) {
case 'Create New Job Order':
return await stepContext.beginDialog('createJobOrderDialog');
break;
case 'Check Status':
return await stepContext.beginDialog('checkJobOrderStatusDialog');
break;
case 'Chat with Team':
userProfile.isHandover = true;
await stepContext.context.sendActivity(
`Hi ${userProfile.user.first_name}, we're glad to assist you. Please type your concern below. A Podmachine associate will getback to you within 3-5 minutes. Thank you for your patience.`
);
await this.userProfileAccessor.set(stepContext.context, userProfile);
return await stepContext.endDialog();
break;
case 'Upgrade Now':
await stepContext.context.sendActivity(
`Redirecting to Upgrade Now page...`
);
return await stepContext.endDialog();
break;
case 'Schedule a Checkpoint Meeting':
await stepContext.context.sendActivity(`Feature in progress...`);
return await stepContext.endDialog();
break;
default:
break;
}
return await stepContext.next();
// return await stepContext.beginDialog('bookingDialog', bookingDetails);
}
I can see the saved user details in the introStep but when it comes to the actStep I no longer see the value and it comes out undefined. Can you help me with implementing UserState because I'm not sure if I'm doing it correctly by loading it, the samples from github is not as clear.
USER PROFILE ACT STEP {}
[onTurnError] unhandled error: DialogContextError: Cannot read properties of undefined (reading 'first_name')
Looks like your bot aren't storing the state, so it can't recover it on the next turn.
Are you setting somewhere the storage your bot are using?
Check this doc on how to use storages:
https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=javascript

React 17.0.1 basic onChange is not updating values into state [duplicate]

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.
I have made an API on another PC that I am calling and fetching the data which I want to set into the state.
Here is my code:
<div id="root"></div>
<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant
const StateSelector = () => {
const initialValue = [
{
category: "",
photo: "",
description: "",
id: 0,
name: "",
rating: 0
}
];
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
// const response = await fetch("http://192.168.1.164:5000/movies/display");
// const json = await response.json();
// const result = json.data.result;
const result = [
{
category: "cat1",
description: "desc1",
id: "1546514491119",
name: "randomname2",
photo: null,
rating: "3"
},
{
category: "cat2",
description: "desc1",
id: "1546837819818",
name: "randomname1",
rating: "5"
}
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies);
} catch (e) {
console.error(e);
}
})();
}, []);
return <p>hello</p>;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
Neither setMovies(result) nor setMovies(...result) works.
I expect the result variable to be pushed into the movies array.
Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.
Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.
setMovies(result);
console.log(movies) // movies here will not be updated
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern
useEffect(() => {
// action on update of movies
}, [movies]);
As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.
However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like
setMovies(prevMovies => ([...prevMovies, ...result]));
Additional details to the previous answer:
While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.
TLDR: The reason is a closure scope around an immutable const value.
Solutions:
read the value in render function (not inside nested functions):
useEffect(() => { setMovies(result) }, [])
console.log(movies)
add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):
useEffect(() => { setMovies(result) }, [])
useEffect(() => { console.log(movies) }, [movies])
use a temporary variable:
useEffect(() => {
const newMovies = result
console.log(newMovies)
setMovies(newMovies)
}, [])
use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):
const moviesRef = useRef(initialValue)
useEffect(() => {
moviesRef.current = result
console.log(moviesRef.current)
}, [])
Explanation why it happens:
If async was the only reason, it would be possible to await setState().
However, both props and state are assumed to be unchanging during 1 render.
Treat this.state as if it were immutable.
With hooks, this assumption is enhanced by using constant values with the const keyword:
const [state, setState] = useState('initial')
The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).
Consider following fake, but synchronous, React-like implementation:
// sync implementation:
let internalState
let renderAgain
const setState = (updateFn) => {
internalState = updateFn(internalState)
renderAgain()
}
const useState = (defaultState) => {
if (!internalState) {
internalState = defaultState
}
return [internalState, setState]
}
const render = (component, node) => {
const {html, handleClick} = component()
node.innerHTML = html
renderAgain = () => render(component, node)
return handleClick
}
// test:
const MyComponent = () => {
const [x, setX] = useState(1)
console.log('in render:', x) // ✅
const handleClick = () => {
setX(current => current + 1)
console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
}
return {
html: `<button>${x}</button>`,
handleClick
}
}
const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef.
As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.
So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement
const { useEffect } = React
import { useState } from 'react-usestateref'
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
const result = [
{
id: "1546514491119",
},
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies.current); // will give you the latest results
} catch (e) {
console.error(e);
}
})();
}, []);
More information:
react-usestsateref
I just finished a rewrite with useReducer, following #kentcdobs article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.
See: https://kentcdodds.com/blog/how-to-use-react-context-effectively
I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action) {
if (state.hasOwnProperty(action.type)) {
return { ...state, [action.type]: state[action.type] = action.newValue };
}
throw new Error(`Unhandled action type: ${action.type}`);
}
const initialState = {
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: {},
};
function DispatchProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value={dispatch}>
<ApplicationContext.Provider value={state}>
{children}
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
}
function useDispatchable(stateName) {
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}
function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }
export {
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
}
With a usage similar to this:
import { useHistory } from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';
import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';
import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';
export const AltIdPage = () => {
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn => {
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
}
const doSubmit = () => {
history.push('s');
}
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
</Row>
<Row>
<MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
</Row>
<Row>
<SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
</Row>
<Row>
<ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
</Row>
</Container>
);
};
AltIdPage.propTypes = {};
Now everything persists smoothly everywhere across all my pages
React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.
Just pass a single argument in parameters state or leave it a black array and it will work perfectly.
React.useEffect(() => {
console.log("effect");
(async () => {
try {
let result = await fetch("/query/countries");
const res = await result.json();
let result1 = await fetch("/query/projects");
const res1 = await result1.json();
let result11 = await fetch("/query/regions");
const res11 = await result11.json();
setData({
countries: res,
projects: res1,
regions: res11
});
} catch {}
})(data)
}, [setData])
# or use this
useEffect(() => {
(async () => {
try {
await Promise.all([
fetch("/query/countries").then((response) => response.json()),
fetch("/query/projects").then((response) => response.json()),
fetch("/query/regions").then((response) => response.json())
]).then(([country, project, region]) => {
// console.log(country, project, region);
setData({
countries: country,
projects: project,
regions: region
});
})
} catch {
console.log("data fetch error")
}
})()
}, [setData]);
Alternatively, you can try React.useRef() for instant change in the React hook.
const movies = React.useRef(null);
useEffect(() => {
movies.current='values';
console.log(movies.current)
}, [])
The closure is not the only reason.
Based on the source code of useState (simplified below). Seems to me the value is never assigned right away.
What happens is that an update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.
Which means even we don't have closure issue, react version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.
function useState(initialState) {
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
}
There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
I too was stuck with the same problem. As other answers above have clarified the error here, which is that useState is asynchronous and you are trying to use the value just after setState. It is not updating on the console.log() part because of the asynchronous nature of setState, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState is completed on the background it will update the value and you will have access to that value on the next render.
If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.
https://www.youtube.com/watch?v=8aGhZQkoFbQ
I found this to be good. Instead of defining state (approach 1) as, example,
const initialValue = 1;
const [state,setState] = useState(initialValue)
Try this approach (approach 2),
const [state = initialValue,setState] = useState()
This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.
P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.
If we have to update state only, then a better way can be if we use the push method to do so.
Here is my code. I want to store URLs from Firebase in state.
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() => {
if (reload === 4) {
downloadUrl1();
}
}, [reload]);
const downloadUrl = async () => {
setImages([]);
try {
for (let i = 0; i < images.length; i++) {
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
}
}
catch (e) {
console.log(e);
}
};
const handleSubmit = async () => {
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
};
This code works to put URLs in state as an array. This might also work for you.
With custom hooks from my library, you can wait for the state values to update:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.
(Live Demo)
import React, { useState, useEffect, useCallback } from "react";
import { useAsyncWatcher } from "use-async-effect2";
function TestComponent(props) {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() => {
setText(`Counter: ${counter}`);
}, [counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
})();
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>{counter}</div>
<button onClick={inc}>Inc counter</button>
</div>
);
}
export default TestComponent;
useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.
(Live Demo)
import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";
function TestComponent(props) {
const [state, setState] = useAsyncDeepState({
counter: 0,
computedCounter: 0
});
useEffect(() => {
setState(({ counter }) => ({
computedCounter: counter * 2
}));
}, [state.counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(({ counter }) => ({ counter: counter + 1 }));
console.log("computedCounter=", state.computedCounter);
})();
});
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : {state.counter}</div>
<div>state.computedCounter : {state.computedCounter}</div>
<button onClick={() => inc()}>Inc counter</button>
</div>
);
}
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})
Without any addtional NPM package
//...
const BackendPageListing = () => {
const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})
const myFunction = ( x ) => {
setPagenateInfo({
...myData,
content: x
})
console.log(myData) // not reflecting change immediately
let myDataNew = {...myData, content: x };
console.log(myDataNew) // Reflecting change immediately
}
return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)
Not saying to do this, but it isn't hard to do what the OP asked without useEffect.
Use a promise to resolve the new state in the body of the setter function:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};
And this is how you use it (example shows the comparison between count and outOfSyncCount/syncCount in the UI rendering):
const App: React.FC = () => {
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () => {
setCount(count + 1);
// Doesn't work
setOutOfSyncCount(count);
// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};
return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};
Use the Background Timer library. It solved my problem.
const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;
Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.

Best way to access data in react

PROBLEM:
I have a MERN application that is has a model with a couple of other models in it. The problem that I figured out later is that it saves the _id of the object and not the actual object in the model when you do this
const checkoutHistory = new Schema({
book: { type: mongoose.Schema.Types.ObjectId, ref: 'books',required: true },
checkoutCopiesNum: {type: Number, required: true},
profChosen: { type: mongoose.Schema.Types.ObjectId, ref: 'prof', required: true },
dueDate: {type: String, required: true}
})
The book: part of the object when retreived will be an id some string like "DKKLDFJhdkghhe839kdd" whatever. This is fine because then I guess I can make an API call in the react app later to search for this book. Is this the correct way to do it though?
The other way that I thought of was in the actual endpoint that retrieves the data was to call the findByID functions and set that data. It didn't work though here is the code for that:
const checkoutHistoryMiddle = async (req, res, next) => {
try {
//get the body of the request
const body = req.body
//check for data
if(!body){
return res.status(400).json({
success: false,
error: 'no body given'
})
}
const history = new CheckoutHist(body)
console.log(history)
// await Book.findById({_id: history.book}, (err, book) => {
// history.book = book
// })
// await Prof.findById({_id: history.profChosen}, (err, prof) => history.profChosen = prof)
console.log(history)
history.save().then(() => next()).catch(error => {
return res.status(400).json({
success: false,
message: error,
msg: "checkout save failed"
})
})
} catch (error) {
res.status(400).json({
success: false,
message: error,
msg: "checkoutHist failed"
})
}
}
I commented out the part I was talking about because well, it didn't work. It still saved the id instead of the object. Which like I said is fine. I gave my other idea a go and decided to do the calls inside the react app.
So I first got the array of objects from the schema provided above like this:
const [bookHist, setBookHist] = useState()
useEffect( () => {
const getHistory = async () => {
api.getCheckoutHist().then(hist => {
setBookHist(hist.data.data.filter((data) => data.book === props.book_id))
})
}
getHistory()
}, [])
This will create an array of objects in bookHist that looks like this
[{_id: "DKJFDKJDKLFJSL", book: "LDKhgajgahgelkji8440skg", checkoutCopiesNum: 3, profChosen: "gjellkdh39gh39kal930alkdfj", dueDate: "11/11/11"}, {...}]
so the next step would be to take each item in the array and get the id to search the database with so api.findProfByID(bookHist[0].profChosen)
then I would need to update the state of bookHist somehow only that item without effect the other items in the array.
The questions I have are what is the best way to update one item in the array state?
How do I make so many api calls? how do I make sure that they are waited on so that the state actually changes once the calls complete?
Here are things I have tried so far:
useEffect(() => {
bookHist.map(async bHist => {
await Axios.get("http://localhost:8174/user/professor/" + bHist.profChosen).then(async prof => {
// console.log(prof)
// console.log(prof)
bHist.profChosen = prof.data.data
// setBookHist(prevStat => ({}))
// setBookHist(...bookHist, [bookHist.])
})
setBookHist(bHist)
})
}, [])
this didn't work I assume because it would not update the state because it is not waiting on the map to finish before it sets the state of bookHist
So then I searched on the internet and found a promisAll method in react like this:
useEffect(() => {
const change = async () => {
if(bookHist){
console.log("prof")
//get the prof data
// const galleries = []
await Promise.all(bookHist.map( (bHist, index) => {
return await Axios.get("http://localhost:8174/user/professor/" + bHist.profChosen);
})).then(someData => {
console.log(someData)
});
}
change()
}, [])
This also does not work for unknown reasons. It only works if it hot reloads and does not refresh. The logging actually logs something when it hot refreshes.
here is the entirety of the funcitional component:
import React, {useState, useEffect} from 'react'
import api from '../../api/index'
import Axios from 'axios'
export default function CheckoutBookHistroy(props){
const [bookHist, setBookHist] = useState()
const [histData, setHistData] = useState([{
book: {},
prof: {}
}])
useEffect( () => {
const getHistory = async () => {
api.getCheckoutHist().then(hist => {
setBookHist(hist.data.data.filter((data) => data.book === props.book_id))
})
}
getHistory()
}, [])
//i also tried this way but this resulted in an infinite loop
const [profChosen, setProfChosen] = useState()
const handleProfFind = async (id) => {
await Axios.get("http://localhost:8174/user/professor/" + id).then(prof => {
setProfChosen(prof.data.data)
})
}
return (
<div>
{
bookHist ?
bookHist.map(data => {
//need to present the prof data here for each data obj
return (
<div>Checked out {data.checkoutCopiesNum}</div>
)}) : <div>no data</div>
}
</div>
)
}
I really hope I can gain some insight into the correct way to do all of this. I must be either really close or awfully wrong. Thank you in advance!
just by looking at your code, i don't see too much issue, although your code is a bit convoluted.
some functions has no caller, ex. handleProfFind. One suggestion, if you want to do something, just do it, no need that many functions, ex.
// assume you only want to do it once after mounting
useEffect( () => {
if (!data) {
api.getCheckoutHist().then(hist => {
// you can set your data state here
// or you can get the id inside each item, and then call more APIs
// whatever you want to do, please finish it here
}
}
}, [])

Repeating a dialog step based on validation

I'm currently building a provisioning bot using v4 of the Bot Framework and I've integrated it with the Microsoft Graph.
The Microsoft Graph is being used to validate user inputs, so in this scenario, it's checking to see if the group name already exists. However, the issue I'm running into is getting the bot to repeat the previous step if the validation finds the group exists.
I've read through the forum and seen a number of solutions, particularly, I have come across the step.activeDialog.state['stepIndex']-2 approach, but have been unable to get it to work. Is this a viable solution for going back a step in NodeJS or should I be looking at another approach?
async nameStep(step) {
// User selected a group type and now is required to enter the name of the group
step.values.sitetype = step.result.value;
return await step.prompt(NAME_PROMPT, 'What do you want to name it');
}
async ownerStep(step) {
// Retrieve the value from the previous step and check against the Microsoft Graph to see if the name has been used previously
step.values.name = step.result;
const getToken =
await axios.post(TOKEN_ENDPOINT, qs.stringify(postData))
.then(response => {
return {
headers: {
'Authorization': 'Bearer ' + response.data.access_token
}
}
})
.catch(error => {
console.log(error);
});
const graphCall =
await axios.get("https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'" + `${step.result}` + "')", getToken)
.then((response) => {
if (response.data.value[0] != null) {
return true;
}
})
.catch((error) => {
console.log(error);
})
if (!graphCall) {
return await step.prompt(NAME_PROMPT, 'What is your email address');
} else {
await step.context.sendActivity("Group already exists");
return await step.activeDialog.state['stepIndex']-2
}
}
Thanking you in advance
You can achieve this by use of a component dialog. Essentially, you extrapolate the steps you would like to repeat into a separate dialog that is called only from within the current (parent) dialog. In the parent, you institute your checks. When a check fails, the component dialog is called again. If it succeeds, the parent dialog continues on.
In the code below, my parent dialog immediately calls the component dialog for a first pass thru presenting the user with two options. Each will send a pre-determined text value which is checked to see if a LUIS intent exists for it.
The first option, "Hello", will succeed with an intent having been found. It then restarts the parent dialog. The parent dialog starts with the text "You have a choice to make in life..." which will re-display as the parent dialog begins again.
The second option will fail and returns the user to the component dialog to try again. The component dialog starts with "Text me something! I'll see if my maker setup a LUIS intent for it." This text will display when either button is clicked because the component dialog is run in both instances. However, only this text will display when LUIS fails to find an intent and restarts the component dialog.
Side note - the parent dialog in this example is, in fact, a component dialog to my main dialog which is why it is exported at the end. So, yes, you can have component dialogs within component dialogs.
Parent Dialog:
const { ComponentDialog, WaterfallDialog } = require('botbuilder-dialogs');
const { LuisRecognizer } = require('botbuilder-ai');
const { ChoiceDialogSub, CHOICE_DIALOG_SUB } = require('./choiceDialog_Sub');
const CHOICE_DIALOG = 'choiceDialog';
class ChoiceDialog extends ComponentDialog {
constructor(id) {
super(id);
this.addDialog(new ChoiceDialogSub(CHOICE_DIALOG_SUB));
this.addDialog(new WaterfallDialog(CHOICE_DIALOG, [
this.welcomeStep.bind(this),
this.choiceLuisStep.bind(this)
]));
this.initialDialogId = CHOICE_DIALOG;
try {
this.recognizer = new LuisRecognizer({
applicationId: process.env.LuisAppId,
endpointKey: process.env.LuisAPIKey,
endpoint: `https://${ process.env.LuisAPIHostName }`
}, {}, true);
} catch (err) {
console.warn(`LUIS Exception: ${ err } Check your LUIS configuration`);
}
}
async welcomeStep(stepContext) {
await stepContext.context.sendActivity('You have a choice to make in life...');
return await stepContext.beginDialog(CHOICE_DIALOG_SUB);
}
async choiceLuisStep(stepContext) {
if (stepContext.context.activity.text) {
const stepResults = stepContext.context.activity.text;
const recognizerResult = await this.recognizer.recognize(stepContext.context);
const intent = await LuisRecognizer.topIntent(recognizerResult);
if (intent === 'Greeting') {
await stepContext.context.sendActivity(`'${ stepResults }' identified in the {${ intent }} intent.`);
return await stepContext.beginDialog(CHOICE_DIALOG);
} else {
await stepContext.context.sendActivity(`No LUIS intent was found for '${ stepResults }'.`);
return await stepContext.beginDialog(CHOICE_DIALOG_SUB);
}
} else {
await stepContext.context.sendActivity('I need text, fool!');
return await stepContext.next();
}
}
}
module.exports.ChoiceDialog = ChoiceDialog;
module.exports.CHOICE_DIALOG = CHOICE_DIALOG;
Component Dialog:
const { ChoicePrompt, ChoiceFactory, ComponentDialog, WaterfallDialog } = require('botbuilder-dialogs');
const CHOICE_DIALOG_SUB = 'choiceDialogSub';
const CHOICE_DIALOG_SUB_PROMPT = 'choicePromptSub';
class ChoiceDialogSub extends ComponentDialog {
constructor(id) {
super(id);
this.addDialog(new ChoicePrompt(CHOICE_DIALOG_SUB_PROMPT))
.addDialog(new WaterfallDialog(CHOICE_DIALOG_SUB, [
this.choiceStep.bind(this)
]));
this.initialDialogId = CHOICE_DIALOG_SUB;
}
async choiceStep(stepContext) {
const choices = ['Hello', 'No soup for you!'];
return await stepContext.prompt(CHOICE_DIALOG_SUB_PROMPT, {
prompt: "Text me something! I'll see if my maker setup a LUIS intent for it.",
choices: ChoiceFactory.toChoices(choices)
});
}
}
module.exports.ChoiceDialogSub = ChoiceDialogSub;
module.exports.CHOICE_DIALOG_SUB = CHOICE_DIALOG_SUB;
Hope of help!

Resources