Handling OAuth with React 18 useEffect hook running twice - reactjs

Background
I have recently upgraded a fairly sizeable React app to React 18 and for the most part it has been great. One of the key changes is the new double mount in development causing useEffect hooks to all run twice, this is clearly documented in their docs.
I have read their new effect documentation https://beta.reactjs.org/learn/lifecycle-of-reactive-effects and although it is quite detailed there is a use case I believe I have found which is not very well covered.
The issue
Essentially the issue I have run into is I am implementing OAuth integration with a third-party product. The flow:
-> User clicks create integration -> Redirect to product login -> Gets redirected back to our app with authorisation code -> We hit our API to finalise the integration (HTTP POST request)
The problem comes now that the useEffect hook runs twice it means that we would hit this last POST request twice, first one would succeed and the second would fail because the integration is already setup.
This is not potentially a major issue but the user would see an error message even though the request worked and just feels like a bad pattern.
Considered solutions
Refactoring to use a button
I could potentially get the user to click a button on the redirect URL after they have logged into the third-party product. This would work and seems to be what the React guides recommend (Although different use case they suggested - https://beta.reactjs.org/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers).
The problem with this is that the user has already clicked a button to create the integration so it feels like a worse user experience.
Ignore the duplicate API call
This issue is only a problem in development however it is still a bit annoying and feels like an issue I want to explore further
Code setup
I have simplified the code for this example but hopefully this gives a rough idea of how the intended code is meant to function.
const IntegrationRedirect: React.FC = () => {
const navigate = useNavigate();
const organisationIntegrationsService = useOrganisationIntegrationsService();
// Make call on the mount of this component
useEffect(() => {
// Call the method
handleCreateIntegration();
}, []);
const handleCreateIntegration = async (): Promise<void> => {
// Setup request
const request: ICreateIntegration = {
authorisationCode: ''
};
try {
// Make service call
const setupIntegrationResponse = await organisationIntegrationsService.createIntegration(request);
// Handle error
if (setupIntegrationResponse.data.errors) {
throw 'Failed to setup integrations';
}
// Navigate away on success
routes.organisation.integrations.navigate(navigate);
}
catch (error) {
// Handle error
}
};
return ();
};
What I am after
I am after suggestions based on the React 18 changes that would handle this situation, I feel that although this is a little specific/niche it is still a viable use case. It would be good to have a clean way to handle this as OAuth integration is quite a common flow for integration between products.

You can use the useRef() together with useEffect() for a workaround
const effectRan = useRef(false)
useEffect(() => {
if (effectRan.current === false) {
// do the async data fetch here
handleCreateIntegration();
}
//cleanup function
return () => {
effectRan.current = true // this will be set to true on the initial unmount
}
}, []);
This is a workaround suggested by Dave Gray on his youtube channel https://www.youtube.com/watch?v=81faZzp18NM

Related

Specifically, how does Reactjs retrieve data from firebase function triggers?

I am using express to create my firebase functions, and I understand how to create regular callable functions. I am lost however on the exact way to implement trigger functions for the background (i.e. onCreate, onDelete, onUpdate, onWrite), as well as how Reactjs in the frontend is supposed to receive the data.
The scenario I have is a generic chat system that uses react, firebase functions with express and realtime database. I am generally confused on the process of using triggers for when someone sends a message, to update another user's frontend data.
I have had a hard time finding a tutorial or documentation on the combination of these questions. Any links or a basic programmatic examples of the life cycle would be wonderful.
The parts I do understand is the way to write a trigger function:
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
.onWrite((change, context) => {
// Only edit data when it is first created.
if (change.before.exists()) {
return null;
}
// Exit when the data is deleted.
if (!change.after.exists()) {
return null;
}
// Grab the current value of what was written to the Realtime Database.
const original = change.after.val();
console.log('Uppercasing', context.params.pushId, original);
const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an "uppercase" sibling in the Realtime Database returns a Promise.
return change.after.ref.parent.child('uppercase').set(uppercase);
});
But I don't understand how this is being called or how the data from this reaches frontend code.
Background functions cannot return anything to client. They run after a certain event i.e. onWrite() in this case. If you want to update data at /messages/{pushId}/original to other users then you'll have to use Firebase Client SDK to listen to that path:
import { getDatabase, ref, onValue} from "firebase/database";
const db = getDatabase();
const msgRef = ref(db, `/messages/${pushId}/original`);
onValue(msgRef, (snapshot) => {
const data = snapshot.val();
console.log(data)
});
You can also listen to /messages/${pushId} with onChildAdded() to get notified about any new node under that path.

Why does my react tests fail in CI-pipeline due to "not wrapped in act()", while working fine locally?

I have a test-suite containing 37 tests that are testing one of my views. Locally, all tests pass without any issues, but when I push my code, the test-suite fails in our pipeline (we are using GitLab).
The error-output from the logs in CI are extremely long (thousands of lines, it even exceeds the limit set by GitLab). The error consists of many "not wrapped in act()"-, and "nested calls to act() are not supported"-warnings (Moslty triggered by useTranslation() from I18Next and componens like Tooltip from Material-UI).
My guess is that async-data from the API (mocked using msw) triggers a state-update after a call to act() has completed, but I'm not sure how to prove this, or even figure out what tests are actually failing.
Has anyone experienced something similar, or knows what's up?
Example of a failing test:
it.each([
[Status.DRAFT, [PAGE_1, PAGE_11, PAGE_2, PAGE_22, PAGE_3]],
[Status.PUBLISHED, [PAGE_1, PAGE_12, PAGE_2, PAGE_21, PAGE_22, PAGE_221]],
])('should be possible to filter nodes by status %s', async (status, expectedVisiblePages) => {
renderComponent();
await waitFor(() => {
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
userEvent.click(screen.getByLabelText('components.FilterMenu.MenuLabel'));
const overlay = await screen.findByRole('presentation');
await waitFor(() => expect(within(overlay).queryByRole('progressbar')).not.toBeInTheDocument());
userEvent.click(within(overlay).getByText(`SiteStatus.${status}`));
userEvent.keyboard('{Esc}');
const items = await screen.findAllByRole('link');
expect(items).toHaveLength(expectedVisiblePages.length);
expectedVisiblePages.forEach((page) => expect(screen.getByText(page.title)).toBeInTheDocument());
});
Update 1
Okay. So I've narrowed it down to this line:
const items = await screen.findAllByRole('link');
There seems to be a lot of stuff happening while waiting for things to appear. I believed that the call to findAllByRole was already wrapped in act() and that this would make sure all updates has been applied.
Update 2
It seems to be a problem partly caused by tests timing out.
I believe multiple calls to waitFor(...) and find[All]By(...) in the same test, in addition to a slow runner, collectively exceeds the timout for the test (5000ms by default). I've tried to adjust this limit by running the tests with --testTimeout 60000. And now, some of the tests are passing. I'm still struggling with the "act()"-warnings. Theese might be caused by a different problem entirely...
The bughunt continues...
After many attempts, I finally found the answer. The CI-server only has 2 CPUs available, and by running the tests with --maxWorkers=2 --maxConcurrent=2, instead of the default --maxWorkers=100% --maxConcurrent=5, proved to solve the problem.
This is a common issue ;)
I guess, you see this problem on CI Server because of the environment (less cpu/mem/etc).
This warning is because you do some async action but did not finish for complete it (because it's async).
You can read more about this issue in this article: https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning
The best solution is waiting for the operation to finish. For example by adding loading indicator and waiting for element remove.
For example:
it('should show empty table', async () => {
const [render] = createRenderAndStore()
mockResponse([])
const { container } = render(<CrmClientsView />) // - this view do async request in first render
await waitForElementToBeRemoved(screen.queryByRole('test-loading'))
await waitFor(() => expect(container).toHaveTextContent('There is no data'))
})

Firebase UI stops changing language in React app

I'm trying to change firebase language dynamically, meaning that I have a button on the page that allows me to switch language which triggers the handling below:
useEffect(() => {
if (!firebase) return;
// This sets up firebaseui
let localizedFirebaseui;
switch (langCode) {
case 'hu':
localizedFirebaseui = require('../intl/firebaseui/npm__hu.js');
break;
case 'pl':
localizedFirebaseui = require('../intl/firebaseui/npm__pl.js');
break;
default:
localizedFirebaseui = require('../intl/firebaseui/npm__en.js');
break;
}
// Configure FirebaseUI.
const uiConfig = {
// Popup sign-in flow rather than redirect flow.
signInFlow: 'popup',
// We will display Google and Facebook as auth providers.
signInOptions: [
originalFirebase.auth.EmailAuthProvider.PROVIDER_ID,
originalFirebase.auth.GoogleAuthProvider.PROVIDER_ID,
originalFirebase.auth.FacebookAuthProvider.PROVIDER_ID,
],
signInSuccessUrl: ROUTE_PATH_TYPEWRITER,
tosUrl: ROUTE_PATH_TERMS_OF_USE,
privacyPolicyUrl: ROUTE_PATH_PRIVACY_POLICY,
};
const authUi = new localizedFirebaseui.auth.AuthUI(originalFirebase.auth());
firebaseuiRef.current && authUi.start(firebaseuiRef.current, uiConfig);
return () => {
authUi && authUi.delete();
};
}, [firebase]);
firebase is an initialized instance in this case, basically on every render of the page useEffect is called which allows me to load localized data and start the UI, and then I delete the instance to be able to reinitialize it again with a different set of translations.
The problem is that the language switch works until I'm choosing every language for the first time, for instance, if I start with en, switch to pl, I'm not able to go back to en, but loading hu will work properly.
Language change itself is fine because the rest of the app is switching without an issue.
Any ideas on how to fix it? Or maybe it's not even possible, or there is a better solution than deleting and starting the UI over and over? I'm having a hard time finding anything related to the topic, I don't know why, is it some trivial error that no one faced before?

React Bootstrap Typeahead AsyncTypehead synchronous issue with Redux

I am developing a user search feature in my react/redux application using react-bootstrap-typehead: http://ericgio.github.io/react-bootstrap-typeahead/
The user search calls an API to search the list of users, so I am using the AsyncTypeahead component.
Since I are using Redux, I am storing the loading and search results within the store, so my code looks something like this:
const { searchPeople, loading, searchResults, selectedPerson } = this.props;
<AsyncTypeahead
isLoading={loading}
options={searchResults}
labelKey="DisplayName"
clearButton
minLength={5}
onSearch={searchPeople}
onChange={handleChange}
placeholder="Search for a user..."
renderMenuItemChildren={option => (
<TypeaheadItem key={option.EmployeeID} item={option} {...this.props} />
)}
/>
The onSearch={searchPeople} calls an action in Redux to call the API and store the results in "searchResults":
const searchPeople = term => async dispatch => {
dispatch({
type: REQUEST_SENT
});
const results = await dispatch(applicationActions.peopleSearch(term));
dispatch({
type: REQUEST_RECEIVED,
data: results
});
};
My "peopleSearch" function is stored in another action where I have all of our user search functionality. That is why I am dispatching to another action.
const peopleSearch = searchTerm => async () => {
const url = `https://api-personSearch.test.com/search=${searchTerm}&Output=JSONP`;
const response = await fetchJsonp(url);
const data = await response.json();
return data.slice(0, 10);
};
Everything works perfectly if I search for a user typing slowly. The problem is, if I type a users name quickly or at a normal pace, multiple "REQUEST_SENT" dispatches get called before any "REQUEST_RECEIVED" get called. So looking at the Redux Dev Tools shows results looking like this:
REQUEST_SENT
REQUEST_SENT
REQUEST_SENT
REQUEST_RECEIVED
REQUEST_RECEIVED
REQUEST_RECEIVED
What ends up getting sent to the interface does not end up being the results for the last letter the user ended up typing.
What would be the proper way to use AsyncTypeahead with Redux so that the results are returned in the proper order that they are sent?
Not Great Solution
One thing that ended up working (even though I think it's sort of a hack) is adding a "delay" to the AsyncTypehead. Adding a delay={1000} prop to the AsyncTypeahead component gives the api just enough time to finish it's call before another call to the api is made.
I would like to find a better solution if one is possible.
I'm having a different issue in my usage, but I can suggest a solution to the ordering problem.
Store the last query string in Redux.
When a request completes, ignore it if it isn't for the last query string.
Ideally, you'd cancel the previous request when receiving a new one in onSearch(), but this will have the same effect. If the user keeps typing, delaying long enough for onSearch() to be fired but quick enough to cause overlapping requests, only the last request will display results.
I think it's a bug in the component where AsyncContainer and TypeheadContainer get out of sync. The delay approach works most of the time but even then I could type it in a way that I would get no results. Not happy with this answer either but turning off the cache is the only way to guarantee that doesn't happen.
useCache={false}

How to system-test real-time react multiplayer game?

I am working on a real-time multiplayer board game built using node and socket.io for the backend and react + redux for the frontend. It's the first time I'm doing a project of this sort, i.e. real-time and multiplayer.
I am unsure on how best to go about integration/system testing. How can I actually automate spinning up, say 10 frontends and having them play a game together? Should I use a testing framework for this and if so, which one would be a good choice and why?
I found this question with the same question. I have figured out a way for it to work, which I'm guessing you have as well by now, but for someone else to come across:
A clarification on terms: integration testing can be done for react with things like Jest + enzyme, using mount(). I'm answering this based on looking for end-to-end / acceptance testing, where you're essentially testing your product from the user's standpoint (here, navigating around on a website).
As this is from the user's perspective, I believe it is irrelevant that you're using React.
So, how to do this? There are many JS testing options. That resource can help understand which testing package you might want to select. You want something that simulates an actual browser.
In investigating some of the options listed in the above resource, I have found that:
TestCafe does not support multi-page testing.
WebdriverIO does support multiple browsers at the same time.
Puppeteer does support multiple pages at the same time.
edit: I initially proposed using nightmare. However, I was getting some wonky behavior when running multiple tests (unexpected timeouts, Electron instances not closing properly), and have investigated some other options. But I'll retain the information for reference:
I selected nightmare because it was advertised as simple.
Below is an example test, using Jest and nightmare (and some sloppy TypeScript). The site has a button to end the player's turn, and there is a header that indicates whose turn it is. I'll simulate clicking that button and making sure the header changes as expected. Also note that you'll need your dev server and frontend running during these tests.
import * as Nightmare from 'nightmare';
let nightmare1: Nightmare;
let nightmare2: Nightmare;
beforeEach(async () => {
nightmare1 = new Nightmare({ show: true })
nightmare2 = new Nightmare({ show: true })
await nightmare1
.goto('http://127.0.0.1:3000');
await nightmare2
.goto('http://127.0.0.1:3000');
});
afterEach(async () => {
await nightmare1.end();
await nightmare2.end();
});
it('sockets turn changes via End Turn button', async () => {
expect.assertions(6);
// Both display the same player's turn ("Red's Turn")
const startingTurnIndicator1 = await nightmare1
.evaluate(() => document.querySelector('h1').innerText);
const startingTurnIndicator2 = await nightmare2
.evaluate(() => document.querySelector('h1').innerText);
expect(startingTurnIndicator1).toBe(startingTurnIndicator2);
// Both change ("Blue's Turn")
const oneClickTI1 = await nightmare1
.click('button')
.evaluate(() => document.querySelector('h1').innerText)
const oneClickTI2 = await nightmare2
.evaluate(() => document.querySelector('h1').innerText);
expect(oneClickTI1).toBe(oneClickTI2);
expect(oneClickTI1).not.toBe(startingTurnIndicator1);
// Both change back ("Red's Turn")
const twoClickTI2 = await nightmare2
.click('button')
.evaluate(() => document.querySelector('h1').innerText)
const twoClickTI1 = await nightmare1
.evaluate(() => document.querySelector('h1').innerText);
expect(twoClickTI1).toBe(twoClickTI2);
expect(twoClickTI1).toBe(startingTurnIndicator2);
expect(twoClickTI1).not.toBe(oneClickTI1);
});
I'm not sure how good the actual code in this test is, but it works.

Resources