Skip to content
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

Change class components to functional components #5431

Merged
merged 4 commits into from
Oct 26, 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
6 changes: 6 additions & 0 deletions .changeset/spotty-pots-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'react-select': patch
'@react-select/docs': patch
---

Change `class` components to `functional` components
140 changes: 55 additions & 85 deletions docs/ExampleWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsx jsx */
import { jsx } from '@emotion/react'; // eslint-disable-line no-unused-vars
import { CSSObject } from '@emotion/serialize';
import { Component } from 'react';
import { ReactNode, useState } from 'react';
import CodeSandboxer, { GitInfo } from 'react-codesandboxer';
import { CodeBlock } from './markdown/renderer';
import pkg from '../packages/react-select/package.json';
Expand All @@ -20,99 +20,69 @@ const gitInfo: GitInfo = {
const sourceUrl = `https://github.com/${gitInfo.account}/react-select/tree/${gitInfo.branch}`;

interface Props {
children?: ReactNode;
readonly label: string;
readonly raw: { readonly default: string };
readonly urlPath: string;
readonly isEditable?: boolean;
}

interface State {
readonly showCode: boolean;
}

export default class ExampleWrapper extends Component<Props, State> {
state: State = { showCode: false };
static defaultProps = { isEditable: true };

renderCodeSample = () => {
let { raw } = this.props;
let { showCode } = this.state;

if (!showCode || !raw) {
return null;
} else {
return <CodeBlock literal={raw.default} codeinfo={['jsx']} />;
}
};

renderSourceViewOption = () => {
let { raw } = this.props;
let { showCode } = this.state;

if (!raw) {
return (
<AAction
href={`${sourceUrl}/${this.props.urlPath}`}
target="_blank"
title="View Source"
>
<SourceIcon />
</AAction>
);
} else {
return (
<ButtonAction
onClick={() => this.setState({ showCode: !showCode })}
title="View Source"
>
<SourceIcon />
</ButtonAction>
);
}
};

renderCSBButton = () => {
let { isEditable, raw, urlPath } = this.props;
export default ({
children,
label,
raw,
urlPath,
isEditable = true,
}: Props) => {
const [showCode, setShowCode] = useState(false);

if (isEditable) {
return (
<CodeSandboxer
example={raw.default}
examplePath={urlPath}
pkgJSON={pkg}
gitInfo={gitInfo}
dependencies={{
[pkg.name]: pkg.version,
}}
>
{({ isLoading }) => (
<ButtonAction title="Edit in CodeSandbox">
{isLoading ? <Spinner /> : <NewWindowIcon />}
return (
<div>
<ExampleHeading>
<h4>{label}</h4>
<Actions>
{raw ? (
<ButtonAction
onClick={() => setShowCode((prev) => !prev)}
title="View Source"
>
<SourceIcon />
</ButtonAction>
) : (
<AAction
href={`${sourceUrl}/${urlPath}`}
target="_blank"
title="View Source"
>
<SourceIcon />
</AAction>
)}
</CodeSandboxer>
);
} else {
return null;
}
};

render() {
return (
<div>
<ExampleHeading>
<h4>{this.props.label}</h4>
<Actions>
{this.renderSourceViewOption()}
{this.renderCSBButton()}
</Actions>
</ExampleHeading>
{this.renderCodeSample()}
{this.props.children}
</div>
);
}
}
{isEditable ? (
<CodeSandboxer
example={raw.default}
examplePath={urlPath}
pkgJSON={pkg}
gitInfo={gitInfo}
dependencies={{
[pkg.name]: pkg.version,
}}
>
{({ isLoading }) => (
<ButtonAction title="Edit in CodeSandbox">
{isLoading ? <Spinner /> : <NewWindowIcon />}
</ButtonAction>
)}
</CodeSandboxer>
) : null}
</Actions>
</ExampleHeading>
{showCode && raw ? (
<CodeBlock literal={raw.default} codeinfo={['jsx']} />
) : null}
{children}
</div>
);
};

const ExampleHeading = (props: any) => (
<div
Expand Down
8 changes: 4 additions & 4 deletions docs/examples/ControlledMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export default () => {

const toggleMenuIsOpen = () => {
setMenuIsOpen((value) => !value);
const { current } = ref;
if (!current) return;
if (menuIsOpen) current.blur();
else current.focus();
const selectEl = ref.current;
if (!selectEl) return;
if (menuIsOpen) selectEl.blur();
else selectEl.focus();
};

return (
Expand Down
29 changes: 9 additions & 20 deletions packages/react-select/src/NonceProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
import * as React from 'react';
import { Component, ReactNode } from 'react';
import { useMemo } from 'react';
import { ReactNode } from 'react';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import memoizeOne from 'memoize-one';

interface NonceProviderProps {
nonce: string;
children: ReactNode;
cacheKey: string;
}

export default class NonceProvider extends Component<NonceProviderProps> {
constructor(props: NonceProviderProps) {
super(props);
this.createEmotionCache = memoizeOne(this.createEmotionCache);
}
createEmotionCache = (nonce: string, key: string) => {
return createCache({ nonce, key });
};
render() {
const emotionCache = this.createEmotionCache(
this.props.nonce,
this.props.cacheKey
);
return (
<CacheProvider value={emotionCache}>{this.props.children}</CacheProvider>
);
}
}
export default ({ nonce, children, cacheKey }: NonceProviderProps) => {
const emotionCache = useMemo(
() => createCache({ key: cacheKey, nonce }),
[cacheKey, nonce]
);
return <CacheProvider value={emotionCache}>{children}</CacheProvider>;
};
146 changes: 58 additions & 88 deletions packages/react-select/src/animated/transitions.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import * as React from 'react';
import {
Component,
ComponentType,
createRef,
CSSProperties,
ReactNode,
useRef,
} from 'react';
import { useEffect, useState } from 'react';
import { ComponentType, CSSProperties, ReactNode, useRef } from 'react';
import { Transition } from 'react-transition-group';
import {
ExitHandler,
Expand Down Expand Up @@ -72,93 +66,69 @@ interface CollapseProps {
in?: boolean;
onExited?: ExitHandler<undefined | HTMLElement>;
}
interface CollapseState {
width: Width;
}

// wrap each MultiValue with a collapse transition; decreases width until
// finally removing from DOM
export class Collapse extends Component<CollapseProps, CollapseState> {
duration = collapseDuration;
rafID?: number | null;
state: CollapseState = { width: 'auto' };
transition: { [K in TransitionStatus]?: CSSProperties } = {
exiting: { width: 0, transition: `width ${this.duration}ms ease-out` },
exited: { width: 0 },
};
nodeRef = createRef<HTMLDivElement>();
export const Collapse = ({ children, in: _in, onExited }: CollapseProps) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that in is from "react-transition-group"—reserved word for a prop was such a bad idea...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed 🥲

const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<Width>('auto');

componentDidMount() {
const { current: ref } = this.nodeRef;
useEffect(() => {
const el = ref.current;
if (!el) return;

/*
A check on existence of ref should not be necessary at this point,
but TypeScript demands it.
Here we're invoking requestAnimationFrame with a callback invoking our
call to getBoundingClientRect and setState in order to resolve an edge case
around portalling. Certain portalling solutions briefly remove children from the DOM
before appending them to the target node. This is to avoid us trying to call getBoundingClientrect
while the Select component is in this state.
*/
if (ref) {
/*
Here we're invoking requestAnimationFrame with a callback invoking our
call to getBoundingClientRect and setState in order to resolve an edge case
around portalling. Certain portalling solutions briefly remove children from the DOM
before appending them to the target node. This is to avoid us trying to call getBoundingClientrect
while the Select component is in this state.
*/
// cannot use `offsetWidth` because it is rounded
this.rafID = window.requestAnimationFrame(() => {
const { width } = ref.getBoundingClientRect();
this.setState({ width });
});
}
}
// cannot use `offsetWidth` because it is rounded
const rafId = window.requestAnimationFrame(() =>
setWidth(el.getBoundingClientRect().width)
);

componentWillUnmount() {
if (this.rafID) {
window.cancelAnimationFrame(this.rafID);
return () => window.cancelAnimationFrame(rafId);
}, []);

const getStyleFromStatus = (status: TransitionStatus) => {
switch (status) {
default:
return { width };
case 'exiting':
return { width: 0, transition: `width ${collapseDuration}ms ease-out` };
case 'exited':
return { width: 0 };
}
}

// get base styles
getStyle = (width: Width): CSSProperties => ({
overflow: 'hidden',
whiteSpace: 'nowrap',
width,
});

// get transition styles
getTransition = (state: TransitionStatus) => this.transition[state];

render() {
const { children, in: inProp, onExited } = this.props;
const exitedProp = () => {
if (this.nodeRef.current && onExited) {
onExited(this.nodeRef.current);
}
};

const { width } = this.state;
};

return (
<Transition
enter={false}
mountOnEnter
unmountOnExit
in={inProp}
onExited={exitedProp}
timeout={this.duration}
nodeRef={this.nodeRef}
>
{(state) => {
const style = {
...this.getStyle(width),
...this.getTransition(state),
};
return (
<div ref={this.nodeRef} style={style}>
{children}
</div>
);
}}
</Transition>
);
}
}
return (
<Transition
enter={false}
mountOnEnter
unmountOnExit
in={_in}
onExited={() => {
const el = ref.current;
if (!el) return;
onExited?.(el);
}}
timeout={collapseDuration}
nodeRef={ref}
>
{(status) => (
<div
ref={ref}
style={{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could the nested ternaries be against an object with state keys, or some getStyleFromState fn with a switch?

overflow: 'hidden',
whiteSpace: 'nowrap',
...getStyleFromStatus(status),
}}
>
{children}
</div>
)}
</Transition>
);
};
Loading