Skip to content

Commit

Permalink
fix(react): race condition when options are changed before long ele…
Browse files Browse the repository at this point in the history
…ment predicate is resolved
  • Loading branch information
nsbarsukov committed Sep 20, 2024
1 parent 2bb4a51 commit 29e5b82
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {MaskitoElementPredicate, MaskitoOptions} from '@maskito/core';
import {useMaskito} from '@maskito/react';
import type {ComponentType} from 'react';
import {useEffect, useState} from 'react';

import {AwesomeInput} from '../awesome-input';

export const SWITCH_OPTIONS_TIME = 1_000;
export const PREDICATE_RESOLVING_TIME = 2_000;

const numberOptions: MaskitoOptions = {mask: /^\d+$/};
const engLettersOptions: MaskitoOptions = {mask: /^[a-z]+$/i};

const elementPredicate: MaskitoElementPredicate = async (element) =>
new Promise((resolve) => {
setTimeout(() => resolve(element.querySelector('.real-input') as HTMLInputElement), PREDICATE_RESOLVING_TIME);
});

export const App: ComponentType = () => {
const [options, setOptions] = useState(numberOptions);
const maskRef = useMaskito({options, elementPredicate});

useEffect(() => {
setTimeout(() => {
setOptions(engLettersOptions);
}, SWITCH_OPTIONS_TIME);
}, []);

return <AwesomeInput ref={maskRef} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {TestWrapper} from './angular-wrapper';
import {PREDICATE_RESOLVING_TIME, SWITCH_OPTIONS_TIME} from './react-app';

describe('React async predicate + maskitoOptions race', () => {
beforeEach(() => {
cy.clock();
cy.mount(TestWrapper);
cy.get('.real-input').should('be.visible').as('textfield');
});

it('can enter any value before no predicate is resolved', () => {
cy.get('@textfield').focus().type('12abc3').should('have.value', '12abc3');
});

it('enabling of the first mask should be skipped if `options` were changed during resolving of element predicate', () => {
cy.smartTick(PREDICATE_RESOLVING_TIME); // predicate is resolved only once for digit cases
cy.get('@textfield').focus().type('12abc3').should('have.value', '12abc3');
});

it('only the last mask should be applied if [maskitoOptions] were changed during resolving of element predicates', () => {
cy.smartTick(SWITCH_OPTIONS_TIME + PREDICATE_RESOLVING_TIME); // enough time to resolve element predicated for both cases
cy.get('@textfield').focus().type('12abc3').should('have.value', 'abc');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {isPlatformBrowser} from '@angular/common';
import {ChangeDetectionStrategy, Component, ElementRef, inject, PLATFORM_ID} from '@angular/core';
import {createRoot} from 'react-dom/client';

import {App} from './react-app';

@Component({standalone: true, selector: 'test-wrapper', template: '', changeDetection: ChangeDetectionStrategy.OnPush})
export class TestWrapper {
constructor() {
if (isPlatformBrowser(inject(PLATFORM_ID))) {
createRoot(inject(ElementRef).nativeElement).render(<App />);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type {MaskitoElementPredicate, MaskitoOptions} from '@maskito/core';
import {maskitoInitialCalibrationPlugin} from '@maskito/core';
import {maskitoTimeOptionsGenerator} from '@maskito/kit';
import {useMaskito} from '@maskito/react';
import type {ComponentType, InputHTMLAttributes} from 'react';
import {forwardRef, useEffect, useState} from 'react';
import type {ComponentType} from 'react';
import {useEffect, useState} from 'react';

import {AwesomeInput} from '../awesome-input';

const timeOptions = maskitoTimeOptionsGenerator({
mode: 'HH:MM',
Expand Down Expand Up @@ -34,21 +36,6 @@ const fastValidPredicate: MaskitoElementPredicate = async (host) =>
setTimeout(() => resolve(correctPredicate(host)), 500);
});

const hiddenInputStyles = {
display: 'none',
};

export const AwesomeInput = forwardRef<HTMLDivElement, InputHTMLAttributes<HTMLInputElement>>((props, ref) => (
<div ref={ref}>
<input style={hiddenInputStyles} />
<input
className="real-input"
{...props}
/>
<input style={hiddenInputStyles} />
</div>
));

export const App: ComponentType = () => {
const [useCorrectPredicate, setUseCorrectPredicate] = useState(false);
const inputRef2sec = useMaskito({options, elementPredicate: longCorrectPredicate});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {forwardRef, type InputHTMLAttributes} from 'react';

const hiddenInputStyles = {
display: 'none',
};

export const AwesomeInput = forwardRef<HTMLDivElement, InputHTMLAttributes<HTMLInputElement>>((props, ref) => (
<div ref={ref}>
<input style={hiddenInputStyles} />
<input
className="real-input"
{...props}
/>
<input style={hiddenInputStyles} />
</div>
));
12 changes: 10 additions & 2 deletions projects/react/src/lib/useMaskito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export const useMaskito = ({
);

const latestPredicateRef = useRef(elementPredicate);
const latestOptionsRef = useRef(options);

latestPredicateRef.current = elementPredicate;
latestOptionsRef.current = options;

useIsomorphicLayoutEffect(() => {
if (!hostElement) {
Expand All @@ -57,15 +59,20 @@ export const useMaskito = ({
const elementOrPromise = predicate(hostElement);

if (isThenable(elementOrPromise)) {
const tempOptions = options;

void elementOrPromise.then((el) => {
if (latestPredicateRef.current === predicate) {
if (
latestPredicateRef.current === predicate &&
latestOptionsRef.current === tempOptions
) {
setElement(el);
}
});
} else {
setElement(elementOrPromise);
}
}, [hostElement, elementPredicate, latestPredicateRef]);
}, [hostElement, elementPredicate, latestPredicateRef, options, latestOptionsRef]);

useIsomorphicLayoutEffect(() => {
if (!element || !options) {
Expand All @@ -76,6 +83,7 @@ export const useMaskito = ({

return () => {
maskedElement.destroy();
setElement(null);
};
}, [options, element]);

Expand Down

0 comments on commit 29e5b82

Please sign in to comment.