Skip to content

Improve performance by wrapping event handlers in useCallback in rjsf/core #3035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions packages/core/src/components/fields/NumberField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useCallback } from "react";
import { asNumber, FieldProps } from "@rjsf/utils";

// Matches a string that ends in a . character, optionally followed by a sequence of
Expand Down Expand Up @@ -41,26 +41,29 @@ function NumberField<T = any, F = any>(props: FieldProps<T, F>) {
*
* @param value - The current value for the change occurring
*/
const handleChange = (value: FieldProps<T, F>["value"]) => {
// Cache the original value in component state
setLastValue(value);
const handleChange = useCallback(
(value: FieldProps<T, F>["value"]) => {
// Cache the original value in component state
setLastValue(value);

// Normalize decimals that don't start with a zero character in advance so
// that the rest of the normalization logic is simpler
if (`${value}`.charAt(0) === ".") {
value = `0${value}`;
}
// Normalize decimals that don't start with a zero character in advance so
// that the rest of the normalization logic is simpler
if (`${value}`.charAt(0) === ".") {
value = `0${value}`;
}

// Check that the value is a string (this can happen if the widget used is a
// <select>, due to an enum declaration etc) then, if the value ends in a
// trailing decimal point or multiple zeroes, strip the trailing values
const processed =
typeof value === "string" && value.match(trailingCharMatcherWithPrefix)
? asNumber(value.replace(trailingCharMatcher, ""))
: asNumber(value);
// Check that the value is a string (this can happen if the widget used is a
// <select>, due to an enum declaration etc) then, if the value ends in a
// trailing decimal point or multiple zeroes, strip the trailing values
const processed =
typeof value === "string" && value.match(trailingCharMatcherWithPrefix)
? asNumber(value.replace(trailingCharMatcher, ""))
: asNumber(value);

onChange(processed as unknown as T);
};
onChange(processed as unknown as T);
},
[onChange]
);

if (typeof lastValue === "string" && typeof value === "number") {
// Construct a regular expression that checks for a string that consists
Expand Down
26 changes: 16 additions & 10 deletions packages/core/src/components/templates/BaseInputTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import { getInputProps, WidgetProps } from "@rjsf/utils";

/** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme.
Expand Down Expand Up @@ -44,15 +44,21 @@ export default function BaseInputTemplate<T = any, F = any>(
inputValue = value == null ? "" : value;
}

const _onChange = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) =>
onChange(value === "" ? options.emptyValue : value);
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
const _onChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
onChange(value === "" ? options.emptyValue : value),
[onChange, options]
);
const _onBlur = useCallback(
({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value),
[onBlur, id]
);
const _onFocus = useCallback(
({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, value),
[onFocus, id]
);

return (
<>
Expand Down
49 changes: 29 additions & 20 deletions packages/core/src/components/widgets/AltDateWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { MouseEvent, useEffect, useReducer } from "react";
import React, { MouseEvent, useCallback, useEffect, useReducer } from "react";

import {
parseDateString,
Expand Down Expand Up @@ -132,27 +132,36 @@ function AltDateWidget<T = any, F = any>({
}
}, [state, time, onChange]);

const handleChange = (property: keyof DateObject, value: string) => {
setState({ [property]: value });
};
const handleChange = useCallback(
(property: keyof DateObject, value: string) => {
setState({ [property]: value });
},
[]
);

const handleSetNow = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (disabled || readonly) {
return;
}
const nowDateObj = parseDateString(new Date().toJSON(), time);
setState(nowDateObj);
};
const handleSetNow = useCallback(
(event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (disabled || readonly) {
return;
}
const nowDateObj = parseDateString(new Date().toJSON(), time);
setState(nowDateObj);
},
[disabled, readonly, time]
);

const handleClear = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (disabled || readonly) {
return;
}
setState(parseDateString("", time));
onChange(undefined);
};
const handleClear = useCallback(
(event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (disabled || readonly) {
return;
}
setState(parseDateString("", time));
onChange(undefined);
},
[disabled, readonly, time, onChange]
);

return (
<ul className="list-inline">
Expand Down
23 changes: 16 additions & 7 deletions packages/core/src/components/widgets/CheckboxWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import { getTemplate, schemaRequiresTrueValue, WidgetProps } from "@rjsf/utils";

/** The `CheckBoxWidget` is a widget for rendering boolean properties.
Expand Down Expand Up @@ -30,14 +30,23 @@ function CheckboxWidget<T = any, F = any>({
// "const" or "enum" keywords
const required = schemaRequiresTrueValue(schema);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
onChange(event.target.checked);
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(event.target.checked),
[onChange]
);

const handleBlur = (event: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, event.target.checked);
const handleBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, event.target.checked),
[onBlur, id]
);

const handleFocus = (event: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, event.target.checked);
const handleFocus = useCallback(
(event: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, event.target.checked),
[onFocus, id]
);

return (
<div className={`checkbox ${disabled || readonly ? "disabled" : ""}`}>
Expand Down
13 changes: 6 additions & 7 deletions packages/core/src/components/widgets/DateWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import { getTemplate, WidgetProps } from "@rjsf/utils";

/** The `DateWidget` component uses the `BaseInputTemplate` changing the type to `date` and transforms
Expand All @@ -13,11 +13,10 @@ export default function DateWidget<T = any, F = any>(props: WidgetProps<T, F>) {
registry,
options
);
return (
<BaseInputTemplate
type="date"
{...props}
onChange={(value) => onChange(value || undefined)}
/>
const handleChange = useCallback(
(value: React.ChangeEvent) => onChange(value || undefined),
[onChange]
);

return <BaseInputTemplate type="date" {...props} onChange={handleChange} />;
}
14 changes: 9 additions & 5 deletions packages/core/src/components/widgets/RadioWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FocusEvent } from "react";
import React, { FocusEvent, useCallback } from "react";
import { WidgetProps } from "@rjsf/utils";

/** The `RadioWidget` is a widget for rendering a radio group.
Expand All @@ -24,11 +24,15 @@ function RadioWidget<T = any, F = any>({
// checked={checked} has been moved above name={name}, As mentioned in #349;
// this is a temporary fix for radio button rendering bug in React, facebook/react#7630.

const handleBlur = (event: FocusEvent<HTMLInputElement>) =>
onBlur(id, event.target.value);
const handleBlur = useCallback(
(event: FocusEvent<HTMLInputElement>) => onBlur(id, event.target.value),
[onBlur, id]
);

const handleFocus = (event: FocusEvent<HTMLInputElement>) =>
onFocus(id, event.target.value);
const handleFocus = useCallback(
(event: FocusEvent<HTMLInputElement>) => onFocus(id, event.target.value),
[onFocus, id]
);

return (
<div className="field-radio-group" id={id}>
Expand Down
35 changes: 22 additions & 13 deletions packages/core/src/components/widgets/SelectWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ChangeEvent, FocusEvent } from "react";
import React, { ChangeEvent, FocusEvent, useCallback } from "react";
import { processSelectValue, WidgetProps } from "@rjsf/utils";

function getValue(
Expand Down Expand Up @@ -37,20 +37,29 @@ function SelectWidget<T = any, F = any>({
const { enumOptions, enumDisabled } = options;
const emptyValue = multiple ? [] : "";

const handleFocus = (event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onFocus(id, processSelectValue(schema, newValue, options));
};
const handleFocus = useCallback(
(event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onFocus(id, processSelectValue(schema, newValue, options));
},
[onFocus, id, schema, multiple, options]
);

const handleBlur = (event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onBlur(id, processSelectValue(schema, newValue, options));
};
const handleBlur = useCallback(
(event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onBlur(id, processSelectValue(schema, newValue, options));
},
[onBlur, id, schema, multiple, options]
);

const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onChange(processSelectValue(schema, newValue, options));
};
const handleChange = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onChange(processSelectValue(schema, newValue, options));
},
[onChange, schema, multiple, options]
);

return (
<select
Expand Down
26 changes: 16 additions & 10 deletions packages/core/src/components/widgets/TextareaWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FocusEvent } from "react";
import React, { FocusEvent, useCallback } from "react";
import { WidgetProps } from "@rjsf/utils";

/** The `TextareaWidget` is a widget for rendering input fields as textarea.
Expand All @@ -18,17 +18,23 @@ function TextareaWidget<T = any, F = any>({
onBlur,
onFocus,
}: WidgetProps<T, F>) {
const handleChange = ({
target: { value },
}: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange(value === "" ? options.emptyValue : value);
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange(value === "" ? options.emptyValue : value),
[onChange, options.emptyValue]
);

const handleBlur = ({ target: { value } }: FocusEvent<HTMLTextAreaElement>) =>
onBlur(id, value);
const handleBlur = useCallback(
({ target: { value } }: FocusEvent<HTMLTextAreaElement>) =>
onBlur(id, value),
[onBlur, id]
);

const handleFocus = ({
target: { value },
}: FocusEvent<HTMLTextAreaElement>) => onFocus(id, value);
const handleFocus = useCallback(
({ target: { value } }: FocusEvent<HTMLTextAreaElement>) =>
onFocus(id, value),
[id, onFocus]
);

return (
<textarea
Expand Down