From b02f123f1a6f0271530bcc12058e0773fe2c738d Mon Sep 17 00:00:00 2001 From: Roy Honders Date: Thu, 2 Jan 2025 15:31:08 +0100 Subject: [PATCH] feat: Use React 19 refs (#1069) This commit updates the code to use the simplified ref handling introduced in React 19. - Removed unnecessary `forwardRef` wrappers. - Passed refs directly as props to function components. This change simplifies the code and improves readability while taking advantage of the latest React features. --- packages/components-react/src/Alert.tsx | 82 +++++------ .../components-react/src/FileInput.test.tsx | 5 +- packages/components-react/src/FileInput.tsx | 127 +++++++++--------- packages/storybook/config/Prettify.tsx | 22 ++- 4 files changed, 117 insertions(+), 119 deletions(-) diff --git a/packages/components-react/src/Alert.tsx b/packages/components-react/src/Alert.tsx index a23ae763a..7a3996297 100644 --- a/packages/components-react/src/Alert.tsx +++ b/packages/components-react/src/Alert.tsx @@ -1,51 +1,55 @@ import { Heading, Paragraph, Alert as UtrechtAlert } from '@utrecht/component-library-react'; import clsx from 'clsx'; -import { ForwardedRef, forwardRef, PropsWithChildren, ReactNode } from 'react'; +import { PropsWithChildren, ReactNode, RefObject } from 'react'; import { Icon } from './icon/Icon'; export interface AlertProps { + ref?: RefObject; type: 'info' | 'ok' | 'warning' | 'error'; heading?: ReactNode; headingLevel?: 1 | 2 | 3 | 4 | 5; textContent?: ReactNode; } -export const Alert = forwardRef( - ( - { type, heading, headingLevel, textContent, children, ...restProps }: PropsWithChildren, - ref: ForwardedRef, - ) => { - return ( - -
- -
-
- - {heading} - - {textContent} - {children} -
-
- ); - }, -); +export const Alert = ({ + ref, + type, + heading, + headingLevel, + textContent, + children, + ...restProps +}: PropsWithChildren) => { + return ( + +
+ +
+
+ + {heading} + + {textContent} + {children} +
+
+ ); +}; Alert.displayName = 'Alert'; diff --git a/packages/components-react/src/FileInput.test.tsx b/packages/components-react/src/FileInput.test.tsx index 2fe9743d7..1af1ede1e 100644 --- a/packages/components-react/src/FileInput.test.tsx +++ b/packages/components-react/src/FileInput.test.tsx @@ -1,5 +1,4 @@ import '@testing-library/jest-dom'; - import { fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { FileInput, FileInputProps } from './FileInput'; @@ -10,6 +9,7 @@ beforeAll(() => { describe('File Input tests', () => { const defaultProps: FileInputProps = { + ref: { current: document.createElement('input') }, buttonText: 'Bestanden kiezen', maxFileSizeInBytes: 10_485_760, allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.png,.bmp,.gif', @@ -30,6 +30,7 @@ describe('File Input tests', () => { const mockOnFileChange = jest.fn(); const propsTest: FileInputProps = { + ref: { current: document.createElement('input') }, buttonText: 'Bestanden kiezen', maxFileSizeInBytes: 10_485_760, allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.png,.bmp,.gif', @@ -57,6 +58,7 @@ describe('File Input tests', () => { const mockOnFileChange = jest.fn(); const propsTest: FileInputProps = { + ref: { current: document.createElement('input') }, buttonText: 'Bestanden kiezen', maxFileSizeInBytes: 10_485_760, allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.png,.bmp,.gif', @@ -87,6 +89,7 @@ describe('File Input tests', () => { const mockOnFileChange = jest.fn(); const propsTest: FileInputProps = { + ref: { current: document.createElement('input') }, buttonText: 'Bestanden kiezen', maxFileSizeInBytes: 10_485_760, allowedFileTypes: '.doc,.docx,.xlsx,.pdf,.zip,.jpg,.bmp,.gif', // Removed .png from allow list diff --git a/packages/components-react/src/FileInput.tsx b/packages/components-react/src/FileInput.tsx index a128ae19c..1b636909c 100644 --- a/packages/components-react/src/FileInput.tsx +++ b/packages/components-react/src/FileInput.tsx @@ -1,9 +1,10 @@ import { Paragraph } from '@utrecht/component-library-react'; -import { ChangeEvent, ForwardedRef, forwardRef, PropsWithChildren, useRef, useState } from 'react'; +import { ChangeEvent, PropsWithChildren, RefObject, useState } from 'react'; import { Button, ButtonProps } from './Button'; import { File } from './File'; export interface FileInputProps extends Omit { + ref: RefObject; buttonText: string; buttonAppearance?: ButtonProps['appearance']; maxFileSizeInBytes: number; @@ -13,72 +14,68 @@ export interface FileInputProps extends Omit { onValueChange?: (callbackFiles: File[]) => void; // eslint-disable-line no-unused-vars } -export const FileInput = forwardRef( - ( - { - children, - buttonText, - maxFileSizeInBytes, - allowedFileTypes, - buttonAppearance, - fileSizeErrorMessage, - fileTypeErrorMessage, - onValueChange, - }: PropsWithChildren, - ref: ForwardedRef, - ) => { - const [files, setFiles] = useState([]); - const inputElement = useRef(null); - const onChange = (newFiles: FileList | null) => { - if (newFiles) { - const updatedFiles = [...files, ...Array.from(newFiles)]; - setFiles(updatedFiles); - if (onValueChange) { - onValueChange(updatedFiles); - } +export const FileInput = ({ + ref, + children, + buttonText, + maxFileSizeInBytes, + allowedFileTypes, + buttonAppearance, + fileSizeErrorMessage, + fileTypeErrorMessage, + onValueChange, +}: PropsWithChildren) => { + const [files, setFiles] = useState([]); + const inputElement = ref; + const onChange = (newFiles: FileList | null) => { + if (newFiles) { + const updatedFiles = [...files, ...Array.from(newFiles)]; + setFiles(updatedFiles); + if (onValueChange) { + onValueChange(updatedFiles); } - }; + } + }; - return ( -
- {children} - ) => { - onChange(event.target.files); - }} - /> -
- - {files.length === 0 && Geen bestand gekozen} -
-
- {files.map((item: File) => { - return ( - setFiles(files.filter((file) => file !== fileToRemove))} - /> - ); - })} -
+ return ( +
+ {children} + ) => { + onChange(event.target.files); + }} + /> +
+ + {files.length === 0 && Geen bestand gekozen}
- ); - }, -); +
+ {files.map((item: File) => { + return ( + setFiles(files.filter((file) => file !== fileToRemove))} + /> + ); + })} +
+
+ ); +}; FileInput.displayName = 'FileInput'; diff --git a/packages/storybook/config/Prettify.tsx b/packages/storybook/config/Prettify.tsx index ba4dc68f9..b774c3c39 100644 --- a/packages/storybook/config/Prettify.tsx +++ b/packages/storybook/config/Prettify.tsx @@ -1,24 +1,18 @@ import * as prettier from 'prettier'; import pluginHTML from 'prettier/plugins/html'; -import { useEffect, useState } from 'react'; +import { use } from 'react'; interface Props { ugly: string; } export function Prettify({ ugly }: Props): string { - const [prettySrc, setPrettySrc] = useState(''); - useEffect(() => { - const prettify = async () => { - const pretty = await prettier.format(ugly, { - parser: 'html', - plugins: [pluginHTML], - }); - setPrettySrc(pretty); - }; + const prettify = async () => { + return prettier.format(ugly, { + parser: 'html', + plugins: [pluginHTML], + }); + }; - prettify(); - }, []); - - return prettySrc; + return use(prettify()); }